@pixi/sound
Version:
WebAudio API playback library with filters
1 lines • 168 kB
Source Map (JSON)
{"version":3,"file":"pixi-sound.mjs","sources":["../src/instance.ts","../src/filters/Filter.ts","../src/filters/DistortionFilter.ts","../src/webaudio/WebAudioUtils.ts","../src/filters/EqualizerFilter.ts","../src/filters/MonoFilter.ts","../src/filters/ReverbFilter.ts","../src/filters/StereoFilter.ts","../src/filters/StreamFilter.ts","../src/filters/TelephoneFilter.ts","../src/htmlaudio/HTMLAudioContext.ts","../src/htmlaudio/HTMLAudioInstance.ts","../src/htmlaudio/HTMLAudioMedia.ts","../src/SoundSprite.ts","../src/utils/supported.ts","../src/webaudio/WebAudioInstance.ts","../src/Filterable.ts","../src/webaudio/WebAudioNodes.ts","../src/webaudio/WebAudioMedia.ts","../src/Sound.ts","../src/webaudio/WebAudioContext.ts","../src/SoundLibrary.ts","../src/utils/playOnce.ts","../src/utils/render.ts","../src/utils/sineTone.ts","../src/soundAsset.ts","../src/index.ts"],"sourcesContent":["import { SoundLibrary } from './SoundLibrary';\n\n/**\n * Singleton instance of the SoundLibrary\n */\nlet instance: SoundLibrary;\n\n/**\n * Internal set function for the singleton instance.\n * @ignore\n * @param sound - - Sound library instance\n */\nfunction setInstance(sound: SoundLibrary): SoundLibrary\n{\n instance = sound;\n\n return sound;\n}\n\n/**\n * Internal get function for the singleton instance.\n * @ignore\n */\nfunction getInstance(): SoundLibrary\n{\n return instance;\n}\n\nexport { getInstance, instance, setInstance };\n","/**\n * Represents a single sound element. Can be used to play, pause, etc. sound instances.\n *\n * @memberof filters\n */\nclass Filter\n{\n /** The node to connect for the filter to the previous filter. */\n public destination: AudioNode;\n\n /** The node to connect for the filter to the previous filter. */\n public source: AudioNode;\n\n /**\n * @param {AudioNode} destination - The audio node to use as the destination for the input AudioNode\n * @param {AudioNode} [source] - Optional output node, defaults to destination node. This is useful\n * when creating filters which contains multiple AudioNode elements chained together.\n */\n constructor(destination: AudioNode, source?: AudioNode)\n {\n this.init(destination, source);\n }\n\n /** Reinitialize */\n protected init(destination: AudioNode, source?: AudioNode): void\n {\n this.destination = destination;\n this.source = source || destination;\n }\n\n /**\n * Connect to the destination.\n * @param {AudioNode} destination - The destination node to connect the output to\n */\n public connect(destination: AudioNode): void\n {\n this.source?.connect(destination);\n }\n\n /** Completely disconnect filter from destination and source nodes. */\n public disconnect(): void\n {\n this.source?.disconnect();\n }\n\n /** Destroy the filter and don't use after this. */\n public destroy(): void\n {\n this.disconnect();\n this.destination = null;\n this.source = null;\n }\n}\n\nexport { Filter };\n","import { getInstance } from '../instance';\nimport { Filter } from './Filter';\n\n/**\n * Filter for adding adding delaynode.\n *\n * @memberof filters\n */\nclass DistortionFilter extends Filter\n{\n /** The Wave shape node use to distort */\n private _distortion: WaveShaperNode;\n\n /** The amount of distoration */\n private _amount: number;\n\n /** @param amount - The amount of distoration from 0 to 1. */\n constructor(amount = 0)\n {\n let distortion: WaveShaperNode;\n\n if (!getInstance().useLegacy)\n {\n const { audioContext } = getInstance().context;\n\n distortion = audioContext.createWaveShaper();\n }\n\n super(distortion);\n\n this._distortion = distortion;\n\n this.amount = amount;\n }\n\n /** The amount of distortion to set. */\n set amount(value: number)\n {\n this._amount = value;\n if (getInstance().useLegacy)\n {\n return;\n }\n const scaledValue = value * 1000;\n const samples = 44100;\n const curve: Float32Array = new Float32Array(samples);\n const deg: number = Math.PI / 180;\n\n let i = 0;\n let x: number;\n\n for (; i < samples; ++i)\n {\n x = (i * 2 / samples) - 1;\n curve[i] = (3 + scaledValue) * x * 20 * deg / (Math.PI + (scaledValue * Math.abs(x)));\n }\n this._distortion.curve = curve;\n this._distortion.oversample = '4x';\n }\n get amount(): number\n {\n return this._amount;\n }\n\n public destroy(): void\n {\n this._distortion = null;\n super.destroy();\n }\n}\n\nexport { DistortionFilter };\n","import { getInstance } from '../instance';\nimport { WebAudioContext } from './WebAudioContext';\n\n/**\n * Internal class for Web Audio abstractions and convenience methods.\n * @memberof webaudio\n */\nclass WebAudioUtils\n{\n /**\n * Dezippering is removed in the future Web Audio API, instead\n * we use the `setValueAtTime` method, however, this is not available\n * in all environments (e.g., Android webview), so we fallback to the `value` setter.\n * @param param - AudioNode parameter object\n * @param value - Value to set\n * @return The value set\n */\n public static setParamValue(param: AudioParam, value: number): number\n {\n if (param.setValueAtTime)\n {\n const context = getInstance().context as WebAudioContext;\n\n param.setValueAtTime(value, context.audioContext.currentTime);\n }\n else\n {\n param.value = value;\n }\n\n return value;\n }\n}\n\nexport { WebAudioUtils };\n","import { getInstance } from '../instance';\nimport { WebAudioUtils } from '../webaudio/WebAudioUtils';\nimport { Filter } from './Filter';\n\ninterface Band\n{\n f: number;\n type: string;\n gain: number;\n}\n\n/**\n * Filter for adding equalizer bands.\n *\n * @memberof filters\n */\nclass EqualizerFilter extends Filter\n{\n /**\n * Band at 32 Hz\n * @readonly\n */\n public static readonly F32: number = 32;\n\n /**\n * Band at 64 Hz\n * @readonly\n */\n public static readonly F64: number = 64;\n\n /**\n * Band at 125 Hz\n * @readonly\n */\n public static readonly F125: number = 125;\n\n /**\n * Band at 250 Hz\n * @readonly\n */\n public static readonly F250: number = 250;\n\n /**\n * Band at 500 Hz\n * @readonly\n */\n public static readonly F500: number = 500;\n\n /**\n * Band at 1000 Hz\n * @readonly\n */\n public static readonly F1K: number = 1000;\n\n /**\n * Band at 2000 Hz\n * @readonly\n */\n public static readonly F2K: number = 2000;\n\n /**\n * Band at 4000 Hz\n * @readonly\n */\n public static readonly F4K: number = 4000;\n\n /**\n * Band at 8000 Hz\n * @readonly\n */\n public static readonly F8K: number = 8000;\n\n /**\n * Band at 16000 Hz\n * @readonly\n */\n public static readonly F16K: number = 16000;\n\n /**\n * The list of bands\n * @readonly\n */\n public readonly bands: BiquadFilterNode[];\n\n /**\n * The map of bands to frequency\n * @readonly\n */\n public readonly bandsMap: Record<number, BiquadFilterNode>;\n\n /**\n * @param f32 - Default gain for 32 Hz\n * @param f64 - Default gain for 64 Hz\n * @param f125 - Default gain for 125 Hz\n * @param f250 - Default gain for 250 Hz\n * @param f500 - Default gain for 500 Hz\n * @param f1k - Default gain for 1000 Hz\n * @param f2k - Default gain for 2000 Hz\n * @param f4k - Default gain for 4000 Hz\n * @param f8k - Default gain for 8000 Hz\n * @param f16k - Default gain for 16000 Hz\n */\n constructor(f32 = 0, f64 = 0, f125 = 0, f250 = 0, f500 = 0,\n f1k = 0, f2k = 0, f4k = 0, f8k = 0, f16k = 0)\n {\n let bands: BiquadFilterNode[] = [];\n\n const equalizerBands: Band[] = [\n {\n f: EqualizerFilter.F32,\n type: 'lowshelf',\n gain: f32,\n },\n {\n f: EqualizerFilter.F64,\n type: 'peaking',\n gain: f64,\n },\n {\n f: EqualizerFilter.F125,\n type: 'peaking',\n gain: f125,\n },\n {\n f: EqualizerFilter.F250,\n type: 'peaking',\n gain: f250,\n },\n {\n f: EqualizerFilter.F500,\n type: 'peaking',\n gain: f500,\n },\n {\n f: EqualizerFilter.F1K,\n type: 'peaking',\n gain: f1k,\n },\n {\n f: EqualizerFilter.F2K,\n type: 'peaking',\n gain: f2k,\n },\n {\n f: EqualizerFilter.F4K,\n type: 'peaking',\n gain: f4k,\n },\n {\n f: EqualizerFilter.F8K,\n type: 'peaking',\n gain: f8k,\n },\n {\n f: EqualizerFilter.F16K,\n type: 'highshelf',\n gain: f16k,\n },\n ];\n\n if (!getInstance().useLegacy)\n {\n bands = equalizerBands.map((band: Band) =>\n {\n const node: BiquadFilterNode = getInstance().context.audioContext.createBiquadFilter();\n\n node.type = band.type as BiquadFilterType;\n WebAudioUtils.setParamValue(node.Q, 1);\n node.frequency.value = band.f; // WebAudioUtils.setParamValue(filter.frequency, band.f);\n WebAudioUtils.setParamValue(node.gain, band.gain);\n\n return node;\n });\n }\n\n // Setup the constructor AudioNode, where first is the input, and last is the output\n super(bands[0], bands[bands.length - 1]);\n\n // Manipulate the bands\n this.bands = bands;\n\n // Create a map\n this.bandsMap = {};\n\n for (let i = 0; i < this.bands.length; i++)\n {\n const node: BiquadFilterNode = this.bands[i];\n\n // Connect the previous band to the current one\n if (i > 0)\n {\n this.bands[i - 1].connect(node);\n }\n this.bandsMap[node.frequency.value] = node;\n }\n }\n\n /**\n * Set gain on a specific frequency.\n * @param frequency - The frequency, see EqualizerFilter.F* for bands\n * @param gain - Recommended -40 to 40.\n */\n public setGain(frequency: number, gain = 0): void\n {\n if (!this.bandsMap[frequency])\n {\n throw new Error(`No band found for frequency ${frequency}`);\n }\n WebAudioUtils.setParamValue(this.bandsMap[frequency].gain, gain);\n }\n\n /**\n * Get gain amount on a specific frequency.\n * @return The amount of gain set.\n */\n public getGain(frequency: number): number\n {\n if (!this.bandsMap[frequency])\n {\n throw new Error(`No band found for frequency ${frequency}`);\n }\n\n return this.bandsMap[frequency].gain.value;\n }\n\n /**\n * Gain at 32 Hz frequencey.\n * @default 0\n */\n public set f32(value: number)\n {\n this.setGain(EqualizerFilter.F32, value);\n }\n public get f32(): number\n {\n return this.getGain(EqualizerFilter.F32);\n }\n\n /**\n * Gain at 64 Hz frequencey.\n * @default 0\n */\n public set f64(value: number)\n {\n this.setGain(EqualizerFilter.F64, value);\n }\n public get f64(): number\n {\n return this.getGain(EqualizerFilter.F64);\n }\n\n /**\n * Gain at 125 Hz frequencey.\n * @default 0\n */\n public set f125(value: number)\n {\n this.setGain(EqualizerFilter.F125, value);\n }\n public get f125(): number\n {\n return this.getGain(EqualizerFilter.F125);\n }\n\n /**\n * Gain at 250 Hz frequencey.\n * @default 0\n */\n public set f250(value: number)\n {\n this.setGain(EqualizerFilter.F250, value);\n }\n public get f250(): number\n {\n return this.getGain(EqualizerFilter.F250);\n }\n\n /**\n * Gain at 500 Hz frequencey.\n * @default 0\n */\n public set f500(value: number)\n {\n this.setGain(EqualizerFilter.F500, value);\n }\n public get f500(): number\n {\n return this.getGain(EqualizerFilter.F500);\n }\n\n /**\n * Gain at 1 KHz frequencey.\n * @default 0\n */\n public set f1k(value: number)\n {\n this.setGain(EqualizerFilter.F1K, value);\n }\n public get f1k(): number\n {\n return this.getGain(EqualizerFilter.F1K);\n }\n\n /**\n * Gain at 2 KHz frequencey.\n * @default 0\n */\n public set f2k(value: number)\n {\n this.setGain(EqualizerFilter.F2K, value);\n }\n public get f2k(): number\n {\n return this.getGain(EqualizerFilter.F2K);\n }\n\n /**\n * Gain at 4 KHz frequencey.\n * @default 0\n */\n public set f4k(value: number)\n {\n this.setGain(EqualizerFilter.F4K, value);\n }\n public get f4k(): number\n {\n return this.getGain(EqualizerFilter.F4K);\n }\n\n /**\n * Gain at 8 KHz frequencey.\n * @default 0\n */\n public set f8k(value: number)\n {\n this.setGain(EqualizerFilter.F8K, value);\n }\n public get f8k(): number\n {\n return this.getGain(EqualizerFilter.F8K);\n }\n\n /**\n * Gain at 16 KHz frequencey.\n * @default 0\n */\n public set f16k(value: number)\n {\n this.setGain(EqualizerFilter.F16K, value);\n }\n public get f16k(): number\n {\n return this.getGain(EqualizerFilter.F16K);\n }\n\n /** Reset all frequency bands to have gain of 0 */\n public reset(): void\n {\n this.bands.forEach((band: BiquadFilterNode) =>\n {\n WebAudioUtils.setParamValue(band.gain, 0);\n });\n }\n\n public destroy(): void\n {\n this.bands.forEach((band: BiquadFilterNode) =>\n {\n band.disconnect();\n });\n (this as any).bands = null;\n (this as any).bandsMap = null;\n }\n}\n\nexport { EqualizerFilter };\n","import { getInstance } from '../instance';\nimport { Filter } from './Filter';\n\n/**\n * Combine all channels into mono channel.\n *\n * @memberof filters\n */\nclass MonoFilter extends Filter\n{\n /** Merger node */\n private _merger: ChannelMergerNode;\n\n constructor()\n {\n let merger: ChannelMergerNode;\n let splitter: ChannelSplitterNode;\n\n if (!getInstance().useLegacy)\n {\n const { audioContext } = getInstance().context;\n\n splitter = audioContext.createChannelSplitter();\n merger = audioContext.createChannelMerger();\n merger.connect(splitter);\n }\n super(merger, splitter);\n this._merger = merger;\n }\n\n public destroy(): void\n {\n this._merger?.disconnect();\n this._merger = null;\n super.destroy();\n }\n}\n\nexport { MonoFilter };\n","import { getInstance } from '../instance';\nimport { Filter } from './Filter';\n\n/**\n * Filter for adding reverb. Refactored from\n * https://github.com/web-audio-components/simple-reverb/\n *\n * @memberof filters\n */\nclass ReverbFilter extends Filter\n{\n private _seconds: number;\n private _decay: number;\n private _reverse: boolean;\n\n /**\n * @param seconds - Seconds for reverb\n * @param decay - The decay length\n * @param reverse - Reverse reverb\n */\n constructor(seconds = 3, decay = 2, reverse = false)\n {\n super(null);\n this._seconds = this._clamp(seconds, 1, 50);\n this._decay = this._clamp(decay, 0, 100);\n this._reverse = reverse;\n this._rebuild();\n }\n\n /**\n * Clamp a value\n * @param value\n * @param min - Minimum value\n * @param max - Maximum value\n * @return Clamped number\n */\n private _clamp(value: number, min: number, max: number): number\n {\n return Math.min(max, Math.max(min, value));\n }\n\n /**\n * Length of reverb in seconds from 1 to 50\n * @default 3\n */\n get seconds(): number\n {\n return this._seconds;\n }\n set seconds(seconds: number)\n {\n this._seconds = this._clamp(seconds, 1, 50);\n this._rebuild();\n }\n\n /**\n * Decay value from 0 to 100\n * @default 2\n */\n get decay(): number\n {\n return this._decay;\n }\n set decay(decay: number)\n {\n this._decay = this._clamp(decay, 0, 100);\n this._rebuild();\n }\n\n /**\n * Reverse value from 0 to 1\n * @default false\n */\n get reverse(): boolean\n {\n return this._reverse;\n }\n set reverse(reverse: boolean)\n {\n this._reverse = reverse;\n this._rebuild();\n }\n\n /**\n * Utility function for building an impulse response\n * from the module parameters.\n */\n private _rebuild(): void\n {\n if (getInstance().useLegacy)\n {\n return;\n }\n const { audioContext } = getInstance().context;\n const rate: number = audioContext.sampleRate;\n const length: number = rate * this._seconds;\n const impulse: AudioBuffer = audioContext.createBuffer(2, length, rate);\n const impulseL: Float32Array = impulse.getChannelData(0);\n const impulseR: Float32Array = impulse.getChannelData(1);\n let n: number;\n\n for (let i = 0; i < length; i++)\n {\n n = this._reverse ? length - i : i;\n impulseL[i] = ((Math.random() * 2) - 1) * Math.pow(1 - (n / length), this._decay);\n impulseR[i] = ((Math.random() * 2) - 1) * Math.pow(1 - (n / length), this._decay);\n }\n const convolver = audioContext.createConvolver();\n\n convolver.buffer = impulse;\n this.init(convolver);\n }\n}\n\nexport { ReverbFilter };\n","import { getInstance } from '../instance';\nimport { WebAudioUtils } from '../webaudio/WebAudioUtils';\nimport { Filter } from './Filter';\n\n/**\n * Filter for adding Stereo panning.\n *\n * @memberof filters\n */\nclass StereoFilter extends Filter\n{\n /** The stereo panning node */\n private _stereo: StereoPannerNode;\n\n /** The stereo panning node */\n private _panner: PannerNode;\n\n /** The amount of panning, -1 is left, 1 is right, 0 is centered */\n private _pan: number;\n\n /** @param pan - The amount of panning, -1 is left, 1 is right, 0 is centered. */\n constructor(pan = 0)\n {\n let stereo: StereoPannerNode;\n let panner: PannerNode;\n let destination: AudioNode;\n\n if (!getInstance().useLegacy)\n {\n const { audioContext } = getInstance().context;\n\n if (audioContext.createStereoPanner)\n {\n stereo = audioContext.createStereoPanner();\n destination = stereo;\n }\n else\n {\n panner = audioContext.createPanner();\n panner.panningModel = 'equalpower';\n destination = panner;\n }\n }\n\n super(destination);\n\n this._stereo = stereo;\n this._panner = panner;\n\n this.pan = pan;\n }\n\n /** Set the amount of panning, where -1 is left, 1 is right, and 0 is centered */\n set pan(value: number)\n {\n this._pan = value;\n if (this._stereo)\n {\n WebAudioUtils.setParamValue(this._stereo.pan, value);\n }\n else if (this._panner)\n {\n this._panner.setPosition(value, 0, 1 - Math.abs(value));\n }\n }\n get pan(): number\n {\n return this._pan;\n }\n\n public destroy(): void\n {\n super.destroy();\n this._stereo = null;\n this._panner = null;\n }\n}\n\nexport { StereoFilter };\n","import { getInstance } from '../instance';\nimport { Filter } from './Filter';\n\n/**\n * Export a MediaStream to be recorded\n *\n * @memberof filters\n */\nclass StreamFilter extends Filter\n{\n private _stream: MediaStream;\n\n constructor()\n {\n let destination: MediaStreamAudioDestinationNode;\n let source: MediaStreamAudioSourceNode;\n\n if (!getInstance().useLegacy)\n {\n const { audioContext } = getInstance().context;\n\n destination = audioContext.createMediaStreamDestination();\n source = audioContext.createMediaStreamSource(destination.stream);\n }\n\n super(destination, source);\n this._stream = destination?.stream;\n }\n\n public get stream(): MediaStream\n {\n return this._stream;\n }\n\n public destroy(): void\n {\n this._stream = null;\n super.destroy();\n }\n}\n\nexport { StreamFilter };\n","import { getInstance } from '../instance';\nimport { WebAudioUtils } from '../webaudio/WebAudioUtils';\nimport { Filter } from './Filter';\n\n/**\n * Creates a telephone-sound filter.\n *\n * @memberof filters\n */\nclass TelephoneFilter extends Filter\n{\n constructor()\n {\n let destination: AudioNode;\n let source: AudioNode;\n\n if (!getInstance().useLegacy)\n {\n const { audioContext } = getInstance().context;\n const lpf1 = audioContext.createBiquadFilter();\n const lpf2 = audioContext.createBiquadFilter();\n const hpf1 = audioContext.createBiquadFilter();\n const hpf2 = audioContext.createBiquadFilter();\n\n lpf1.type = 'lowpass';\n WebAudioUtils.setParamValue(lpf1.frequency, 2000.0);\n\n lpf2.type = 'lowpass';\n WebAudioUtils.setParamValue(lpf2.frequency, 2000.0);\n\n hpf1.type = 'highpass';\n WebAudioUtils.setParamValue(hpf1.frequency, 500.0);\n\n hpf2.type = 'highpass';\n WebAudioUtils.setParamValue(hpf2.frequency, 500.0);\n\n lpf1.connect(lpf2);\n lpf2.connect(hpf1);\n hpf1.connect(hpf2);\n\n destination = lpf1;\n source = hpf2;\n }\n\n super(destination, source);\n }\n}\n\nexport { TelephoneFilter };\n","import { EventEmitter } from 'pixi.js';\nimport { Filter } from '../filters/Filter';\nimport { IMediaContext } from '../interfaces/IMediaContext';\n\n/**\n * The fallback version of WebAudioContext which uses `<audio>` instead of WebAudio API.\n * @memberof htmlaudio\n * @extends PIXI.EventEmitter\n */\nclass HTMLAudioContext extends EventEmitter implements IMediaContext\n{\n /** Current global speed from 0 to 1 */\n public speed = 1;\n\n /** Current muted status of the context */\n public muted = false;\n\n /** Current volume from 0 to 1 */\n public volume = 1;\n\n /** Current paused status */\n public paused = false;\n\n /** Internal trigger when volume, mute or speed changes */\n public refresh(): void\n {\n this.emit('refresh');\n }\n\n /** Internal trigger paused changes */\n public refreshPaused(): void\n {\n this.emit('refreshPaused');\n }\n\n /**\n * HTML Audio does not support filters, this is non-functional API.\n */\n public get filters(): Filter[]\n {\n console.warn('HTML Audio does not support filters');\n\n return null;\n }\n public set filters(_filters: Filter[])\n {\n console.warn('HTML Audio does not support filters');\n }\n\n /**\n * HTML Audio does not support `audioContext`\n * @readonly\n * @type {AudioContext}\n */\n public get audioContext(): AudioContext\n {\n console.warn('HTML Audio does not support audioContext');\n\n return null;\n }\n\n /**\n * Toggles the muted state.\n * @return The current muted state.\n */\n public toggleMute(): boolean\n {\n this.muted = !this.muted;\n this.refresh();\n\n return this.muted;\n }\n\n /**\n * Toggles the paused state.\n * @return The current paused state.\n */\n public togglePause(): boolean\n {\n this.paused = !this.paused;\n this.refreshPaused();\n\n return this.paused;\n }\n\n /** Destroy and don't use after this */\n public destroy(): void\n {\n this.removeAllListeners();\n }\n}\n\nexport { HTMLAudioContext };\n","import { EventEmitter, Ticker } from 'pixi.js';\nimport { Filter } from '../filters/Filter';\nimport { IMediaInstance } from '../interfaces/IMediaInstance';\nimport { PlayOptions } from '../Sound';\nimport { HTMLAudioMedia } from './HTMLAudioMedia';\n\nlet id = 0;\n\n/**\n * Instance which wraps the `<audio>` element playback.\n * @memberof htmlaudio\n * @extends PIXI.EventEmitter\n */\nclass HTMLAudioInstance extends EventEmitter implements IMediaInstance\n{\n /** Extra padding, in seconds, to deal with low-latecy of HTMLAudio. */\n public static readonly PADDING: number = 0.1;\n\n /** The current unique ID for this instance. */\n public readonly id: number;\n\n /** The instance of the Audio element. */\n private _source: HTMLAudioElement;\n\n /** The instance of the Audio media element. */\n private _media: HTMLAudioMedia;\n\n /** Playback rate, where 1 is 100%. */\n private _end: number;\n\n /** Current instance paused state. */\n private _paused: boolean;\n\n /** Current instance muted state. */\n private _muted: boolean;\n\n /** Current actual paused state. */\n private _pausedReal: boolean;\n\n /** Total length of the audio. */\n private _duration: number;\n\n /** Playback rate, where 1 is 100%. */\n private _start: number;\n\n /** `true` if the audio is actually playing. */\n private _playing: boolean;\n\n /** Volume for the instance. */\n private _volume: number;\n\n /** Speed for the instance. */\n private _speed: number;\n\n /** `true` for looping the playback */\n private _loop: boolean;\n\n /** @param parent - Parent element */\n constructor(parent: HTMLAudioMedia)\n {\n super();\n\n this.id = id++;\n\n this.init(parent);\n }\n\n /**\n * Set a property by name, this makes it easy to chain values\n * @param name - Name of the property to set\n * @param value - Value to set property to\n */\n public set(name: 'speed' | 'volume' | 'muted' | 'loop' | 'paused', value: number | boolean): this\n {\n if (this[name] === undefined)\n {\n throw new Error(`Property with name ${name} does not exist.`);\n }\n else\n {\n switch (name)\n {\n case 'speed': this.speed = value as number; break;\n case 'volume': this.volume = value as number; break;\n case 'paused': this.paused = value as boolean; break;\n case 'loop': this.loop = value as boolean; break;\n case 'muted': this.muted = value as boolean; break;\n }\n }\n\n return this;\n }\n\n /** The current playback progress from 0 to 1. */\n public get progress(): number\n {\n const { currentTime } = this._source;\n\n return currentTime / this._duration;\n }\n\n /** Pauses the sound. */\n public get paused(): boolean\n {\n return this._paused;\n }\n public set paused(paused: boolean)\n {\n this._paused = paused;\n this.refreshPaused();\n }\n\n /**\n * Reference: http://stackoverflow.com/a/40370077\n * @private\n */\n private _onPlay(): void\n {\n this._playing = true;\n }\n\n /**\n * Reference: http://stackoverflow.com/a/40370077\n * @private\n */\n private _onPause(): void\n {\n this._playing = false;\n }\n\n /**\n * Initialize the instance.\n * @param {htmlaudio.HTMLAudioMedia} media - Same as constructor\n */\n public init(media: HTMLAudioMedia): void\n {\n this._playing = false;\n this._duration = media.source.duration;\n const source = this._source = media.source.cloneNode(false) as HTMLAudioElement;\n\n source.src = media.parent.url;\n source.onplay = this._onPlay.bind(this);\n source.onpause = this._onPause.bind(this);\n media.context.on('refresh', this.refresh, this);\n media.context.on('refreshPaused', this.refreshPaused, this);\n this._media = media;\n }\n\n /**\n * Stop the sound playing\n * @private\n */\n private _internalStop(): void\n {\n if (this._source && this._playing)\n {\n this._source.onended = null;\n this._source.pause();\n }\n }\n\n /** Stop the sound playing */\n public stop(): void\n {\n this._internalStop();\n\n if (this._source)\n {\n this.emit('stop');\n }\n }\n\n /** Set the instance speed from 0 to 1 */\n public get speed(): number\n {\n return this._speed;\n }\n public set speed(speed: number)\n {\n this._speed = speed;\n this.refresh();\n }\n\n /** Get the set the volume for this instance from 0 to 1 */\n public get volume(): number\n {\n return this._volume;\n }\n public set volume(volume: number)\n {\n this._volume = volume;\n this.refresh();\n }\n\n /** If the sound instance should loop playback */\n public get loop(): boolean\n {\n return this._loop;\n }\n public set loop(loop: boolean)\n {\n this._loop = loop;\n this.refresh();\n }\n\n /** `true` if the sound is muted */\n public get muted(): boolean\n {\n return this._muted;\n }\n public set muted(muted: boolean)\n {\n this._muted = muted;\n this.refresh();\n }\n\n /**\n * HTML Audio does not support filters, this is non-functional API.\n */\n public get filters(): Filter[]\n {\n console.warn('HTML Audio does not support filters');\n\n return null;\n }\n public set filters(_filters: Filter[])\n {\n console.warn('HTML Audio does not support filters');\n }\n\n /** Call whenever the loop, speed or volume changes */\n public refresh(): void\n {\n const global = this._media.context;\n const sound = this._media.parent;\n\n // Update the looping\n this._source.loop = this._loop || sound.loop;\n\n // Update the volume\n const globalVolume = global.volume * (global.muted ? 0 : 1);\n const soundVolume = sound.volume * (sound.muted ? 0 : 1);\n const instanceVolume = this._volume * (this._muted ? 0 : 1);\n\n this._source.volume = instanceVolume * globalVolume * soundVolume;\n\n // Update the speed\n this._source.playbackRate = this._speed * global.speed * sound.speed;\n }\n\n /** Handle changes in paused state, either globally or sound or instance */\n public refreshPaused(): void\n {\n const global = this._media.context;\n const sound = this._media.parent;\n\n // Handle the paused state\n const pausedReal = this._paused || sound.paused || global.paused;\n\n if (pausedReal !== this._pausedReal)\n {\n this._pausedReal = pausedReal;\n\n if (pausedReal)\n {\n this._internalStop();\n\n /**\n * The sound is paused.\n * @event paused\n */\n this.emit('paused');\n }\n else\n {\n /**\n * The sound is unpaused.\n * @event resumed\n */\n this.emit('resumed');\n\n // resume the playing with offset\n this.play({\n start: this._source.currentTime,\n end: this._end,\n volume: this._volume,\n speed: this._speed,\n loop: this._loop,\n });\n }\n\n /**\n * The sound is paused or unpaused.\n * @event pause\n * @property {boolean} paused - If the instance was paused or not.\n */\n this.emit('pause', pausedReal);\n }\n }\n\n /** Start playing the sound/ */\n public play(options: PlayOptions): void\n {\n const { start, end, speed, loop, volume, muted } = options;\n\n if (end)\n {\n // eslint-disable-next-line no-console\n console.assert(end > start, 'End time is before start time');\n }\n\n this._speed = speed;\n this._volume = volume;\n this._loop = !!loop;\n this._muted = muted;\n this.refresh();\n\n // WebAudio doesn't support looping when a duration is set\n // we'll set this just for the heck of it\n if (this.loop && end !== null)\n {\n console.warn('Looping not support when specifying an \"end\" time');\n this.loop = false;\n }\n\n this._start = start;\n this._end = end || this._duration;\n\n // Lets expand the start and end a little\n // to deal with the low-latecy of playing audio this way\n // this is a little fudge-factor\n this._start = Math.max(0, this._start - HTMLAudioInstance.PADDING);\n this._end = Math.min(this._end + HTMLAudioInstance.PADDING, this._duration);\n\n this._source.onloadedmetadata = () =>\n {\n if (this._source)\n {\n this._source.currentTime = start;\n this._source.onloadedmetadata = null;\n this.emit('progress', start / this._duration, this._duration);\n Ticker.shared.add(this._onUpdate, this);\n }\n };\n this._source.onended = this._onComplete.bind(this);\n this._source.play();\n\n /**\n * The sound is started.\n * @event start\n */\n this.emit('start');\n }\n\n /**\n * Handle time update on sound.\n * @private\n */\n private _onUpdate(): void\n {\n this.emit('progress', this.progress, this._duration);\n if (this._source.currentTime >= this._end && !this._source.loop)\n {\n this._onComplete();\n }\n }\n\n /**\n * Callback when completed.\n * @private\n */\n private _onComplete(): void\n {\n Ticker.shared.remove(this._onUpdate, this);\n this._internalStop();\n this.emit('progress', 1, this._duration);\n /**\n * The sound ends, don't use after this\n * @event end\n */\n this.emit('end', this);\n }\n\n /** Don't use after this. */\n public destroy(): void\n {\n Ticker.shared.remove(this._onUpdate, this);\n this.removeAllListeners();\n\n const source = this._source;\n\n if (source)\n {\n // Remove the listeners\n source.onended = null;\n source.onplay = null;\n source.onpause = null;\n\n this._internalStop();\n }\n\n this._source = null;\n this._speed = 1;\n this._volume = 1;\n this._loop = false;\n this._end = null;\n this._start = 0;\n this._duration = 0;\n this._playing = false;\n this._pausedReal = false;\n this._paused = false;\n this._muted = false;\n\n if (this._media)\n {\n this._media.context.off('refresh', this.refresh, this);\n this._media.context.off('refreshPaused', this.refreshPaused, this);\n this._media = null;\n }\n }\n\n /**\n * To string method for instance.\n * @return The string representation of instance.\n */\n public toString(): string\n {\n return `[HTMLAudioInstance id=${this.id}]`;\n }\n}\n\nexport { HTMLAudioInstance };\n","import { EventEmitter } from 'pixi.js';\nimport { Filter } from '../filters/Filter';\nimport { IMedia } from '../interfaces/IMedia';\nimport { LoadedCallback, Sound } from '../Sound';\nimport { HTMLAudioContext } from './HTMLAudioContext';\nimport { HTMLAudioInstance } from './HTMLAudioInstance';\n\n/**\n * The fallback version of Sound which uses `<audio>` instead of WebAudio API.\n * @memberof htmlaudio\n * @extends PIXI.EventEmitter\n */\nclass HTMLAudioMedia extends EventEmitter implements IMedia\n{\n public parent: Sound;\n private _source: HTMLAudioElement;\n\n public init(parent: Sound): void\n {\n this.parent = parent;\n this._source = parent.options.source as HTMLAudioElement || new Audio();\n if (parent.url)\n {\n this._source.src = parent.url;\n }\n }\n\n // Implement create\n public create(): HTMLAudioInstance\n {\n return new HTMLAudioInstance(this);\n }\n\n /**\n * If the audio media is playable (ready).\n * @readonly\n */\n public get isPlayable(): boolean\n {\n return !!this._source && this._source.readyState === 4;\n }\n\n /**\n * THe duration of the media in seconds.\n * @readonly\n */\n public get duration(): number\n {\n return this._source.duration;\n }\n\n /**\n * Reference to the context.\n * @readonly\n */\n public get context(): HTMLAudioContext\n {\n return this.parent.context as HTMLAudioContext;\n }\n\n /** The collection of filters, does not apply to HTML Audio. */\n public get filters(): Filter[]\n {\n return null;\n }\n public set filters(_filters: Filter[])\n {\n console.warn('HTML Audio does not support filters');\n }\n\n // Override the destroy\n public destroy(): void\n {\n this.removeAllListeners();\n\n this.parent = null;\n\n if (this._source)\n {\n this._source.src = '';\n this._source.load();\n this._source = null;\n }\n }\n\n /**\n * Get the audio source element.\n * @type {HTMLAudioElement}\n * @readonly\n */\n public get source(): HTMLAudioElement\n {\n return this._source;\n }\n\n // Implement the method to being preloading\n public load(callback?: LoadedCallback): void\n {\n const source = this._source;\n const sound = this.parent;\n\n // See if the source is already loaded\n if (source.readyState === 4)\n {\n sound.isLoaded = true;\n const instance = sound.autoPlayStart();\n\n if (callback)\n {\n setTimeout(() =>\n {\n callback(null, sound, instance);\n }, 0);\n }\n\n return;\n }\n\n // If there's no source, we cannot load\n if (!sound.url)\n {\n callback(new Error('sound.url or sound.source must be set'));\n\n return;\n }\n\n // Set the source\n source.src = sound.url;\n\n const onLoad = () =>\n {\n // eslint-disable-next-line @typescript-eslint/no-use-before-define\n removeListeners();\n sound.isLoaded = true;\n const instance = sound.autoPlayStart();\n\n if (callback)\n {\n callback(null, sound, instance);\n }\n };\n\n const onAbort = () =>\n {\n // eslint-disable-next-line @typescript-eslint/no-use-before-define\n removeListeners();\n if (callback)\n {\n callback(new Error('Sound loading has been aborted'));\n }\n };\n\n const onError = () =>\n {\n // eslint-disable-next-line @typescript-eslint/no-use-before-define\n removeListeners();\n const message = `Failed to load audio element (code: ${source.error.code})`;\n\n if (callback)\n {\n callback(new Error(message));\n }\n else\n {\n console.error(message);\n }\n };\n\n // Remove all event listeners\n const removeListeners = () =>\n {\n // Listen for callback\n source.removeEventListener('canplaythrough', onLoad);\n source.removeEventListener('load', onLoad);\n source.removeEventListener('abort', onAbort);\n source.removeEventListener('error', onError);\n };\n\n // Listen for callback\n source.addEventListener('canplaythrough', onLoad, false);\n source.addEventListener('load', onLoad, false);\n source.addEventListener('abort', onAbort, false);\n source.addEventListener('error', onError, false);\n\n // Begin the loading\n source.load();\n }\n}\n\nexport { HTMLAudioMedia };\n","import { IMediaInstance } from './interfaces';\nimport { CompleteCallback, Sound } from './Sound';\n\n/** Data for adding new sound sprites. */\ninterface SoundSpriteData\n{\n /** The start time in seconds. */\n start: number;\n /** The end time in seconds. */\n end: number;\n /** The optional speed, if not speed, uses the default speed of the parent. */\n speed?: number;\n}\n\n// Collection of sound sprites\ntype SoundSprites = Record<string, SoundSprite>;\n\n/**\n * Object that represents a single Sound's sprite. To add sound sprites\n * use the {@link Sound#addSprites} method.\n * @example\n * import { sound } from '@pixi/sound';\n * sound.add('alias', {\n * url: 'path/to/file.ogg',\n * sprites: {\n * blast: { start: 0, end: 0.2 },\n * boom: { start: 0.3, end: 0.5 },\n * },\n * loaded() {\n * sound.play('alias', 'blast');\n * }\n * );\n *\n */\nclass SoundSprite\n{\n /**\n * The reference sound\n * @readonly\n */\n public parent: Sound;\n\n /**\n * The starting location in seconds.\n * @readonly\n */\n public start: number;\n\n /**\n * The ending location in seconds\n * @readonly\n */\n public end: number;\n\n /**\n * The speed override where 1 is 100% speed playback.\n * @readonly\n */\n public speed: number;\n\n /**\n * The duration of the sound in seconds.\n * @readonly\n */\n public duration: number;\n\n /**\n * Whether to loop the sound sprite.\n * @readonly\n */\n public loop: boolean;\n\n /**\n * @param parent - The parent sound\n * @param options - Data associated with object.\n */\n constructor(parent: Sound, options: SoundSpriteData)\n {\n this.parent = parent;\n Object.assign(this, options);\n this.duration = this.end - this.start;\n\n // eslint-disable-next-line no-console\n console.assert(this.duration > 0, 'End time must be after start time');\n }\n\n /**\n * Play the sound sprite.\n * @param {Function} [complete] - Function call when complete\n * @return Sound instance being played.\n */\n public play(complete?: CompleteCallback): IMediaInstance | Promise<IMediaInstance>\n {\n return this.parent.play({\n complete,\n speed: this.speed || this.parent.speed,\n end: this.end,\n start: this.start,\n loop: this.loop\n });\n }\n\n /** Destroy and don't use after this */\n public destroy(): void\n {\n this.parent = null;\n }\n}\n\nexport type { SoundSpriteData, SoundSprites };\nexport { SoundSprite };\n","type ExtensionMap = Record<string, boolean>;\n\n/**\n * The list of extensions that can be played. This is the preferred order of playback.\n * If you want to priority the order of playback, you can use this array to do so.\n * @readonly\n * @memberof utils\n */\nconst extensions: string[] = [\n 'ogg',\n 'oga',\n 'opus',\n 'm4a',\n 'mp3',\n 'mpeg',\n 'wav',\n 'aiff',\n 'wma',\n 'mid',\n 'caf',\n];\n\nconst mimes: string[] = [\n 'audio/mpeg',\n 'audio/ogg',\n];\n\n/**\n * The list of browser supported audio formats.\n * @readonly\n * @memberof utils\n * @property {boolean} mp3 - `true` if file-type is supported\n * @property {boolean} ogg - `true` if file-type is supported\n * @property {boolean} oga - `true` if file-type is supported\n * @property {boolean} opus - `true` if file-type is supported\n * @property {boolean} mpeg - `true` if file-type is supported\n * @property {boolean} wav - `true` if file-type is supported\n * @property {boolean} aiff - `true` if file-type is supported\n * @property {boolean} wma - `true` if file-type is supported\n * @property {boolean} mid - `true` if file-type is supported\n * @property {boolean} caf - `true` if file-type is supported. Note that for this we check if the\n * 'opus' codec is supported inside the caf container.\n */\nconst supported: ExtensionMap = {};\n\n/**\n * Function to validate file type formats. This is called when the library initializes, but can\n * be called again if you need to recognize a format not listed in `utils.extensions` at\n * initialization.\n * @memberof utils\n * @param typeOverrides - - Dictionary of type overrides (inputs for\n * AudioElement.canPlayType()), keyed by extension from the\n * utils.extensions array.\n */\nfunction validateFormats(typeOverrides?: Record<string, string>): void\n{\n const overrides: Record<string, string> = {\n m4a: 'audio/mp4',\n oga: 'audio/ogg',\n opus: 'audio/ogg; codecs=\"opus\"',\n caf: 'audio/x-caf; codecs=\"opus\"', ...(typeOverrides || {})\n };\n const audio = document.createElement('audio');\n const formats: ExtensionMap = {};\n const no = /^no$/;\n\n extensions.forEach((ext) =>\n {\n const canByExt = audio.canPlayType(`audio/${ext}`).replace(no, '');\n const canByType = overrides[ext] ? audio.canPlayType(overrides[ext]).replace(no, '') : '';\n\n formats[ext] = !!canByExt || !!canByType;\n });\n Object.assign(supported, formats);\n}\n\n// initialize supported\nvalidateFormats();\n\nexport {\n extensions,\n mimes,\n supported,\n validateFormats,\n};\n","import { EventEmitter, Ticker } from 'pixi.js';\nimport { Filter } from '../filters/Filter';\nimport { IMediaInstance } from '../interfaces';\nimport { PlayOptions } from '../Sound';\nimport { WebAudioMedia } from './WebAudioMedia';\nimport { WebAudioUtils } from './WebAudioUtils';\n\nlet id = 0;\n\n/**\n * A single play instance that handles the AudioBufferSourceNode.\n * @memberof webaudio\n * @extends PIXI.EventEmitter\n */\nclass WebAudioInstance extends EventEmitter implements IMediaInstance\n{\n /**\n * The current unique ID for this instance.\n * @readonly\n */\n public readonly id: number;\n\n /** The source Sound. */\n private _media: WebAudioMedia;\n\n /** true if paused. */\n private _paused: boolean;\n\n /** true if muted. */\n private _muted: boolean;\n\n /** true if paused. */\n private _pausedReal: boolean;\n\n /** The instance volume */\n private _volume: number;\n\n /** Last update frame number. */\n private _lastUpdate: number;\n\n /** The total number of seconds elapsed in playback. */\n private _elapsed: number;\n\n /** Playback rate, where 1 is 100%. */\n private _speed: number;\n\n /** Playback rate, where 1 is 100%. */\n private _end: number;\n\n /** `true` if should be looping. */\n private _loop: boolean;\n\n /** Gain node for controlling volume of instance */\n private _gain: GainNode;\n\n /** Length of the sound in seconds. */\n private _duration: number;\n\n /** The progress of the sound from 0 to 1. */\n private _progress: number;\n\n /** Audio buffer source clone from Sound object. */\n private _source: AudioBufferSourceNode;\n\n /** The filters */\n private _filters: Filter[];\n\n constructor(media: WebAudioMedia)\n {\n super();\n\n this.id = id++;\n this._media = null;\n this._paused = false;\n this._muted = false;\n this._elapsed = 0;\n\n // Initialize\n this.init(media);\n }\n\n /**\n * Set a property by name, this makes it easy to chain values\n * @param name - Name of the property to set.\n * @param value - Value to set property to.\n */\n public set(name: 'speed' | 'volume' | 'muted' | 'loop' | 'paused', value: number | boolean): this\n {\n if (this[name] === undefined)\n {\n throw new Error(`Property with name ${name} does not exist.`);\n }\n else\n {\n switch (name)\n {\n case 'speed': this.speed = value as number; break;\n case 'volume': this.volume = value as number; break;\n case 'm