You've already forked godot
mirror of
https://github.com/godotengine/godot.git
synced 2025-11-07 12:30:27 +00:00
Godot has a ScriptProcessorNode audio driver implementation for the (deprecated) Web API. As reported by some users, this fallback was not properly re-added during the Godot 4 transition, and was left as "dead code". While the API is deprecated, it is still supported by most browsers, and some WebView may not implement AudioWorklet correctly (the new recommended API). This commit re-adds the ScriptProcessorNode implementation as a fallback if the AudioWorklet driver fails to initialized (and can be forced if desired via project settings as usual).
2263 lines
58 KiB
JavaScript
2263 lines
58 KiB
JavaScript
/**************************************************************************/
|
|
/* library_godot_audio.js */
|
|
/**************************************************************************/
|
|
/* This file is part of: */
|
|
/* GODOT ENGINE */
|
|
/* https://godotengine.org */
|
|
/**************************************************************************/
|
|
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
|
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
|
/* */
|
|
/* Permission is hereby granted, free of charge, to any person obtaining */
|
|
/* a copy of this software and associated documentation files (the */
|
|
/* "Software"), to deal in the Software without restriction, including */
|
|
/* without limitation the rights to use, copy, modify, merge, publish, */
|
|
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
|
/* permit persons to whom the Software is furnished to do so, subject to */
|
|
/* the following conditions: */
|
|
/* */
|
|
/* The above copyright notice and this permission notice shall be */
|
|
/* included in all copies or substantial portions of the Software. */
|
|
/* */
|
|
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
|
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
|
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
|
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
|
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
|
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
|
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
|
/**************************************************************************/
|
|
|
|
/**
|
|
* @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(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.setAudioBuffer(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 {Array<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(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
|
|
* pitchScale?: number
|
|
* loopMode?: LoopMode
|
|
* volume?: Float32Array
|
|
* start?: boolean
|
|
* }} 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(params, options = {}) {
|
|
/** @type {string} */
|
|
this.id = params.id;
|
|
/** @type {string} */
|
|
this.streamObjectId = params.streamObjectId;
|
|
/** @type {number} */
|
|
this.offset = options.offset ?? 0;
|
|
/** @type {number} */
|
|
this._playbackPosition = options.offset;
|
|
/** @type {number} */
|
|
this.startTime = options.startTime ?? 0;
|
|
/** @type {boolean} */
|
|
this.isPaused = false;
|
|
/** @type {boolean} */
|
|
this.isStarted = false;
|
|
/** @type {boolean} */
|
|
this.isCanceled = false;
|
|
/** @type {number} */
|
|
this.pauseTime = 0;
|
|
/** @type {number} */
|
|
this._playbackRate = 44100;
|
|
/** @type {LoopMode} */
|
|
this.loopMode = options.loopMode ?? this.getSample().loopMode ?? 'disabled';
|
|
/** @type {number} */
|
|
this._pitchScale = options.pitchScale ?? 1;
|
|
/** @type {number} */
|
|
this._sourceStartTime = 0;
|
|
/** @type {Map<Bus, SampleNodeBus>} */
|
|
this._sampleNodeBuses = new Map();
|
|
/** @type {AudioBufferSourceNode | null} */
|
|
this._source = GodotAudio.ctx.createBufferSource();
|
|
|
|
this._onended = null;
|
|
/** @type {AudioWorkletNode | null} */
|
|
this._positionWorklet = null;
|
|
|
|
this.setPlaybackRate(options.playbackRate ?? 44100);
|
|
this._source.buffer = this.getSample().getAudioBuffer();
|
|
|
|
this._addEndedListener();
|
|
|
|
const bus = GodotAudio.Bus.getBus(params.busIndex);
|
|
const sampleNodeBus = this.getSampleNodeBus(bus);
|
|
sampleNodeBus.setVolume(options.volume);
|
|
|
|
this.connectPositionWorklet(options.start);
|
|
}
|
|
|
|
/**
|
|
* Gets the playback rate.
|
|
* @returns {number}
|
|
*/
|
|
getPlaybackRate() {
|
|
return this._playbackRate;
|
|
}
|
|
|
|
/**
|
|
* Gets the playback position.
|
|
* @returns {number}
|
|
*/
|
|
getPlaybackPosition() {
|
|
return this._playbackPosition;
|
|
}
|
|
|
|
/**
|
|
* 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() {
|
|
if (this.isStarted) {
|
|
return;
|
|
}
|
|
this._resetSourceStartTime();
|
|
this._source.start(this.startTime, this.offset);
|
|
this.isStarted = true;
|
|
}
|
|
|
|
/**
|
|
* Stops the `SampleNode`.
|
|
* @returns {void}
|
|
*/
|
|
stop() {
|
|
this.clear();
|
|
}
|
|
|
|
/**
|
|
* Restarts the `SampleNode`.
|
|
*/
|
|
restart() {
|
|
this.isPaused = false;
|
|
this.pauseTime = 0;
|
|
this._resetSourceStartTime();
|
|
this._restart();
|
|
}
|
|
|
|
/**
|
|
* Pauses the `SampleNode`.
|
|
* @param {boolean} [enable=true] State of the pause.
|
|
* @returns {void}
|
|
*/
|
|
pause(enable = true) {
|
|
if (enable) {
|
|
this._pause();
|
|
return;
|
|
}
|
|
|
|
this._unpause();
|
|
}
|
|
|
|
/**
|
|
* 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 {Array<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);
|
|
}
|
|
|
|
/**
|
|
* Sets up and connects the source to the GodotPositionReportingProcessor
|
|
* If the worklet module is not loaded in, it will be added
|
|
*/
|
|
connectPositionWorklet(start) {
|
|
try {
|
|
this._positionWorklet = this.createPositionWorklet();
|
|
this._source.connect(this._positionWorklet);
|
|
if (start) {
|
|
this.start();
|
|
}
|
|
} catch (error) {
|
|
if (error?.name !== 'InvalidStateError') {
|
|
throw error;
|
|
}
|
|
const path = GodotConfig.locate_file('godot.audio.position.worklet.js');
|
|
GodotAudio.ctx.audioWorklet
|
|
.addModule(path)
|
|
.then(() => {
|
|
if (!this.isCanceled) {
|
|
this._positionWorklet = this.createPositionWorklet();
|
|
this._source.connect(this._positionWorklet);
|
|
if (start) {
|
|
this.start();
|
|
}
|
|
}
|
|
}).catch((addModuleError) => {
|
|
GodotRuntime.error('Failed to create PositionWorklet.', addModuleError);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates the AudioWorkletProcessor used to track playback position.
|
|
* @returns {AudioWorkletNode}
|
|
*/
|
|
createPositionWorklet() {
|
|
const worklet = new AudioWorkletNode(
|
|
GodotAudio.ctx,
|
|
'godot-position-reporting-processor'
|
|
);
|
|
worklet.port.onmessage = (event) => {
|
|
switch (event.data['type']) {
|
|
case 'position':
|
|
this._playbackPosition = (parseInt(event.data.data, 10) / this.getSample().sampleRate) + this.offset;
|
|
break;
|
|
default:
|
|
// Do nothing.
|
|
}
|
|
};
|
|
return worklet;
|
|
}
|
|
|
|
/**
|
|
* Clears the `SampleNode`.
|
|
* @returns {void}
|
|
*/
|
|
clear() {
|
|
this.isCanceled = true;
|
|
this.isPaused = false;
|
|
this.pauseTime = 0;
|
|
|
|
if (this._source != null) {
|
|
this._source.removeEventListener('ended', this._onended);
|
|
this._onended = null;
|
|
if (this.isStarted) {
|
|
this._source.stop();
|
|
}
|
|
this._source.disconnect();
|
|
this._source = null;
|
|
}
|
|
|
|
for (const sampleNodeBus of this._sampleNodeBuses.values()) {
|
|
sampleNodeBus.clear();
|
|
}
|
|
this._sampleNodeBuses.clear();
|
|
|
|
if (this._positionWorklet) {
|
|
this._positionWorklet.disconnect();
|
|
this._positionWorklet.port.onmessage = null;
|
|
this._positionWorklet = null;
|
|
}
|
|
|
|
GodotAudio.SampleNode.delete(this.id);
|
|
}
|
|
|
|
/**
|
|
* Resets the source start time
|
|
* @returns {void}
|
|
*/
|
|
_resetSourceStartTime() {
|
|
this._sourceStartTime = GodotAudio.ctx.currentTime;
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
|
|
/**
|
|
* Restarts the `SampleNode`.
|
|
* Honors `isPaused` and `pauseTime`.
|
|
* @returns {void}
|
|
*/
|
|
_restart() {
|
|
if (this._source != null) {
|
|
this._source.disconnect();
|
|
}
|
|
this._source = GodotAudio.ctx.createBufferSource();
|
|
this._source.buffer = this.getSample().getAudioBuffer();
|
|
|
|
// Make sure that we connect the new source to the sample node bus.
|
|
for (const sampleNodeBus of this._sampleNodeBuses.values()) {
|
|
this.connect(sampleNodeBus.getInputNode());
|
|
}
|
|
|
|
this._addEndedListener();
|
|
const pauseTime = this.isPaused
|
|
? this.pauseTime
|
|
: 0;
|
|
this.connectPositionWorklet();
|
|
this._source.start(this.startTime, this.offset + pauseTime);
|
|
this.isStarted = true;
|
|
}
|
|
|
|
/**
|
|
* Pauses the `SampleNode`.
|
|
* @returns {void}
|
|
*/
|
|
_pause() {
|
|
this.isPaused = true;
|
|
this.pauseTime = (GodotAudio.ctx.currentTime - this._sourceStartTime) / this.getPlaybackRate();
|
|
this._source.stop();
|
|
}
|
|
|
|
/**
|
|
* Unpauses the `SampleNode`.
|
|
* @returns {void}
|
|
*/
|
|
_unpause() {
|
|
this._restart();
|
|
this.isPaused = false;
|
|
this.pauseTime = 0;
|
|
}
|
|
|
|
/**
|
|
* Adds an "ended" listener to the source node to repeat it if necessary.
|
|
* @returns {void}
|
|
*/
|
|
_addEndedListener() {
|
|
if (this._onended != null) {
|
|
this._source.removeEventListener('ended', this._onended);
|
|
}
|
|
|
|
/** @type {SampleNode} */
|
|
// eslint-disable-next-line consistent-this
|
|
const self = this;
|
|
this._onended = (_) => {
|
|
if (self.isPaused) {
|
|
return;
|
|
}
|
|
|
|
switch (self.getSample().loopMode) {
|
|
case 'disabled': {
|
|
const id = this.id;
|
|
self.stop();
|
|
if (GodotAudio.sampleFinishedCallback != null) {
|
|
const idCharPtr = GodotRuntime.allocString(id);
|
|
GodotAudio.sampleFinishedCallback(idCharPtr);
|
|
GodotRuntime.free(idCharPtr);
|
|
}
|
|
} break;
|
|
case 'forward':
|
|
case 'backward':
|
|
self.restart();
|
|
break;
|
|
default:
|
|
// do nothing
|
|
}
|
|
};
|
|
this._source.addEventListener('ended', this._onended);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.getBusOrNull(fromIndex);
|
|
if (movedBus == null) {
|
|
return;
|
|
}
|
|
const buses = GodotAudio.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() {
|
|
/** @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) {
|
|
const linear = GodotAudio.db_to_linear(val);
|
|
if (isFinite(linear)) {
|
|
this._gainNode.gain.value = linear;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
_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 {Array<Bus>}
|
|
*/
|
|
buses: null,
|
|
/**
|
|
* Reference to the current bus in solo mode.
|
|
* @type {Bus | null}
|
|
*/
|
|
busSolo: null,
|
|
Bus,
|
|
|
|
/**
|
|
* Callback to signal that a sample has finished.
|
|
* @type {(playbackObjectIdPtr: number) => void | null}
|
|
*/
|
|
sampleFinishedCallback: null,
|
|
|
|
/** @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.
|
|
// opts['latencyHint'] = latency / 1000;
|
|
const ctx = new (window.AudioContext || window.webkitAudioContext)(opts);
|
|
GodotAudio.ctx = ctx;
|
|
ctx.onstatechange = function () {
|
|
let state = 0;
|
|
switch (ctx.state) {
|
|
case 'suspended':
|
|
state = 0;
|
|
break;
|
|
case 'running':
|
|
state = 1;
|
|
break;
|
|
case 'closed':
|
|
state = 2;
|
|
break;
|
|
default:
|
|
// Do nothing.
|
|
}
|
|
onstatechange(state);
|
|
};
|
|
ctx.onstatechange(); // Immediately notify state.
|
|
// Update computed latency
|
|
GodotAudio.interval = setInterval(function () {
|
|
let computed_latency = 0;
|
|
if (ctx.baseLatency) {
|
|
computed_latency += GodotAudio.ctx.baseLatency;
|
|
}
|
|
if (ctx.outputLatency) {
|
|
computed_latency += GodotAudio.ctx.outputLatency;
|
|
}
|
|
onlatencyupdate(computed_latency);
|
|
}, 1000);
|
|
GodotOS.atexit(GodotAudio.close_async);
|
|
return ctx.destination.channelCount;
|
|
},
|
|
|
|
create_input: function (callback) {
|
|
if (GodotAudio.input) {
|
|
return 0; // Already started.
|
|
}
|
|
function gotMediaInput(stream) {
|
|
try {
|
|
GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream);
|
|
callback(GodotAudio.input);
|
|
} catch (e) {
|
|
GodotRuntime.error('Failed creating input.', e);
|
|
}
|
|
}
|
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
navigator.mediaDevices.getUserMedia({
|
|
'audio': true,
|
|
}).then(gotMediaInput, function (e) {
|
|
GodotRuntime.error('Error getting user media.', e);
|
|
});
|
|
} else {
|
|
if (!navigator.getUserMedia) {
|
|
navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
|
|
}
|
|
if (!navigator.getUserMedia) {
|
|
GodotRuntime.error('getUserMedia not available.');
|
|
return 1;
|
|
}
|
|
navigator.getUserMedia({
|
|
'audio': true,
|
|
}, gotMediaInput, function (e) {
|
|
GodotRuntime.print(e);
|
|
});
|
|
}
|
|
return 0;
|
|
},
|
|
|
|
close_async: function (resolve, reject) {
|
|
const ctx = GodotAudio.ctx;
|
|
GodotAudio.ctx = null;
|
|
// Audio was not initialized.
|
|
if (!ctx) {
|
|
resolve();
|
|
return;
|
|
}
|
|
// Remove latency callback
|
|
if (GodotAudio.interval) {
|
|
clearInterval(GodotAudio.interval);
|
|
GodotAudio.interval = 0;
|
|
}
|
|
// Disconnect input, if it was started.
|
|
if (GodotAudio.input) {
|
|
GodotAudio.input.disconnect();
|
|
GodotAudio.input = null;
|
|
}
|
|
// Disconnect output
|
|
let closed = Promise.resolve();
|
|
if (GodotAudio.driver) {
|
|
closed = GodotAudio.driver.close();
|
|
}
|
|
closed.then(function () {
|
|
return ctx.close();
|
|
}).then(function () {
|
|
ctx.onstatechange = null;
|
|
resolve();
|
|
}).catch(function (e) {
|
|
ctx.onstatechange = null;
|
|
GodotRuntime.error('Error closing AudioContext', e);
|
|
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);
|
|
GodotAudio.SampleNode.create(
|
|
{
|
|
busIndex,
|
|
id: playbackObjectId,
|
|
streamObjectId,
|
|
},
|
|
startOptions
|
|
);
|
|
},
|
|
|
|
/**
|
|
* 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 {Array<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.getBusOrNull(index);
|
|
if (bus == null) {
|
|
return;
|
|
}
|
|
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.getBusOrNull(busIndex);
|
|
if (bus == null) {
|
|
// Cannot send from an invalid bus.
|
|
return;
|
|
}
|
|
let targetBus = GodotAudio.Bus.getBusOrNull(sendIndex);
|
|
if (targetBus == null) {
|
|
// Send to master.
|
|
targetBus = GodotAudio.Bus.getBus(0);
|
|
}
|
|
bus.setSend(targetBus);
|
|
},
|
|
|
|
/**
|
|
* 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.getBusOrNull(busIndex);
|
|
if (bus == null) {
|
|
return;
|
|
}
|
|
bus.setVolumeDb(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.getBusOrNull(busIndex);
|
|
if (bus == null) {
|
|
return;
|
|
}
|
|
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.getBusOrNull(busIndex);
|
|
if (bus == null) {
|
|
return;
|
|
}
|
|
bus.mute(enable);
|
|
},
|
|
},
|
|
|
|
godot_audio_is_available__sig: 'i',
|
|
godot_audio_is_available__proxy: 'sync',
|
|
godot_audio_is_available: function () {
|
|
if (!(window.AudioContext || window.webkitAudioContext)) {
|
|
return 0;
|
|
}
|
|
return 1;
|
|
},
|
|
|
|
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;
|
|
},
|
|
|
|
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;
|
|
},
|
|
|
|
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
|
|
) {
|
|
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
|
|
);
|
|
GodotRuntime.setHeapValue(p_mix_rate, GodotAudio.ctx.sampleRate, 'i32');
|
|
return channels;
|
|
},
|
|
|
|
godot_audio_resume__proxy: 'sync',
|
|
godot_audio_resume__sig: 'v',
|
|
godot_audio_resume: function () {
|
|
if (GodotAudio.ctx && GodotAudio.ctx.state !== 'running') {
|
|
GodotAudio.ctx.resume();
|
|
}
|
|
},
|
|
|
|
godot_audio_input_start__proxy: 'sync',
|
|
godot_audio_input_start__sig: 'i',
|
|
godot_audio_input_start: function () {
|
|
return GodotAudio.create_input(function (input) {
|
|
input.connect(GodotAudio.driver.get_node());
|
|
});
|
|
},
|
|
|
|
godot_audio_input_stop__proxy: 'sync',
|
|
godot_audio_input_stop__sig: 'v',
|
|
godot_audio_input_stop: function () {
|
|
if (GodotAudio.input) {
|
|
const tracks = GodotAudio.input['mediaStream']['getTracks']();
|
|
for (let i = 0; i < tracks.length; i++) {
|
|
tracks[i]['stop']();
|
|
}
|
|
GodotAudio.input.disconnect();
|
|
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: 'viiiifi',
|
|
/**
|
|
* 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} pitchScale Pitch scale
|
|
* @param {number} volumePtr Volume pointer
|
|
* @returns {void}
|
|
*/
|
|
godot_audio_sample_start: function (
|
|
playbackObjectIdStrPtr,
|
|
streamObjectIdStrPtr,
|
|
busIndex,
|
|
offset,
|
|
pitchScale,
|
|
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 {SampleNodeOptions} */
|
|
const startOptions = {
|
|
offset,
|
|
volume,
|
|
playbackRate: 1,
|
|
pitchScale,
|
|
start: true,
|
|
};
|
|
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_get_sample_playback_position__proxy: 'sync',
|
|
godot_audio_get_sample_playback_position__sig: 'di',
|
|
/**
|
|
* Returns the position of the playback position.
|
|
* @param {number} playbackObjectIdStrPtr Playback object id pointer
|
|
* @returns {number}
|
|
*/
|
|
godot_audio_get_sample_playback_position: function (playbackObjectIdStrPtr) {
|
|
const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr);
|
|
const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(playbackObjectId);
|
|
if (sampleNode == null) {
|
|
return 0;
|
|
}
|
|
return sampleNode.getPlaybackPosition();
|
|
},
|
|
|
|
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));
|
|
},
|
|
|
|
godot_audio_sample_set_finished_callback__proxy: 'sync',
|
|
godot_audio_sample_set_finished_callback__sig: 'vi',
|
|
/**
|
|
* Sets the finished callback
|
|
* @param {Number} callbackPtr Finished callback pointer
|
|
* @returns {void}
|
|
*/
|
|
godot_audio_sample_set_finished_callback: function (callbackPtr) {
|
|
GodotAudio.sampleFinishedCallback = GodotRuntime.get_func(callbackPtr);
|
|
},
|
|
};
|
|
|
|
autoAddDeps(_GodotAudio, '$GodotAudio');
|
|
mergeInto(LibraryManager.library, _GodotAudio);
|
|
|
|
/**
|
|
* The AudioWorklet API driver, used when threads are available.
|
|
*/
|
|
const GodotAudioWorklet = {
|
|
$GodotAudioWorklet__deps: ['$GodotAudio', '$GodotConfig'],
|
|
$GodotAudioWorklet: {
|
|
promise: null,
|
|
worklet: null,
|
|
ring_buffer: null,
|
|
|
|
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();
|
|
});
|
|
GodotAudio.driver = GodotAudioWorklet;
|
|
},
|
|
|
|
start: function (in_buf, out_buf, state) {
|
|
GodotAudioWorklet.promise.then(function () {
|
|
const node = GodotAudioWorklet.worklet;
|
|
node.connect(GodotAudio.ctx.destination);
|
|
node.port.postMessage({
|
|
'cmd': 'start',
|
|
'data': [state, in_buf, out_buf],
|
|
});
|
|
node.port.onmessage = function (event) {
|
|
GodotRuntime.error(event.data);
|
|
};
|
|
});
|
|
},
|
|
|
|
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;
|
|
let pending_samples = 0;
|
|
const wbuf = new Float32Array(p_out_size);
|
|
|
|
function send(port) {
|
|
if (pending_samples === 0) {
|
|
return;
|
|
}
|
|
const buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size);
|
|
const size = buffer.length;
|
|
const tot_sent = pending_samples;
|
|
out_callback(wpos, pending_samples);
|
|
if (wpos + pending_samples >= size) {
|
|
const high = size - wpos;
|
|
wbuf.set(buffer.subarray(wpos, size));
|
|
pending_samples -= high;
|
|
wpos = 0;
|
|
}
|
|
if (pending_samples > 0) {
|
|
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;
|
|
pending_samples = 0;
|
|
}
|
|
this.receive = function (recv_buf) {
|
|
const buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size);
|
|
const from = rpos;
|
|
let to_write = recv_buf.length;
|
|
let high = 0;
|
|
if (rpos + to_write >= p_in_size) {
|
|
high = p_in_size - rpos;
|
|
buffer.set(recv_buf.subarray(0, high), rpos);
|
|
to_write -= high;
|
|
rpos = 0;
|
|
}
|
|
if (to_write) {
|
|
buffer.set(recv_buf.subarray(high, to_write), rpos);
|
|
}
|
|
in_callback(from, recv_buf.length);
|
|
rpos += to_write;
|
|
};
|
|
this.consumed = function (size, port) {
|
|
pending_samples += size;
|
|
send(port);
|
|
};
|
|
}
|
|
GodotAudioWorklet.ring_buffer = new RingBuffer();
|
|
GodotAudioWorklet.promise.then(function () {
|
|
const node = GodotAudioWorklet.worklet;
|
|
const buffer = GodotRuntime.heapSlice(HEAPF32, p_out_buf, p_out_size);
|
|
node.connect(GodotAudio.ctx.destination);
|
|
node.port.postMessage({
|
|
'cmd': 'start_nothreads',
|
|
'data': [buffer, p_in_size],
|
|
});
|
|
node.port.onmessage = function (event) {
|
|
if (!GodotAudioWorklet.worklet) {
|
|
return;
|
|
}
|
|
if (event.data['cmd'] === 'read') {
|
|
const read = event.data['data'];
|
|
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) {
|
|
GodotRuntime.error('Input chunk is too big');
|
|
return;
|
|
}
|
|
GodotAudioWorklet.ring_buffer.receive(buf);
|
|
} else {
|
|
GodotRuntime.error(event.data);
|
|
}
|
|
};
|
|
});
|
|
},
|
|
|
|
get_node: function () {
|
|
return GodotAudioWorklet.worklet;
|
|
},
|
|
|
|
close: function () {
|
|
return new Promise(function (resolve, reject) {
|
|
if (GodotAudioWorklet.promise === null) {
|
|
return;
|
|
}
|
|
const p = GodotAudioWorklet.promise;
|
|
p.then(function () {
|
|
GodotAudioWorklet.worklet.port.postMessage({
|
|
'cmd': 'stop',
|
|
'data': null,
|
|
});
|
|
GodotAudioWorklet.worklet.disconnect();
|
|
GodotAudioWorklet.worklet.port.onmessage = null;
|
|
GodotAudioWorklet.worklet = null;
|
|
GodotAudioWorklet.promise = null;
|
|
resolve();
|
|
}).catch(function (err) {
|
|
// Aborted?
|
|
GodotRuntime.error(err);
|
|
});
|
|
});
|
|
},
|
|
},
|
|
|
|
godot_audio_worklet_create__proxy: 'sync',
|
|
godot_audio_worklet_create__sig: 'ii',
|
|
godot_audio_worklet_create: function (channels) {
|
|
try {
|
|
GodotAudioWorklet.create(channels);
|
|
} catch (e) {
|
|
GodotRuntime.error('Error starting AudioDriverWorklet', e);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
},
|
|
|
|
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
|
|
) {
|
|
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);
|
|
GodotAudioWorklet.start(in_buffer, out_buffer, state);
|
|
},
|
|
|
|
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
|
|
) {
|
|
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
|
|
);
|
|
},
|
|
|
|
godot_audio_worklet_state_wait__sig: 'iiii',
|
|
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);
|
|
},
|
|
|
|
godot_audio_worklet_state_add__sig: 'iiii',
|
|
godot_audio_worklet_state_add: function (p_state, p_idx, p_value) {
|
|
return Atomics.add(HEAP32, (p_state >> 2) + p_idx, p_value);
|
|
},
|
|
|
|
godot_audio_worklet_state_get__sig: 'iii',
|
|
godot_audio_worklet_state_get: function (p_state, p_idx) {
|
|
return Atomics.load(HEAP32, (p_state >> 2) + p_idx);
|
|
},
|
|
};
|
|
|
|
autoAddDeps(GodotAudioWorklet, '$GodotAudioWorklet');
|
|
mergeInto(LibraryManager.library, GodotAudioWorklet);
|
|
|
|
/*
|
|
* The ScriptProcessorNode API, used as a fallback if AudioWorklet is not available.
|
|
*/
|
|
const GodotAudioScript = {
|
|
$GodotAudioScript__deps: ['$GodotAudio'],
|
|
$GodotAudioScript: {
|
|
script: null,
|
|
|
|
create: function (buffer_length, channel_count) {
|
|
GodotAudioScript.script = GodotAudio.ctx.createScriptProcessor(
|
|
buffer_length,
|
|
2,
|
|
channel_count
|
|
);
|
|
GodotAudio.driver = GodotAudioScript;
|
|
return GodotAudioScript.script.bufferSize;
|
|
},
|
|
|
|
start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess) {
|
|
GodotAudioScript.script.onaudioprocess = function (event) {
|
|
// Read input
|
|
const inb = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size);
|
|
const input = event.inputBuffer;
|
|
if (GodotAudio.input) {
|
|
const inlen = input.getChannelData(0).length;
|
|
for (let ch = 0; ch < 2; ch++) {
|
|
const data = input.getChannelData(ch);
|
|
for (let s = 0; s < inlen; s++) {
|
|
inb[s * 2 + ch] = data[s];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Let Godot process the input/output.
|
|
onprocess();
|
|
|
|
// Write the output.
|
|
const outb = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size);
|
|
const output = event.outputBuffer;
|
|
const channels = output.numberOfChannels;
|
|
for (let ch = 0; ch < channels; ch++) {
|
|
const data = output.getChannelData(ch);
|
|
// Loop through samples and assign computed values.
|
|
for (let sample = 0; sample < data.length; sample++) {
|
|
data[sample] = outb[sample * channels + ch];
|
|
}
|
|
}
|
|
};
|
|
GodotAudioScript.script.connect(GodotAudio.ctx.destination);
|
|
},
|
|
|
|
get_node: function () {
|
|
return GodotAudioScript.script;
|
|
},
|
|
|
|
close: function () {
|
|
return new Promise(function (resolve, reject) {
|
|
GodotAudioScript.script.disconnect();
|
|
GodotAudioScript.script.onaudioprocess = null;
|
|
GodotAudioScript.script = null;
|
|
resolve();
|
|
});
|
|
},
|
|
},
|
|
|
|
godot_audio_script_create__proxy: 'sync',
|
|
godot_audio_script_create__sig: 'iii',
|
|
godot_audio_script_create: function (buffer_length, channel_count) {
|
|
const buf_len = GodotRuntime.getHeapValue(buffer_length, 'i32');
|
|
try {
|
|
const out_len = GodotAudioScript.create(buf_len, channel_count);
|
|
GodotRuntime.setHeapValue(buffer_length, out_len, 'i32');
|
|
} catch (e) {
|
|
GodotRuntime.error('Error starting AudioDriverScriptProcessor', e);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
},
|
|
|
|
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
|
|
) {
|
|
const onprocess = GodotRuntime.get_func(p_cb);
|
|
GodotAudioScript.start(
|
|
p_in_buf,
|
|
p_in_size,
|
|
p_out_buf,
|
|
p_out_size,
|
|
onprocess
|
|
);
|
|
},
|
|
};
|
|
|
|
autoAddDeps(GodotAudioScript, '$GodotAudioScript');
|
|
mergeInto(LibraryManager.library, GodotAudioScript);
|