diff options
author | Adam Scott <ascott.ca@gmail.com> | 2024-04-18 10:50:34 -0400 |
---|---|---|
committer | Adam Scott <ascott.ca@gmail.com> | 2024-06-18 11:06:31 -0400 |
commit | 52fa4f05f3945fdf511c249adede9b6d07c51beb (patch) | |
tree | 2ac2aca86c09dd757fd4a8b5defab932f2b8eb5d /platform/web/js | |
parent | eb20a68b323c1fcb75492f8132e1bd6d321713ec (diff) | |
download | redot-engine-52fa4f05f3945fdf511c249adede9b6d07c51beb.tar.gz |
Add samples playback support
Diffstat (limited to 'platform/web/js')
-rw-r--r-- | platform/web/js/engine/features.js | 3 | ||||
-rw-r--r-- | platform/web/js/libs/library_godot_audio.js | 1614 |
2 files changed, 1586 insertions, 31 deletions
diff --git a/platform/web/js/engine/features.js b/platform/web/js/engine/features.js index 601fa4a117..c366683dfa 100644 --- a/platform/web/js/engine/features.js +++ b/platform/web/js/engine/features.js @@ -76,7 +76,8 @@ const Features = { */ getMissingFeatures: function (supportedFeatures = {}) { const { - threads: supportsThreads = true, + // Quotes are needed for the Closure compiler. + 'threads': supportsThreads = true, } = supportedFeatures; const missing = []; diff --git a/platform/web/js/libs/library_godot_audio.js b/platform/web/js/libs/library_godot_audio.js index b54c5cac85..d7baece781 100644 --- a/platform/web/js/libs/library_godot_audio.js +++ b/platform/web/js/libs/library_godot_audio.js @@ -28,18 +28,1048 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -const GodotAudio = { +/** + * @typedef { "disabled" | "forward" | "backward" | "pingpong" } LoopMode + */ + +/** + * @typedef {{ + * id: string + * audioBuffer: AudioBuffer + * }} SampleParams + * @typedef {{ + * numberOfChannels?: number + * sampleRate?: number + * loopMode?: LoopMode + * loopBegin?: number + * loopEnd?: number + * }} SampleOptions + */ + +/** + * Represents a sample, memory-wise. + * @class + */ +class Sample { + /** + * Returns a `Sample`. + * @param {string} id Id of the `Sample` to get. + * @returns {Sample} + * @throws {ReferenceError} When no `Sample` is found + */ + static getSample(id) { + if (!GodotAudio.samples.has(id)) { + throw new ReferenceError(`Could not find sample "${id}"`); + } + return GodotAudio.samples.get(id); + } + + /** + * Returns a `Sample` or `null`, if it doesn't exist. + * @param {string} id Id of the `Sample` to get. + * @returns {Sample?} + */ + static getSampleOrNull(id) { + return GodotAudio.samples.get(id) ?? null; + } + + /** + * Creates a `Sample` based on the params. Will register it to the + * `GodotAudio.samples` registry. + * @param {SampleParams} params Base params + * @param {SampleOptions} [options={}] Optional params + * @returns {Sample} + */ + static create(params, options = {}) { + const sample = new GodotAudio.Sample(params, options); + GodotAudio.samples.set(params.id, sample); + return sample; + } + + /** + * Deletes a `Sample` based on the id. + * @param {string} id `Sample` id to delete + * @returns {void} + */ + static delete(id) { + GodotAudio.samples.delete(id); + } + + /** + * `Sample` constructor. + * @param {SampleParams} params Base params + * @param {SampleOptions} [options={}] Optional params + * @constructor + */ + constructor(params, options = {}) { + /** @type {string} */ + this.id = params.id; + /** @type {AudioBuffer} */ + this._audioBuffer = null; + /** @type {number} */ + this.numberOfChannels = options.numberOfChannels ?? 2; + /** @type {number} */ + this.sampleRate = options.sampleRate ?? 44100; + /** @type {LoopMode} */ + this.loopMode = options.loopMode ?? 'disabled'; + /** @type {number} */ + this.loopBegin = options.loopBegin ?? 0; + /** @type {number} */ + this.loopEnd = options.loopEnd ?? 0; + + this.setAudioBuffer(params.audioBuffer); + } + + /** + * Gets the audio buffer of the sample. + * @returns {AudioBuffer} + */ + getAudioBuffer() { + return this._duplicateAudioBuffer(); + } + + /** + * Sets the audio buffer of the sample. + * @param {AudioBuffer} val The audio buffer to set. + * @returns {void} + */ + setAudioBuffer(val) { + this._audioBuffer = val; + } + + /** + * Clears the current sample. + * @returns {void} + */ + clear() { + this.audioBuffer = null; + GodotAudio.Sample.delete(this.id); + } + + /** + * Returns a duplicate of the stored audio buffer. + * @returns {AudioBuffer} + */ + _duplicateAudioBuffer() { + if (this._audioBuffer == null) { + throw new Error('couldn\'t duplicate a null audioBuffer'); + } + /** @type {Float32Array[]} */ + const channels = new Array(this._audioBuffer.numberOfChannels); + for (let i = 0; i < this._audioBuffer.numberOfChannels; i++) { + const channel = new Float32Array(this._audioBuffer.getChannelData(i)); + channels[i] = channel; + } + const buffer = GodotAudio.ctx.createBuffer( + this.numberOfChannels, + this._audioBuffer.length, + this._audioBuffer.sampleRate + ); + for (let i = 0; i < channels.length; i++) { + buffer.copyToChannel(channels[i], i, 0); + } + return buffer; + } +} + +/** + * Represents a `SampleNode` linked to a `Bus`. + * @class + */ +class SampleNodeBus { + /** + * Creates a new `SampleNodeBus`. + * @param {Bus} bus The bus related to the new `SampleNodeBus`. + * @returns {SampleNodeBus} + */ + static create(bus) { + return new GodotAudio.SampleNodeBus(bus); + } + + /** + * `SampleNodeBus` constructor. + * @param {Bus} bus The bus related to the new `SampleNodeBus`. + * @constructor + */ + constructor(bus) { + const NUMBER_OF_WEB_CHANNELS = 6; + + /** @type {Bus} */ + this._bus = bus; + + /** @type {ChannelSplitterNode} */ + this._channelSplitter = GodotAudio.ctx.createChannelSplitter(NUMBER_OF_WEB_CHANNELS); + /** @type {GainNode} */ + this._l = GodotAudio.ctx.createGain(); + /** @type {GainNode} */ + this._r = GodotAudio.ctx.createGain(); + /** @type {GainNode} */ + this._sl = GodotAudio.ctx.createGain(); + /** @type {GainNode} */ + this._sr = GodotAudio.ctx.createGain(); + /** @type {GainNode} */ + this._c = GodotAudio.ctx.createGain(); + /** @type {GainNode} */ + this._lfe = GodotAudio.ctx.createGain(); + /** @type {ChannelMergerNode} */ + this._channelMerger = GodotAudio.ctx.createChannelMerger(NUMBER_OF_WEB_CHANNELS); + + this._channelSplitter + .connect(this._l, GodotAudio.WebChannel.CHANNEL_L) + .connect( + this._channelMerger, + GodotAudio.WebChannel.CHANNEL_L, + GodotAudio.WebChannel.CHANNEL_L + ); + this._channelSplitter + .connect(this._r, GodotAudio.WebChannel.CHANNEL_R) + .connect( + this._channelMerger, + GodotAudio.WebChannel.CHANNEL_L, + GodotAudio.WebChannel.CHANNEL_R + ); + this._channelSplitter + .connect(this._sl, GodotAudio.WebChannel.CHANNEL_SL) + .connect( + this._channelMerger, + GodotAudio.WebChannel.CHANNEL_L, + GodotAudio.WebChannel.CHANNEL_SL + ); + this._channelSplitter + .connect(this._sr, GodotAudio.WebChannel.CHANNEL_SR) + .connect( + this._channelMerger, + GodotAudio.WebChannel.CHANNEL_L, + GodotAudio.WebChannel.CHANNEL_SR + ); + this._channelSplitter + .connect(this._c, GodotAudio.WebChannel.CHANNEL_C) + .connect( + this._channelMerger, + GodotAudio.WebChannel.CHANNEL_L, + GodotAudio.WebChannel.CHANNEL_C + ); + this._channelSplitter + .connect(this._lfe, GodotAudio.WebChannel.CHANNEL_L) + .connect( + this._channelMerger, + GodotAudio.WebChannel.CHANNEL_L, + GodotAudio.WebChannel.CHANNEL_LFE + ); + + this._channelMerger.connect(this._bus.getInputNode()); + } + + /** + * Returns the input node. + * @returns {AudioNode} + */ + getInputNode() { + return this._channelSplitter; + } + + /** + * Returns the output node. + * @returns {AudioNode} + */ + getOutputNode() { + return this._channelMerger; + } + + /** + * Sets the volume for each (split) channel. + * @param {Float32Array} volume Volume array from the engine for each channel. + * @returns {void} + */ + setVolume(volume) { + if (volume.length !== GodotAudio.MAX_VOLUME_CHANNELS) { + throw new Error( + `Volume length isn't "${GodotAudio.MAX_VOLUME_CHANNELS}", is ${volume.length} instead` + ); + } + this._l.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_L] ?? 0; + this._r.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_R] ?? 0; + this._sl.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_SL] ?? 0; + this._sr.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_SR] ?? 0; + this._c.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_C] ?? 0; + this._lfe.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_LFE] ?? 0; + } + + /** + * Clears the current `SampleNodeBus` instance. + * @returns {void} + */ + clear() { + this._bus = null; + this._channelSplitter.disconnect(); + this._channelSplitter = null; + this._l.disconnect(); + this._l = null; + this._r.disconnect(); + this._r = null; + this._sl.disconnect(); + this._sl = null; + this._sr.disconnect(); + this._sr = null; + this._c.disconnect(); + this._c = null; + this._lfe.disconnect(); + this._lfe = null; + this._channelMerger.disconnect(); + this._channelMerger = null; + } +} + +/** + * @typedef {{ + * id: string + * streamObjectId: string + * busIndex: number + * }} SampleNodeParams + * @typedef {{ + * offset?: number + * playbackRate?: number + * startTime?: number + * loopMode?: LoopMode + * volume?: Float32Array + * }} SampleNodeOptions + */ + +/** + * Represents an `AudioNode` of a `Sample`. + * @class + */ +class SampleNode { + /** + * Returns a `SampleNode`. + * @param {string} id Id of the `SampleNode`. + * @returns {SampleNode} + * @throws {ReferenceError} When no `SampleNode` is not found + */ + static getSampleNode(id) { + if (!GodotAudio.sampleNodes.has(id)) { + throw new ReferenceError(`Could not find sample node "${id}"`); + } + return GodotAudio.sampleNodes.get(id); + } + + /** + * Returns a `SampleNode`, returns null if not found. + * @param {string} id Id of the SampleNode. + * @returns {SampleNode?} + */ + static getSampleNodeOrNull(id) { + return GodotAudio.sampleNodes.get(id) ?? null; + } + + /** + * Stops a `SampleNode` by id. + * @param {string} id Id of the `SampleNode` to stop. + * @returns {void} + */ + static stopSampleNode(id) { + const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(id); + if (sampleNode == null) { + return; + } + sampleNode.stop(); + } + + /** + * Pauses the `SampleNode` by id. + * @param {string} id Id of the `SampleNode` to pause. + * @param {boolean} enable State of the pause + * @returns {void} + */ + static pauseSampleNode(id, enable) { + const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(id); + if (sampleNode == null) { + return; + } + sampleNode.pause(enable); + } + + /** + * Creates a `SampleNode` based on the params. Will register the `SampleNode` to + * the `GodotAudio.sampleNodes` regisery. + * @param {SampleNodeParams} params Base params. + * @param {SampleNodeOptions} options Optional params. + * @returns {SampleNode} + */ + static create(params, options = {}) { + const sampleNode = new GodotAudio.SampleNode(params, options); + GodotAudio.sampleNodes.set(params.id, sampleNode); + return sampleNode; + } + + /** + * Deletes a `SampleNode` based on the id. + * @param {string} id Id of the `SampleNode` to delete. + * @returns {void} + */ + static delete(id) { + GodotAudio.sampleNodes.delete(id); + } + + /** + * @param {SampleNodeParams} params Base params + * @param {SampleNodeOptions} [options={}] Optional params + * @constructor + */ + constructor(params, options = {}) { + /** @type {string} */ + this.id = params.id; + /** @type {string} */ + this.streamObjectId = params.streamObjectId; + /** @type {number} */ + this.offset = options.offset ?? 0; + /** @type {LoopMode} */ + this.startTime = options.startTime ?? 0; + /** @type {number} */ + this.pauseTime = 0; + /** @type {number} */ + this._playbackRate = 44100; + /** @type {LoopMode} */ + this._loopMode = 'disabled'; + /** @type {number} */ + this._pitchScale = 1; + /** @type {Map<Bus, SampleNodeBus>} */ + this._sampleNodeBuses = new Map(); + /** @type {AudioBufferSourceNode} */ + this._source = GodotAudio.ctx.createBufferSource(); + + this.setPlaybackRate(options.playbackRate ?? 44100); + this.setLoopMode(options.loopMode ?? this.getSample().loopMode ?? 'disabled'); + this._source.buffer = this.getSample().getAudioBuffer(); + + /** @type {SampleNode} */ + // eslint-disable-next-line consistent-this + const self = this; + this._source.addEventListener('ended', (_) => { + switch (self.getSample().loopMode) { + case 'disabled': + GodotAudio.SampleNode.stopSampleNode(self.id); + break; + default: + // do nothing + } + }); + + const bus = GodotAudio.Bus.getBus(params.busIndex); + const sampleNodeBus = this.getSampleNodeBus(bus); + sampleNodeBus.setVolume(options.volume); + } + + /** + * Gets the loop mode of the current instance. + * @returns {LoopMode} + */ + getLoopMode() { + return this._loopMode; + } + + /** + * Sets the loop mode of the current instance. + * @param {LoopMode} val Value to set. + * @returns {void} + */ + setLoopMode(val) { + this._loopMode = val; + switch (val) { + case 'forward': + case 'backward': + this._source.loop = true; + break; + default: + this._source.loop = false; + } + } + + /** + * Gets the playback rate. + * @returns {number} + */ + getPlaybackRate() { + return this._playbackRate; + } + + /** + * Sets the playback rate. + * @param {number} val Value to set. + * @returns {void} + */ + setPlaybackRate(val) { + this._playbackRate = val; + this._syncPlaybackRate(); + } + + /** + * Gets the pitch scale. + * @returns {number} + */ + getPitchScale() { + return this._pitchScale; + } + + /** + * Sets the pitch scale. + * @param {number} val Value to set. + * @returns {void} + */ + setPitchScale(val) { + this._pitchScale = val; + this._syncPlaybackRate(); + } + + /** + * Returns the linked `Sample`. + * @returns {Sample} + */ + getSample() { + return GodotAudio.Sample.getSample(this.streamObjectId); + } + + /** + * Returns the output node. + * @returns {AudioNode} + */ + getOutputNode() { + return this._source; + } + + /** + * Starts the `SampleNode`. + * @returns {void} + */ + start() { + this._source.start(this.offset); + } + + /** + * Stops the `SampleNode`. + * @returns {void} + */ + stop() { + this._source.stop(); + this.clear(); + } + + /** + * Pauses the `SampleNode`. + * @param {boolean} [enable=true] State of the pause. + * @returns {void} + */ + pause(enable = true) { + if (enable) { + this.pauseTime = (GodotAudio.ctx.currentTime - this.startTime) / this.playbackRate; + this._source.stop(); + return; + } + + if (this.pauseTime === 0) { + return; + } + + this._source.disconnect(); + this._source = GodotAudio.ctx.createBufferSource(); + + this._source.buffer = this.getSample().getAudioBuffer(); + this._source.connect(this._gain); + this._source.start(this.offset + this.pauseTime); + } + + /** + * Connects an AudioNode to the output node of this `SampleNode`. + * @param {AudioNode} node AudioNode to connect. + * @returns {void} + */ + connect(node) { + return this.getOutputNode().connect(node); + } + + /** + * Sets the volumes of the `SampleNode` for each buses passed in parameters. + * @param {Bus[]} buses + * @param {Float32Array} volumes + */ + setVolumes(buses, volumes) { + for (let busIdx = 0; busIdx < buses.length; busIdx++) { + const sampleNodeBus = this.getSampleNodeBus(buses[busIdx]); + sampleNodeBus.setVolume( + volumes.slice( + busIdx * GodotAudio.MAX_VOLUME_CHANNELS, + (busIdx * GodotAudio.MAX_VOLUME_CHANNELS) + GodotAudio.MAX_VOLUME_CHANNELS + ) + ); + } + } + + /** + * Returns the SampleNodeBus based on the bus in parameters. + * @param {Bus} bus Bus to get the SampleNodeBus from. + * @returns {SampleNodeBus} + */ + getSampleNodeBus(bus) { + if (!this._sampleNodeBuses.has(bus)) { + const sampleNodeBus = GodotAudio.SampleNodeBus.create(bus); + this._sampleNodeBuses.set(bus, sampleNodeBus); + this._source.connect(sampleNodeBus.getInputNode()); + } + return this._sampleNodeBuses.get(bus); + } + + /** + * Clears the `SampleNode`. + * @returns {void} + */ + clear() { + this._source.stop(); + this._source.disconnect(); + this._source = null; + + for (const sampleNodeBus of this._sampleNodeBuses.values()) { + sampleNodeBus.clear(); + } + this._sampleNodeBuses.clear(); + this._sampleNodeBuses = null; + + GodotAudio.SampleNode.delete(this.id); + } + + /** + * Syncs the `AudioNode` playback rate based on the `SampleNode` playback rate and pitch scale. + * @returns {void} + */ + _syncPlaybackRate() { + this._source.playbackRate.value = this.getPlaybackRate() * this.getPitchScale(); + } +} + +/** + * Collection of nodes to represents a Godot Engine audio bus. + * @class + */ +class Bus { + /** + * Returns the number of registered buses. + * @returns {number} + */ + static getCount() { + return GodotAudio.buses.length; + } + + /** + * Sets the number of registered buses. + * Will delete buses if lower than the current number. + * @param {number} val Count of registered buses. + * @returns {void} + */ + static setCount(val) { + const buses = GodotAudio.buses; + if (val === buses.length) { + return; + } + + if (val < buses.length) { + // TODO: what to do with nodes connected to the deleted buses? + const deletedBuses = buses.slice(val); + for (let i = 0; i < deletedBuses.length; i++) { + const deletedBus = deletedBuses[i]; + deletedBus.clear(); + } + GodotAudio.buses = buses.slice(0, val); + return; + } + + for (let i = GodotAudio.buses.length; i < val; i++) { + GodotAudio.Bus.create(); + } + } + + /** + * Returns a `Bus` based on it's index number. + * @param {number} index + * @returns {Bus} + * @throws {ReferenceError} If the index value is outside the registry. + */ + static getBus(index) { + if (index < 0 || index >= GodotAudio.buses.length) { + throw new ReferenceError(`invalid bus index "${index}"`); + } + return GodotAudio.buses[index]; + } + + /** + * Returns a `Bus` based on it's index number. Returns null if it doesn't exist. + * @param {number} index + * @returns {Bus?} + */ + static getBusOrNull(index) { + if (index < 0 || index >= GodotAudio.buses.length) { + return null; + } + return GodotAudio.buses[index]; + } + + /** + * Move a bus from an index to another. + * @param {number} fromIndex From index + * @param {number} toIndex To index + * @returns {void} + */ + static move(fromIndex, toIndex) { + const movedBus = GodotAudio.Bus.getBus(fromIndex); + let buses = GodotAudio.buses; + buses = buses.filter((_, i) => i !== fromIndex); + // Inserts at index. + buses.splice(toIndex - 1, 0, movedBus); + GodotAudio.buses = buses; + } + + /** + * Adds a new bus at the specified index. + * @param {number} index Index to add a new bus. + * @returns {void} + */ + static addAt(index) { + const newBus = GodotAudio.Bus.create(); + if (index !== newBus.getId()) { + GodotAudio.Bus.move(newBus.getId(), index); + } + } + + /** + * Creates a `Bus` and registers it. + * @returns {Bus} + */ + static create() { + const newBus = new GodotAudio.Bus(); + const isFirstBus = GodotAudio.buses.length === 0; + GodotAudio.buses.push(newBus); + if (isFirstBus) { + newBus.setSend(null); + } else { + newBus.setSend(GodotAudio.Bus.getBus(0)); + } + return newBus; + } + + /** + * `Bus` constructor. + * @constructor + */ + constructor() { + /** @type {Set<SampleNode>} */ + this._sampleNodes = new Set(); + /** @type {boolean} */ + this.isSolo = false; + /** @type {Bus?} */ + this._send = null; + + /** @type {GainNode} */ + this._gainNode = GodotAudio.ctx.createGain(); + /** @type {GainNode} */ + this._soloNode = GodotAudio.ctx.createGain(); + /** @type {GainNode} */ + this._muteNode = GodotAudio.ctx.createGain(); + + this._gainNode + .connect(this._soloNode) + .connect(this._muteNode); + } + + /** + * Returns the current id of the bus (its index). + * @returns {number} + */ + getId() { + return GodotAudio.buses.indexOf(this); + } + + /** + * Returns the bus volume db value. + * @returns {number} + */ + getVolumeDb() { + return GodotAudio.linear_to_db(this._gainNode.gain.value); + } + + /** + * Sets the bus volume db value. + * @param {number} val Value to set + * @returns {void} + */ + setVolumeDb(val) { + this._gainNode.gain.value = GodotAudio.db_to_linear(val); + } + + /** + * Returns the "send" bus. + * If null, this bus sends its contents directly to the output. + * If not null, this bus sends its contents to another bus. + * @returns {Bus?} + */ + getSend() { + return this._send; + } + + /** + * Sets the "send" bus. + * If null, this bus sends its contents directly to the output. + * If not null, this bus sends its contents to another bus. + * + * **Note:** if null, `getId()` must be equal to 0. Otherwise, it will throw. + * @param {Bus?} val + * @returns {void} + * @throws {Error} When val is `null` and `getId()` isn't equal to 0 + */ + setSend(val) { + this._send = val; + if (val == null) { + if (this.getId() == 0) { + this.getOutputNode().connect(GodotAudio.ctx.destination); + return; + } + throw new Error( + `Cannot send to "${val}" without the bus being at index 0 (current index: ${this.getId()})` + ); + } + this.connect(val); + } + + /** + * Returns the input node of the bus. + * @returns {AudioNode} + */ + getInputNode() { + return this._gainNode; + } + + /** + * Returns the output node of the bus. + * @returns {AudioNode} + */ + getOutputNode() { + return this._muteNode; + } + + /** + * Sets the mute status of the bus. + * @param {boolean} enable + */ + mute(enable) { + this._muteNode.gain.value = enable ? 0 : 1; + } + + /** + * Sets the solo status of the bus. + * @param {boolean} enable + */ + solo(enable) { + if (this.isSolo === enable) { + return; + } + + if (enable) { + if (GodotAudio.busSolo != null && GodotAudio.busSolo !== this) { + GodotAudio.busSolo._disableSolo(); + } + this._enableSolo(); + return; + } + + this._disableSolo(); + } + + /** + * Wrapper to simply add a sample node to the bus. + * @param {SampleNode} sampleNode `SampleNode` to remove + * @returns {void} + */ + addSampleNode(sampleNode) { + this._sampleNodes.add(sampleNode); + sampleNode.getOutputNode().connect(this.getInputNode()); + } + + /** + * Wrapper to simply remove a sample node from the bus. + * @param {SampleNode} sampleNode `SampleNode` to remove + * @returns {void} + */ + removeSampleNode(sampleNode) { + this._sampleNodes.delete(sampleNode); + sampleNode.getOutputNode().disconnect(); + } + + /** + * Wrapper to simply connect to another bus. + * @param {Bus} bus + * @returns {void} + */ + connect(bus) { + if (bus == null) { + throw new Error('cannot connect to null bus'); + } + this.getOutputNode().disconnect(); + this.getOutputNode().connect(bus.getInputNode()); + return bus; + } + + /** + * Clears the current bus. + * @returns {void} + */ + clear() { + GodotAudio.buses = GodotAudio.buses.filter((v) => v !== this); + } + + /** @type {Bus["prototype"]["_syncSampleNodes"]} */ + _syncSampleNodes() { + const sampleNodes = Array.from(this._sampleNodes); + for (let i = 0; i < sampleNodes.length; i++) { + const sampleNode = sampleNodes[i]; + sampleNode.getOutputNode().disconnect(); + sampleNode.getOutputNode().connect(this.getInputNode()); + } + } + + /** + * Process to enable solo. + * @returns {void} + */ + _enableSolo() { + this.isSolo = true; + GodotAudio.busSolo = this; + this._soloNode.gain.value = 1; + const otherBuses = GodotAudio.buses.filter( + (otherBus) => otherBus !== this + ); + for (let i = 0; i < otherBuses.length; i++) { + const otherBus = otherBuses[i]; + otherBus._soloNode.gain.value = 0; + } + } + + /** + * Process to disable solo. + * @returns {void} + */ + _disableSolo() { + this.isSolo = false; + GodotAudio.busSolo = null; + this._soloNode.gain.value = 1; + const otherBuses = GodotAudio.buses.filter( + (otherBus) => otherBus !== this + ); + for (let i = 0; i < otherBuses.length; i++) { + const otherBus = otherBuses[i]; + otherBus._soloNode.gain.value = 1; + } + } +} + +const _GodotAudio = { $GodotAudio__deps: ['$GodotRuntime', '$GodotOS'], $GodotAudio: { + /** + * Max number of volume channels. + */ + MAX_VOLUME_CHANNELS: 8, + + /** + * Represents the index of each sound channel relative to the engine. + */ + GodotChannel: Object.freeze({ + CHANNEL_L: 0, + CHANNEL_R: 1, + CHANNEL_C: 3, + CHANNEL_LFE: 4, + CHANNEL_RL: 5, + CHANNEL_RR: 6, + CHANNEL_SL: 7, + CHANNEL_SR: 8, + }), + + /** + * Represents the index of each sound channel relative to the Web Audio API. + */ + WebChannel: Object.freeze({ + CHANNEL_L: 0, + CHANNEL_R: 1, + CHANNEL_SL: 2, + CHANNEL_SR: 3, + CHANNEL_C: 4, + CHANNEL_LFE: 5, + }), + + // `Sample` class + /** + * Registry of `Sample`s. + * @type {Map<string, Sample>} + */ + samples: null, + Sample, + + // `SampleNodeBus` class + SampleNodeBus, + + // `SampleNode` class + /** + * Registry of `SampleNode`s. + * @type {Map<string, SampleNode>} + */ + sampleNodes: null, + SampleNode, + + // `Bus` class + /** + * Registry of `Bus`es. + * @type {Bus[]} + */ + buses: null, + /** + * Reference to the current bus in solo mode. + * @type {Bus | null} + */ + busSolo: null, + Bus, + + /** @type {AudioContext} */ ctx: null, input: null, driver: null, interval: 0, + /** + * Converts linear volume to Db. + * @param {number} linear Linear value to convert. + * @returns {number} + */ + linear_to_db: function (linear) { + // eslint-disable-next-line no-loss-of-precision + return Math.log(linear) * 8.6858896380650365530225783783321; + }, + /** + * Converts Db volume to linear. + * @param {number} db Db value to convert. + * @returns {number} + */ + db_to_linear: function (db) { + // eslint-disable-next-line no-loss-of-precision + return Math.exp(db * 0.11512925464970228420089957273422); + }, + init: function (mix_rate, latency, onstatechange, onlatencyupdate) { + // Initialize classes static values. + GodotAudio.samples = new Map(); + GodotAudio.sampleNodes = new Map(); + GodotAudio.buses = []; + GodotAudio.busSolo = null; + const opts = {}; // If mix_rate is 0, let the browser choose. if (mix_rate) { + GodotAudio.sampleRate = mix_rate; opts['sampleRate'] = mix_rate; } // Do not specify, leave 'interactive' for good performance. @@ -58,8 +1088,8 @@ const GodotAudio = { case 'closed': state = 2; break; - - // no default + default: + // Do nothing. } onstatechange(state); }; @@ -148,6 +1178,163 @@ const GodotAudio = { resolve(); }); }, + + /** + * Triggered when a sample node needs to start. + * @param {string} playbackObjectId The unique id of the sample playback + * @param {string} streamObjectId The unique id of the stream + * @param {number} busIndex Index of the bus currently binded to the sample playback + * @param {SampleNodeOptions} startOptions Optional params + * @returns {void} + */ + start_sample: function ( + playbackObjectId, + streamObjectId, + busIndex, + startOptions + ) { + GodotAudio.SampleNode.stopSampleNode(playbackObjectId); + const sampleNode = GodotAudio.SampleNode.create( + { + busIndex, + id: playbackObjectId, + streamObjectId, + }, + startOptions + ); + sampleNode.start(); + }, + + /** + * Triggered when a sample node needs to be stopped. + * @param {string} playbackObjectId Id of the sample playback + * @returns {void} + */ + stop_sample: function (playbackObjectId) { + GodotAudio.SampleNode.stopSampleNode(playbackObjectId); + }, + + /** + * Triggered when a sample node needs to be paused or unpaused. + * @param {string} playbackObjectId Id of the sample playback + * @param {boolean} pause State of the pause + * @returns {void} + */ + sample_set_pause: function (playbackObjectId, pause) { + GodotAudio.SampleNode.pauseSampleNode(playbackObjectId, pause); + }, + + /** + * Triggered when a sample node needs its pitch scale to be updated. + * @param {string} playbackObjectId Id of the sample playback + * @param {number} pitchScale Pitch scale of the sample playback + * @returns {void} + */ + update_sample_pitch_scale: function (playbackObjectId, pitchScale) { + const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(playbackObjectId); + if (sampleNode == null) { + return; + } + sampleNode.setPitchScale(pitchScale); + }, + + /** + * Triggered when a sample node volumes need to be updated. + * @param {string} playbackObjectId Id of the sample playback + * @param {number[]} busIndexes Indexes of the buses that need to be updated + * @param {Float32Array} volumes Array of the volumes + * @returns {void} + */ + sample_set_volumes_linear: function (playbackObjectId, busIndexes, volumes) { + const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(playbackObjectId); + if (sampleNode == null) { + return; + } + const buses = busIndexes.map((busIndex) => GodotAudio.Bus.getBus(busIndex)); + sampleNode.setVolumes(buses, volumes); + }, + + /** + * Triggered when the bus count changes. + * @param {number} count Number of buses + * @returns {void} + */ + set_sample_bus_count: function (count) { + GodotAudio.Bus.setCount(count); + }, + + /** + * Triggered when a bus needs to be removed. + * @param {number} index Bus index + * @returns {void} + */ + remove_sample_bus: function (index) { + const bus = GodotAudio.Bus.getBus(index); + bus.clear(); + }, + + /** + * Triggered when a bus needs to be at the desired position. + * @param {number} atPos Position to add the bus + * @returns {void} + */ + add_sample_bus: function (atPos) { + GodotAudio.Bus.addAt(atPos); + }, + + /** + * Triggered when a bus needs to be moved. + * @param {number} busIndex Index of the bus to move + * @param {number} toPos Index of the new position of the bus + * @returns {void} + */ + move_sample_bus: function (busIndex, toPos) { + GodotAudio.Bus.move(busIndex, toPos); + }, + + /** + * Triggered when the "send" value of a bus changes. + * @param {number} busIndex Index of the bus to update the "send" value + * @param {number} sendIndex Index of the bus that is the new "send" + * @returns {void} + */ + set_sample_bus_send: function (busIndex, sendIndex) { + const bus = GodotAudio.Bus.getBus(busIndex); + bus.setSend(GodotAudio.Bus.getBus(sendIndex)); + }, + + /** + * Triggered when a bus needs its volume db to be updated. + * @param {number} busIndex Index of the bus to update its volume db + * @param {number} volumeDb Volume of the bus + * @returns {void} + */ + set_sample_bus_volume_db: function (busIndex, volumeDb) { + const bus = GodotAudio.Bus.getBus(busIndex); + bus.volumeDb = volumeDb; + }, + + /** + * Triggered when a bus needs to update its solo status + * @param {number} busIndex Index of the bus to update its solo status + * @param {boolean} enable Status of the solo + * @returns {void} + */ + set_sample_bus_solo: function (busIndex, enable) { + const bus = GodotAudio.Bus.getBus(busIndex); + bus.solo(enable); + }, + + /** + * Triggered when a bus needs to update its mute status + * @param {number} busIndex Index of the bus to update its mute status + * @param {boolean} enable Status of the mute + * @returns {void} + */ + set_sample_bus_mute: function (busIndex, enable) { + const bus = GodotAudio.Bus.getBus(busIndex); + bus.mute(enable); + }, }, godot_audio_is_available__sig: 'i', @@ -162,22 +1349,32 @@ const GodotAudio = { godot_audio_has_worklet__proxy: 'sync', godot_audio_has_worklet__sig: 'i', godot_audio_has_worklet: function () { - return (GodotAudio.ctx && GodotAudio.ctx.audioWorklet) ? 1 : 0; + return GodotAudio.ctx && GodotAudio.ctx.audioWorklet ? 1 : 0; }, godot_audio_has_script_processor__proxy: 'sync', godot_audio_has_script_processor__sig: 'i', godot_audio_has_script_processor: function () { - return (GodotAudio.ctx && GodotAudio.ctx.createScriptProcessor) ? 1 : 0; + return GodotAudio.ctx && GodotAudio.ctx.createScriptProcessor ? 1 : 0; }, godot_audio_init__proxy: 'sync', godot_audio_init__sig: 'iiiii', - godot_audio_init: function (p_mix_rate, p_latency, p_state_change, p_latency_update) { + godot_audio_init: function ( + p_mix_rate, + p_latency, + p_state_change, + p_latency_update + ) { const statechange = GodotRuntime.get_func(p_state_change); const latencyupdate = GodotRuntime.get_func(p_latency_update); const mix_rate = GodotRuntime.getHeapValue(p_mix_rate, 'i32'); - const channels = GodotAudio.init(mix_rate, p_latency, statechange, latencyupdate); + const channels = GodotAudio.init( + mix_rate, + p_latency, + statechange, + latencyupdate + ); GodotRuntime.setHeapValue(p_mix_rate, GodotAudio.ctx.sampleRate, 'i32'); return channels; }, @@ -210,10 +1407,311 @@ const GodotAudio = { GodotAudio.input = null; } }, + + godot_audio_sample_stream_is_registered__proxy: 'sync', + godot_audio_sample_stream_is_registered__sig: 'ii', + /** + * Returns if the sample stream is registered + * @param {number} streamObjectIdStrPtr Pointer of the streamObjectId + * @returns {number} + */ + godot_audio_sample_stream_is_registered: function (streamObjectIdStrPtr) { + const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr); + return Number(GodotAudio.Sample.getSampleOrNull(streamObjectId) != null); + }, + + godot_audio_sample_register_stream__proxy: 'sync', + godot_audio_sample_register_stream__sig: 'viiiiiii', + /** + * Registers a stream. + * @param {number} streamObjectIdStrPtr StreamObjectId pointer + * @param {number} framesPtr Frames pointer + * @param {number} framesTotal Frames total value + * @param {number} loopModeStrPtr Loop mode pointer + * @param {number} loopBegin Loop begin value + * @param {number} loopEnd Loop end value + * @returns {void} + */ + godot_audio_sample_register_stream: function ( + streamObjectIdStrPtr, + framesPtr, + framesTotal, + loopModeStrPtr, + loopBegin, + loopEnd + ) { + const BYTES_PER_FLOAT32 = 4; + const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr); + const loopMode = GodotRuntime.parseString(loopModeStrPtr); + const numberOfChannels = 2; + const sampleRate = GodotAudio.ctx.sampleRate; + + /** @type {Float32Array} */ + const subLeft = GodotRuntime.heapSub(HEAPF32, framesPtr, framesTotal); + /** @type {Float32Array} */ + const subRight = GodotRuntime.heapSub( + HEAPF32, + framesPtr + framesTotal * BYTES_PER_FLOAT32, + framesTotal + ); + + const audioBuffer = GodotAudio.ctx.createBuffer( + numberOfChannels, + framesTotal, + sampleRate + ); + audioBuffer.copyToChannel(new Float32Array(subLeft), 0, 0); + audioBuffer.copyToChannel(new Float32Array(subRight), 1, 0); + + GodotAudio.Sample.create( + { + id: streamObjectId, + audioBuffer, + }, + { + loopBegin, + loopEnd, + loopMode, + numberOfChannels, + sampleRate, + } + ); + }, + + godot_audio_sample_unregister_stream__proxy: 'sync', + godot_audio_sample_unregister_stream__sig: 'vi', + /** + * Unregisters a stream. + * @param {number} streamObjectIdStrPtr StreamObjectId pointer + * @returns {void} + */ + godot_audio_sample_unregister_stream: function (streamObjectIdStrPtr) { + const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr); + const sample = GodotAudio.Sample.getSampleOrNull(streamObjectId); + if (sample != null) { + sample.clear(); + } + }, + + godot_audio_sample_start__proxy: 'sync', + godot_audio_sample_start__sig: 'viiiii', + /** + * Starts a sample. + * @param {number} playbackObjectIdStrPtr Playback object id pointer + * @param {number} streamObjectIdStrPtr Stream object id pointer + * @param {number} busIndex Bus index + * @param {number} offset Sample offset + * @param {number} volumePtr Volume pointer + * @returns {void} + */ + godot_audio_sample_start: function ( + playbackObjectIdStrPtr, + streamObjectIdStrPtr, + busIndex, + offset, + volumePtr + ) { + /** @type {string} */ + const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr); + /** @type {string} */ + const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr); + /** @type {Float32Array} */ + const volume = GodotRuntime.heapSub(HEAPF32, volumePtr, 8); + /** @type {SampleNodeConstructorOptions} */ + const startOptions = { + offset, + volume, + playbackRate: 1, + }; + GodotAudio.start_sample( + playbackObjectId, + streamObjectId, + busIndex, + startOptions + ); + }, + + godot_audio_sample_stop__proxy: 'sync', + godot_audio_sample_stop__sig: 'vi', + /** + * Stops a sample from playing. + * @param {number} playbackObjectIdStrPtr Playback object id pointer + * @returns {void} + */ + godot_audio_sample_stop: function (playbackObjectIdStrPtr) { + const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr); + GodotAudio.stop_sample(playbackObjectId); + }, + + godot_audio_sample_set_pause__proxy: 'sync', + godot_audio_sample_set_pause__sig: 'vii', + /** + * Sets the pause state of a sample. + * @param {number} playbackObjectIdStrPtr Playback object id pointer + * @param {number} pause Pause state + */ + godot_audio_sample_set_pause: function (playbackObjectIdStrPtr, pause) { + const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr); + GodotAudio.sample_set_pause(playbackObjectId, Boolean(pause)); + }, + + godot_audio_sample_is_active__proxy: 'sync', + godot_audio_sample_is_active__sig: 'ii', + /** + * Returns if the sample is active. + * @param {number} playbackObjectIdStrPtr Playback object id pointer + * @returns {number} + */ + godot_audio_sample_is_active: function (playbackObjectIdStrPtr) { + const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr); + return Number(GodotAudio.sampleNodes.has(playbackObjectId)); + }, + + godot_audio_sample_update_pitch_scale__proxy: 'sync', + godot_audio_sample_update_pitch_scale__sig: 'vii', + /** + * Updates the pitch scale of a sample. + * @param {number} playbackObjectIdStrPtr Playback object id pointer + * @param {number} pitchScale Pitch scale value + * @returns {void} + */ + godot_audio_sample_update_pitch_scale: function ( + playbackObjectIdStrPtr, + pitchScale + ) { + const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr); + GodotAudio.update_sample_pitch_scale(playbackObjectId, pitchScale); + }, + + godot_audio_sample_set_volumes_linear__proxy: 'sync', + godot_audio_sample_set_volumes_linear__sig: 'vii', + /** + * Sets the volumes linear of each mentioned bus for the sample. + * @param {number} playbackObjectIdStrPtr Playback object id pointer + * @param {number} busesPtr Buses array pointer + * @param {number} busesSize Buses array size + * @param {number} volumesPtr Volumes array pointer + * @param {number} volumesSize Volumes array size + * @returns {void} + */ + godot_audio_sample_set_volumes_linear: function ( + playbackObjectIdStrPtr, + busesPtr, + busesSize, + volumesPtr, + volumesSize + ) { + /** @type {string} */ + const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr); + + /** @type {Uint32Array} */ + const buses = GodotRuntime.heapSub(HEAP32, busesPtr, busesSize); + /** @type {Float32Array} */ + const volumes = GodotRuntime.heapSub(HEAPF32, volumesPtr, volumesSize); + + GodotAudio.sample_set_volumes_linear( + playbackObjectId, + Array.from(buses), + volumes + ); + }, + + godot_audio_sample_bus_set_count__proxy: 'sync', + godot_audio_sample_bus_set_count__sig: 'vi', + /** + * Sets the bus count. + * @param {number} count Bus count + * @returns {void} + */ + godot_audio_sample_bus_set_count: function (count) { + GodotAudio.set_sample_bus_count(count); + }, + + godot_audio_sample_bus_remove__proxy: 'sync', + godot_audio_sample_bus_remove__sig: 'vi', + /** + * Removes a bus. + * @param {number} index Index of the bus to remove + * @returns {void} + */ + godot_audio_sample_bus_remove: function (index) { + GodotAudio.remove_sample_bus(index); + }, + + godot_audio_sample_bus_add__proxy: 'sync', + godot_audio_sample_bus_add__sig: 'vi', + /** + * Adds a bus at the defined position. + * @param {number} atPos Position to add the bus + * @returns {void} + */ + godot_audio_sample_bus_add: function (atPos) { + GodotAudio.add_sample_bus(atPos); + }, + + godot_audio_sample_bus_move__proxy: 'sync', + godot_audio_sample_bus_move__sig: 'vii', + /** + * Moves the bus from a position to another. + * @param {number} fromPos Position of the bus to move + * @param {number} toPos Final position of the bus + * @returns {void} + */ + godot_audio_sample_bus_move: function (fromPos, toPos) { + GodotAudio.move_sample_bus(fromPos, toPos); + }, + + godot_audio_sample_bus_set_send__proxy: 'sync', + godot_audio_sample_bus_set_send__sig: 'vii', + /** + * Sets the "send" of a bus. + * @param {number} bus Position of the bus to set the send + * @param {number} sendIndex Position of the "send" bus + * @returns {void} + */ + godot_audio_sample_bus_set_send: function (bus, sendIndex) { + GodotAudio.set_sample_bus_send(bus, sendIndex); + }, + + godot_audio_sample_bus_set_volume_db__proxy: 'sync', + godot_audio_sample_bus_set_volume_db__sig: 'vii', + /** + * Sets the volume db of a bus. + * @param {number} bus Position of the bus to set the volume db + * @param {number} volumeDb Volume db to set + * @returns {void} + */ + godot_audio_sample_bus_set_volume_db: function (bus, volumeDb) { + GodotAudio.set_sample_bus_volume_db(bus, volumeDb); + }, + + godot_audio_sample_bus_set_solo__proxy: 'sync', + godot_audio_sample_bus_set_solo__sig: 'vii', + /** + * Sets the state of solo for a bus + * @param {number} bus Position of the bus to set the solo state + * @param {number} enable State of the solo + * @returns {void} + */ + godot_audio_sample_bus_set_solo: function (bus, enable) { + GodotAudio.set_sample_bus_solo(bus, Boolean(enable)); + }, + + godot_audio_sample_bus_set_mute__proxy: 'sync', + godot_audio_sample_bus_set_mute__sig: 'vii', + /** + * Sets the state of mute for a bus + * @param {number} bus Position of the bus to set the mute state + * @param {number} enable State of the mute + * @returns {void} + */ + godot_audio_sample_bus_set_mute: function (bus, enable) { + GodotAudio.set_sample_bus_mute(bus, Boolean(enable)); + }, }; -autoAddDeps(GodotAudio, '$GodotAudio'); -mergeInto(LibraryManager.library, GodotAudio); +autoAddDeps(_GodotAudio, '$GodotAudio'); +mergeInto(LibraryManager.library, _GodotAudio); /** * The AudioWorklet API driver, used when threads are available. @@ -227,16 +1725,18 @@ const GodotAudioWorklet = { create: function (channels) { const path = GodotConfig.locate_file('godot.audio.worklet.js'); - GodotAudioWorklet.promise = GodotAudio.ctx.audioWorklet.addModule(path).then(function () { - GodotAudioWorklet.worklet = new AudioWorkletNode( - GodotAudio.ctx, - 'godot-processor', - { - 'outputChannelCount': [channels], - } - ); - return Promise.resolve(); - }); + GodotAudioWorklet.promise = GodotAudio.ctx.audioWorklet + .addModule(path) + .then(function () { + GodotAudioWorklet.worklet = new AudioWorkletNode( + GodotAudio.ctx, + 'godot-processor', + { + outputChannelCount: [channels], + } + ); + return Promise.resolve(); + }); GodotAudio.driver = GodotAudioWorklet; }, @@ -254,7 +1754,14 @@ const GodotAudioWorklet = { }); }, - start_no_threads: function (p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback) { + start_no_threads: function ( + p_out_buf, + p_out_size, + out_callback, + p_in_buf, + p_in_size, + in_callback + ) { function RingBuffer() { let wpos = 0; let rpos = 0; @@ -276,7 +1783,10 @@ const GodotAudioWorklet = { wpos = 0; } if (pending_samples > 0) { - wbuf.set(buffer.subarray(wpos, wpos + pending_samples), tot_sent - pending_samples); + wbuf.set( + buffer.subarray(wpos, wpos + pending_samples), + tot_sent - pending_samples + ); } port.postMessage({ 'cmd': 'chunk', 'data': wbuf.subarray(0, tot_sent) }); wpos += pending_samples; @@ -319,7 +1829,10 @@ const GodotAudioWorklet = { } if (event.data['cmd'] === 'read') { const read = event.data['data']; - GodotAudioWorklet.ring_buffer.consumed(read, GodotAudioWorklet.worklet.port); + GodotAudioWorklet.ring_buffer.consumed( + read, + GodotAudioWorklet.worklet.port + ); } else if (event.data['cmd'] === 'input') { const buf = event.data['data']; if (buf.length > p_in_size) { @@ -376,7 +1889,13 @@ const GodotAudioWorklet = { godot_audio_worklet_start__proxy: 'sync', godot_audio_worklet_start__sig: 'viiiii', - godot_audio_worklet_start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, p_state) { + godot_audio_worklet_start: function ( + p_in_buf, + p_in_size, + p_out_buf, + p_out_size, + p_state + ) { const out_buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size); const in_buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size); const state = GodotRuntime.heapSub(HEAP32, p_state, 4); @@ -385,14 +1904,33 @@ const GodotAudioWorklet = { godot_audio_worklet_start_no_threads__proxy: 'sync', godot_audio_worklet_start_no_threads__sig: 'viiiiii', - godot_audio_worklet_start_no_threads: function (p_out_buf, p_out_size, p_out_callback, p_in_buf, p_in_size, p_in_callback) { + godot_audio_worklet_start_no_threads: function ( + p_out_buf, + p_out_size, + p_out_callback, + p_in_buf, + p_in_size, + p_in_callback + ) { const out_callback = GodotRuntime.get_func(p_out_callback); const in_callback = GodotRuntime.get_func(p_in_callback); - GodotAudioWorklet.start_no_threads(p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback); + GodotAudioWorklet.start_no_threads( + p_out_buf, + p_out_size, + out_callback, + p_in_buf, + p_in_size, + in_callback + ); }, godot_audio_worklet_state_wait__sig: 'iiii', - godot_audio_worklet_state_wait: function (p_state, p_idx, p_expected, p_timeout) { + godot_audio_worklet_state_wait: function ( + p_state, + p_idx, + p_expected, + p_timeout + ) { Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout); return Atomics.load(HEAP32, (p_state >> 2) + p_idx); }, @@ -412,7 +1950,7 @@ autoAddDeps(GodotAudioWorklet, '$GodotAudioWorklet'); mergeInto(LibraryManager.library, GodotAudioWorklet); /* - * The deprecated ScriptProcessorNode API, used when threads are disabled. + * The ScriptProcessorNode API, used when threads are disabled. */ const GodotAudioScript = { $GodotAudioScript__deps: ['$GodotAudio'], @@ -420,7 +1958,11 @@ const GodotAudioScript = { script: null, create: function (buffer_length, channel_count) { - GodotAudioScript.script = GodotAudio.ctx.createScriptProcessor(buffer_length, 2, channel_count); + GodotAudioScript.script = GodotAudio.ctx.createScriptProcessor( + buffer_length, + 2, + channel_count + ); GodotAudio.driver = GodotAudioScript; return GodotAudioScript.script.bufferSize; }, @@ -488,9 +2030,21 @@ const GodotAudioScript = { godot_audio_script_start__proxy: 'sync', godot_audio_script_start__sig: 'viiiii', - godot_audio_script_start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, p_cb) { + godot_audio_script_start: function ( + p_in_buf, + p_in_size, + p_out_buf, + p_out_size, + p_cb + ) { const onprocess = GodotRuntime.get_func(p_cb); - GodotAudioScript.start(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess); + GodotAudioScript.start( + p_in_buf, + p_in_size, + p_out_buf, + p_out_size, + onprocess + ); }, }; |