Path: blob/master/platform/web/js/libs/library_godot_audio.js
10279 views
/**************************************************************************/1/* library_godot_audio.js */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930/**31* @typedef { "disabled" | "forward" | "backward" | "pingpong" } LoopMode32*/3334/**35* @typedef {{36* id: string37* audioBuffer: AudioBuffer38* }} SampleParams39* @typedef {{40* numberOfChannels?: number41* sampleRate?: number42* loopMode?: LoopMode43* loopBegin?: number44* loopEnd?: number45* }} SampleOptions46*/4748/**49* Represents a sample, memory-wise.50* @class51*/52class Sample {53/**54* Returns a `Sample`.55* @param {string} id Id of the `Sample` to get.56* @returns {Sample}57* @throws {ReferenceError} When no `Sample` is found58*/59static getSample(id) {60if (!GodotAudio.samples.has(id)) {61throw new ReferenceError(`Could not find sample "${id}"`);62}63return GodotAudio.samples.get(id);64}6566/**67* Returns a `Sample` or `null`, if it doesn't exist.68* @param {string} id Id of the `Sample` to get.69* @returns {Sample?}70*/71static getSampleOrNull(id) {72return GodotAudio.samples.get(id) ?? null;73}7475/**76* Creates a `Sample` based on the params. Will register it to the77* `GodotAudio.samples` registry.78* @param {SampleParams} params Base params79* @param {SampleOptions | undefined} options Optional params.80* @returns {Sample}81*/82static create(params, options = {}) {83const sample = new GodotAudio.Sample(params, options);84GodotAudio.samples.set(params.id, sample);85return sample;86}8788/**89* Deletes a `Sample` based on the id.90* @param {string} id `Sample` id to delete91* @returns {void}92*/93static delete(id) {94GodotAudio.samples.delete(id);95}9697/**98* `Sample` constructor.99* @param {SampleParams} params Base params100* @param {SampleOptions | undefined} options Optional params.101*/102constructor(params, options = {}) {103/** @type {string} */104this.id = params.id;105/** @type {AudioBuffer} */106this._audioBuffer = null;107/** @type {number} */108this.numberOfChannels = options.numberOfChannels ?? 2;109/** @type {number} */110this.sampleRate = options.sampleRate ?? 44100;111/** @type {LoopMode} */112this.loopMode = options.loopMode ?? 'disabled';113/** @type {number} */114this.loopBegin = options.loopBegin ?? 0;115/** @type {number} */116this.loopEnd = options.loopEnd ?? 0;117118this.setAudioBuffer(params.audioBuffer);119}120121/**122* Gets the audio buffer of the sample.123* @returns {AudioBuffer}124*/125getAudioBuffer() {126return this._duplicateAudioBuffer();127}128129/**130* Sets the audio buffer of the sample.131* @param {AudioBuffer} val The audio buffer to set.132* @returns {void}133*/134setAudioBuffer(val) {135this._audioBuffer = val;136}137138/**139* Clears the current sample.140* @returns {void}141*/142clear() {143this.setAudioBuffer(null);144GodotAudio.Sample.delete(this.id);145}146147/**148* Returns a duplicate of the stored audio buffer.149* @returns {AudioBuffer}150*/151_duplicateAudioBuffer() {152if (this._audioBuffer == null) {153throw new Error('couldn\'t duplicate a null audioBuffer');154}155/** @type {Array<Float32Array>} */156const channels = new Array(this._audioBuffer.numberOfChannels);157for (let i = 0; i < this._audioBuffer.numberOfChannels; i++) {158const channel = new Float32Array(this._audioBuffer.getChannelData(i));159channels[i] = channel;160}161const buffer = GodotAudio.ctx.createBuffer(162this.numberOfChannels,163this._audioBuffer.length,164this._audioBuffer.sampleRate165);166for (let i = 0; i < channels.length; i++) {167buffer.copyToChannel(channels[i], i, 0);168}169return buffer;170}171}172173/**174* Represents a `SampleNode` linked to a `Bus`.175* @class176*/177class SampleNodeBus {178/**179* Creates a new `SampleNodeBus`.180* @param {Bus} bus The bus related to the new `SampleNodeBus`.181* @returns {SampleNodeBus}182*/183static create(bus) {184return new GodotAudio.SampleNodeBus(bus);185}186187/**188* `SampleNodeBus` constructor.189* @param {Bus} bus The bus related to the new `SampleNodeBus`.190*/191constructor(bus) {192const NUMBER_OF_WEB_CHANNELS = 6;193194/** @type {Bus} */195this._bus = bus;196197/** @type {ChannelSplitterNode} */198this._channelSplitter = GodotAudio.ctx.createChannelSplitter(NUMBER_OF_WEB_CHANNELS);199/** @type {GainNode} */200this._l = GodotAudio.ctx.createGain();201/** @type {GainNode} */202this._r = GodotAudio.ctx.createGain();203/** @type {GainNode} */204this._sl = GodotAudio.ctx.createGain();205/** @type {GainNode} */206this._sr = GodotAudio.ctx.createGain();207/** @type {GainNode} */208this._c = GodotAudio.ctx.createGain();209/** @type {GainNode} */210this._lfe = GodotAudio.ctx.createGain();211/** @type {ChannelMergerNode} */212this._channelMerger = GodotAudio.ctx.createChannelMerger(NUMBER_OF_WEB_CHANNELS);213214this._channelSplitter215.connect(this._l, GodotAudio.WebChannel.CHANNEL_L)216.connect(217this._channelMerger,218GodotAudio.WebChannel.CHANNEL_L,219GodotAudio.WebChannel.CHANNEL_L220);221this._channelSplitter222.connect(this._r, GodotAudio.WebChannel.CHANNEL_R)223.connect(224this._channelMerger,225GodotAudio.WebChannel.CHANNEL_L,226GodotAudio.WebChannel.CHANNEL_R227);228this._channelSplitter229.connect(this._sl, GodotAudio.WebChannel.CHANNEL_SL)230.connect(231this._channelMerger,232GodotAudio.WebChannel.CHANNEL_L,233GodotAudio.WebChannel.CHANNEL_SL234);235this._channelSplitter236.connect(this._sr, GodotAudio.WebChannel.CHANNEL_SR)237.connect(238this._channelMerger,239GodotAudio.WebChannel.CHANNEL_L,240GodotAudio.WebChannel.CHANNEL_SR241);242this._channelSplitter243.connect(this._c, GodotAudio.WebChannel.CHANNEL_C)244.connect(245this._channelMerger,246GodotAudio.WebChannel.CHANNEL_L,247GodotAudio.WebChannel.CHANNEL_C248);249this._channelSplitter250.connect(this._lfe, GodotAudio.WebChannel.CHANNEL_L)251.connect(252this._channelMerger,253GodotAudio.WebChannel.CHANNEL_L,254GodotAudio.WebChannel.CHANNEL_LFE255);256257this._channelMerger.connect(this._bus.getInputNode());258}259260/**261* Returns the input node.262* @returns {AudioNode}263*/264getInputNode() {265return this._channelSplitter;266}267268/**269* Returns the output node.270* @returns {AudioNode}271*/272getOutputNode() {273return this._channelMerger;274}275276/**277* Sets the volume for each (split) channel.278* @param {Float32Array} volume Volume array from the engine for each channel.279* @returns {void}280*/281setVolume(volume) {282if (volume.length !== GodotAudio.MAX_VOLUME_CHANNELS) {283throw new Error(284`Volume length isn't "${GodotAudio.MAX_VOLUME_CHANNELS}", is ${volume.length} instead`285);286}287this._l.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_L] ?? 0;288this._r.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_R] ?? 0;289this._sl.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_SL] ?? 0;290this._sr.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_SR] ?? 0;291this._c.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_C] ?? 0;292this._lfe.gain.value = volume[GodotAudio.GodotChannel.CHANNEL_LFE] ?? 0;293}294295/**296* Clears the current `SampleNodeBus` instance.297* @returns {void}298*/299clear() {300this._bus = null;301this._channelSplitter.disconnect();302this._channelSplitter = null;303this._l.disconnect();304this._l = null;305this._r.disconnect();306this._r = null;307this._sl.disconnect();308this._sl = null;309this._sr.disconnect();310this._sr = null;311this._c.disconnect();312this._c = null;313this._lfe.disconnect();314this._lfe = null;315this._channelMerger.disconnect();316this._channelMerger = null;317}318}319320/**321* @typedef {{322* id: string323* streamObjectId: string324* busIndex: number325* }} SampleNodeParams326* @typedef {{327* offset?: number328* playbackRate?: number329* startTime?: number330* pitchScale?: number331* loopMode?: LoopMode332* volume?: Float32Array333* start?: boolean334* }} SampleNodeOptions335*/336337/**338* Represents an `AudioNode` of a `Sample`.339* @class340*/341class SampleNode {342/**343* Returns a `SampleNode`.344* @param {string} id Id of the `SampleNode`.345* @returns {SampleNode}346* @throws {ReferenceError} When no `SampleNode` is not found347*/348static getSampleNode(id) {349if (!GodotAudio.sampleNodes.has(id)) {350throw new ReferenceError(`Could not find sample node "${id}"`);351}352return GodotAudio.sampleNodes.get(id);353}354355/**356* Returns a `SampleNode`, returns null if not found.357* @param {string} id Id of the SampleNode.358* @returns {SampleNode?}359*/360static getSampleNodeOrNull(id) {361return GodotAudio.sampleNodes.get(id) ?? null;362}363364/**365* Stops a `SampleNode` by id.366* @param {string} id Id of the `SampleNode` to stop.367* @returns {void}368*/369static stopSampleNode(id) {370const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(id);371if (sampleNode == null) {372return;373}374sampleNode.stop();375}376377/**378* Pauses the `SampleNode` by id.379* @param {string} id Id of the `SampleNode` to pause.380* @param {boolean} enable State of the pause381* @returns {void}382*/383static pauseSampleNode(id, enable) {384const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(id);385if (sampleNode == null) {386return;387}388sampleNode.pause(enable);389}390391/**392* Creates a `SampleNode` based on the params. Will register the `SampleNode` to393* the `GodotAudio.sampleNodes` regisery.394* @param {SampleNodeParams} params Base params.395* @param {SampleNodeOptions | undefined} options Optional params.396* @returns {SampleNode}397*/398static create(params, options = {}) {399const sampleNode = new GodotAudio.SampleNode(params, options);400GodotAudio.sampleNodes.set(params.id, sampleNode);401return sampleNode;402}403404/**405* Deletes a `SampleNode` based on the id.406* @param {string} id Id of the `SampleNode` to delete.407* @returns {void}408*/409static delete(id) {410GodotAudio.deleteSampleNode(id);411}412413/**414* @param {SampleNodeParams} params Base params415* @param {SampleNodeOptions | undefined} options Optional params.416*/417constructor(params, options = {}) {418/** @type {string} */419this.id = params.id;420/** @type {string} */421this.streamObjectId = params.streamObjectId;422/** @type {number} */423this.offset = options.offset ?? 0;424/** @type {number} */425this._playbackPosition = options.offset;426/** @type {number} */427this.startTime = options.startTime ?? 0;428/** @type {boolean} */429this.isPaused = false;430/** @type {boolean} */431this.isStarted = false;432/** @type {boolean} */433this.isCanceled = false;434/** @type {number} */435this.pauseTime = 0;436/** @type {number} */437this._playbackRate = 44100;438/** @type {LoopMode} */439this.loopMode = options.loopMode ?? this.getSample().loopMode ?? 'disabled';440/** @type {number} */441this._pitchScale = options.pitchScale ?? 1;442/** @type {number} */443this._sourceStartTime = 0;444/** @type {Map<Bus, SampleNodeBus>} */445this._sampleNodeBuses = new Map();446/** @type {AudioBufferSourceNode | null} */447this._source = GodotAudio.ctx.createBufferSource();448449this._onended = null;450/** @type {AudioWorkletNode | null} */451this._positionWorklet = null;452453this.setPlaybackRate(options.playbackRate ?? 44100);454this._source.buffer = this.getSample().getAudioBuffer();455456this._addEndedListener();457458const bus = GodotAudio.Bus.getBus(params.busIndex);459const sampleNodeBus = this.getSampleNodeBus(bus);460sampleNodeBus.setVolume(options.volume);461462this.connectPositionWorklet(options.start).catch((err) => {463const newErr = new Error('Failed to create PositionWorklet.');464newErr.cause = err;465GodotRuntime.error(newErr);466});467}468469/**470* Gets the playback rate.471* @returns {number}472*/473getPlaybackRate() {474return this._playbackRate;475}476477/**478* Gets the playback position.479* @returns {number}480*/481getPlaybackPosition() {482return this._playbackPosition;483}484485/**486* Sets the playback rate.487* @param {number} val Value to set.488* @returns {void}489*/490setPlaybackRate(val) {491this._playbackRate = val;492this._syncPlaybackRate();493}494495/**496* Gets the pitch scale.497* @returns {number}498*/499getPitchScale() {500return this._pitchScale;501}502503/**504* Sets the pitch scale.505* @param {number} val Value to set.506* @returns {void}507*/508setPitchScale(val) {509this._pitchScale = val;510this._syncPlaybackRate();511}512513/**514* Returns the linked `Sample`.515* @returns {Sample}516*/517getSample() {518return GodotAudio.Sample.getSample(this.streamObjectId);519}520521/**522* Returns the output node.523* @returns {AudioNode}524*/525getOutputNode() {526return this._source;527}528529/**530* Starts the `SampleNode`.531* @returns {void}532*/533start() {534if (this.isStarted) {535return;536}537this._resetSourceStartTime();538this._source.start(this.startTime, this.offset);539this.isStarted = true;540}541542/**543* Stops the `SampleNode`.544* @returns {void}545*/546stop() {547this.clear();548}549550/**551* Restarts the `SampleNode`.552*/553restart() {554this.isPaused = false;555this.pauseTime = 0;556this._resetSourceStartTime();557this._restart();558}559560/**561* Pauses the `SampleNode`.562* @param {boolean} [enable=true] State of the pause.563* @returns {void}564*/565pause(enable = true) {566if (enable) {567this._pause();568return;569}570571this._unpause();572}573574/**575* Connects an AudioNode to the output node of this `SampleNode`.576* @param {AudioNode} node AudioNode to connect.577* @returns {void}578*/579connect(node) {580return this.getOutputNode().connect(node);581}582583/**584* Sets the volumes of the `SampleNode` for each buses passed in parameters.585* @param {Array<Bus>} buses586* @param {Float32Array} volumes587*/588setVolumes(buses, volumes) {589for (let busIdx = 0; busIdx < buses.length; busIdx++) {590const sampleNodeBus = this.getSampleNodeBus(buses[busIdx]);591sampleNodeBus.setVolume(592volumes.slice(593busIdx * GodotAudio.MAX_VOLUME_CHANNELS,594(busIdx * GodotAudio.MAX_VOLUME_CHANNELS) + GodotAudio.MAX_VOLUME_CHANNELS595)596);597}598}599600/**601* Returns the SampleNodeBus based on the bus in parameters.602* @param {Bus} bus Bus to get the SampleNodeBus from.603* @returns {SampleNodeBus}604*/605getSampleNodeBus(bus) {606if (!this._sampleNodeBuses.has(bus)) {607const sampleNodeBus = GodotAudio.SampleNodeBus.create(bus);608this._sampleNodeBuses.set(bus, sampleNodeBus);609this._source.connect(sampleNodeBus.getInputNode());610}611return this._sampleNodeBuses.get(bus);612}613614/**615* Sets up and connects the source to the GodotPositionReportingProcessor616* If the worklet module is not loaded in, it will be added617*/618async connectPositionWorklet(start) {619await GodotAudio.audioPositionWorkletPromise;620if (this.isCanceled) {621return;622}623this._source.connect(this.getPositionWorklet());624if (start) {625this.start();626}627}628629/**630* Get a AudioWorkletProcessor631* @returns {AudioWorkletNode}632*/633getPositionWorklet() {634if (this._positionWorklet != null) {635return this._positionWorklet;636}637if (GodotAudio.audioPositionWorkletNodes.length > 0) {638this._positionWorklet = GodotAudio.audioPositionWorkletNodes.pop();639} else {640this._positionWorklet = new AudioWorkletNode(641GodotAudio.ctx,642'godot-position-reporting-processor'643);644}645this._playbackPosition = this.offset;646this._positionWorklet.port.onmessage = (event) => {647switch (event.data['type']) {648case 'position':649this._playbackPosition = (parseInt(event.data.data, 10) / this.getSample().sampleRate) + this.offset;650break;651default:652// Do nothing.653}654};655656const resetParameter = this._positionWorklet.parameters.get('reset');657resetParameter.setValueAtTime(1, GodotAudio.ctx.currentTime);658resetParameter.setValueAtTime(0, GodotAudio.ctx.currentTime + 1);659660return this._positionWorklet;661}662663/**664* Clears the `SampleNode`.665* @returns {void}666*/667clear() {668this.isCanceled = true;669this.isPaused = false;670this.pauseTime = 0;671672if (this._source != null) {673this._source.removeEventListener('ended', this._onended);674this._onended = null;675if (this.isStarted) {676this._source.stop();677}678this._source.disconnect();679this._source = null;680}681682for (const sampleNodeBus of this._sampleNodeBuses.values()) {683sampleNodeBus.clear();684}685this._sampleNodeBuses.clear();686687if (this._positionWorklet) {688this._positionWorklet.disconnect();689this._positionWorklet.port.onmessage = null;690GodotAudio.audioPositionWorkletNodes.push(this._positionWorklet);691this._positionWorklet = null;692}693694GodotAudio.SampleNode.delete(this.id);695}696697/**698* Resets the source start time699* @returns {void}700*/701_resetSourceStartTime() {702this._sourceStartTime = GodotAudio.ctx.currentTime;703}704705/**706* Syncs the `AudioNode` playback rate based on the `SampleNode` playback rate and pitch scale.707* @returns {void}708*/709_syncPlaybackRate() {710this._source.playbackRate.value = this.getPlaybackRate() * this.getPitchScale();711}712713/**714* Restarts the `SampleNode`.715* Honors `isPaused` and `pauseTime`.716* @returns {void}717*/718_restart() {719if (this._source != null) {720this._source.disconnect();721}722this._source = GodotAudio.ctx.createBufferSource();723this._source.buffer = this.getSample().getAudioBuffer();724725// Make sure that we connect the new source to the sample node bus.726for (const sampleNodeBus of this._sampleNodeBuses.values()) {727this.connect(sampleNodeBus.getInputNode());728}729730this._addEndedListener();731const pauseTime = this.isPaused732? this.pauseTime733: 0;734if (this._positionWorklet != null) {735this._positionWorklet.port.postMessage({ type: 'clear' });736this._source.connect(this._positionWorklet);737}738this._source.start(this.startTime, this.offset + pauseTime);739this.isStarted = true;740}741742/**743* Pauses the `SampleNode`.744* @returns {void}745*/746_pause() {747if (!this.isStarted) {748return;749}750this.isPaused = true;751this.pauseTime = (GodotAudio.ctx.currentTime - this._sourceStartTime) / this.getPlaybackRate();752this._source.stop();753}754755/**756* Unpauses the `SampleNode`.757* @returns {void}758*/759_unpause() {760this._restart();761this.isPaused = false;762this.pauseTime = 0;763}764765/**766* Adds an "ended" listener to the source node to repeat it if necessary.767* @returns {void}768*/769_addEndedListener() {770if (this._onended != null) {771this._source.removeEventListener('ended', this._onended);772}773774/** @type {SampleNode} */775// eslint-disable-next-line consistent-this776const self = this;777this._onended = (_) => {778if (self.isPaused) {779return;780}781782switch (self.getSample().loopMode) {783case 'disabled':784self.stop();785break;786case 'forward':787case 'backward':788self.restart();789break;790default:791// do nothing792}793};794this._source.addEventListener('ended', this._onended);795}796}797798/**799* Collection of nodes to represents a Godot Engine audio bus.800* @class801*/802class Bus {803/**804* Returns the number of registered buses.805* @returns {number}806*/807static getCount() {808return GodotAudio.buses.length;809}810811/**812* Sets the number of registered buses.813* Will delete buses if lower than the current number.814* @param {number} val Count of registered buses.815* @returns {void}816*/817static setCount(val) {818const buses = GodotAudio.buses;819if (val === buses.length) {820return;821}822823if (val < buses.length) {824// TODO: what to do with nodes connected to the deleted buses?825const deletedBuses = buses.slice(val);826for (let i = 0; i < deletedBuses.length; i++) {827const deletedBus = deletedBuses[i];828deletedBus.clear();829}830GodotAudio.buses = buses.slice(0, val);831return;832}833834for (let i = GodotAudio.buses.length; i < val; i++) {835GodotAudio.Bus.create();836}837}838839/**840* Returns a `Bus` based on it's index number.841* @param {number} index842* @returns {Bus}843* @throws {ReferenceError} If the index value is outside the registry.844*/845static getBus(index) {846if (index < 0 || index >= GodotAudio.buses.length) {847throw new ReferenceError(`invalid bus index "${index}"`);848}849return GodotAudio.buses[index];850}851852/**853* Returns a `Bus` based on it's index number. Returns null if it doesn't exist.854* @param {number} index855* @returns {Bus?}856*/857static getBusOrNull(index) {858if (index < 0 || index >= GodotAudio.buses.length) {859return null;860}861return GodotAudio.buses[index];862}863864/**865* Move a bus from an index to another.866* @param {number} fromIndex From index867* @param {number} toIndex To index868* @returns {void}869*/870static move(fromIndex, toIndex) {871const movedBus = GodotAudio.Bus.getBusOrNull(fromIndex);872if (movedBus == null) {873return;874}875const buses = GodotAudio.buses.filter((_, i) => i !== fromIndex);876// Inserts at index.877buses.splice(toIndex - 1, 0, movedBus);878GodotAudio.buses = buses;879}880881/**882* Adds a new bus at the specified index.883* @param {number} index Index to add a new bus.884* @returns {void}885*/886static addAt(index) {887const newBus = GodotAudio.Bus.create();888if (index !== newBus.getId()) {889GodotAudio.Bus.move(newBus.getId(), index);890}891}892893/**894* Creates a `Bus` and registers it.895* @returns {Bus}896*/897static create() {898const newBus = new GodotAudio.Bus();899const isFirstBus = GodotAudio.buses.length === 0;900GodotAudio.buses.push(newBus);901if (isFirstBus) {902newBus.setSend(null);903} else {904newBus.setSend(GodotAudio.Bus.getBus(0));905}906return newBus;907}908909/**910* `Bus` constructor.911*/912constructor() {913/** @type {Set<SampleNode>} */914this._sampleNodes = new Set();915/** @type {boolean} */916this.isSolo = false;917/** @type {Bus?} */918this._send = null;919920/** @type {GainNode} */921this._gainNode = GodotAudio.ctx.createGain();922/** @type {GainNode} */923this._soloNode = GodotAudio.ctx.createGain();924/** @type {GainNode} */925this._muteNode = GodotAudio.ctx.createGain();926927this._gainNode928.connect(this._soloNode)929.connect(this._muteNode);930}931932/**933* Returns the current id of the bus (its index).934* @returns {number}935*/936getId() {937return GodotAudio.buses.indexOf(this);938}939940/**941* Returns the bus volume db value.942* @returns {number}943*/944getVolumeDb() {945return GodotAudio.linear_to_db(this._gainNode.gain.value);946}947948/**949* Sets the bus volume db value.950* @param {number} val Value to set951* @returns {void}952*/953setVolumeDb(val) {954const linear = GodotAudio.db_to_linear(val);955if (isFinite(linear)) {956this._gainNode.gain.value = linear;957}958}959960/**961* Returns the "send" bus.962* If null, this bus sends its contents directly to the output.963* If not null, this bus sends its contents to another bus.964* @returns {Bus?}965*/966getSend() {967return this._send;968}969970/**971* Sets the "send" bus.972* If null, this bus sends its contents directly to the output.973* If not null, this bus sends its contents to another bus.974*975* **Note:** if null, `getId()` must be equal to 0. Otherwise, it will throw.976* @param {Bus?} val977* @returns {void}978* @throws {Error} When val is `null` and `getId()` isn't equal to 0979*/980setSend(val) {981this._send = val;982if (val == null) {983if (this.getId() == 0) {984this.getOutputNode().connect(GodotAudio.ctx.destination);985return;986}987throw new Error(988`Cannot send to "${val}" without the bus being at index 0 (current index: ${this.getId()})`989);990}991this.connect(val);992}993994/**995* Returns the input node of the bus.996* @returns {AudioNode}997*/998getInputNode() {999return this._gainNode;1000}10011002/**1003* Returns the output node of the bus.1004* @returns {AudioNode}1005*/1006getOutputNode() {1007return this._muteNode;1008}10091010/**1011* Sets the mute status of the bus.1012* @param {boolean} enable1013*/1014mute(enable) {1015this._muteNode.gain.value = enable ? 0 : 1;1016}10171018/**1019* Sets the solo status of the bus.1020* @param {boolean} enable1021*/1022solo(enable) {1023if (this.isSolo === enable) {1024return;1025}10261027if (enable) {1028if (GodotAudio.busSolo != null && GodotAudio.busSolo !== this) {1029GodotAudio.busSolo._disableSolo();1030}1031this._enableSolo();1032return;1033}10341035this._disableSolo();1036}10371038/**1039* Wrapper to simply add a sample node to the bus.1040* @param {SampleNode} sampleNode `SampleNode` to remove1041* @returns {void}1042*/1043addSampleNode(sampleNode) {1044this._sampleNodes.add(sampleNode);1045sampleNode.getOutputNode().connect(this.getInputNode());1046}10471048/**1049* Wrapper to simply remove a sample node from the bus.1050* @param {SampleNode} sampleNode `SampleNode` to remove1051* @returns {void}1052*/1053removeSampleNode(sampleNode) {1054this._sampleNodes.delete(sampleNode);1055sampleNode.getOutputNode().disconnect();1056}10571058/**1059* Wrapper to simply connect to another bus.1060* @param {Bus} bus1061* @returns {void}1062*/1063connect(bus) {1064if (bus == null) {1065throw new Error('cannot connect to null bus');1066}1067this.getOutputNode().disconnect();1068this.getOutputNode().connect(bus.getInputNode());1069return bus;1070}10711072/**1073* Clears the current bus.1074* @returns {void}1075*/1076clear() {1077GodotAudio.buses = GodotAudio.buses.filter((v) => v !== this);1078}10791080_syncSampleNodes() {1081const sampleNodes = Array.from(this._sampleNodes);1082for (let i = 0; i < sampleNodes.length; i++) {1083const sampleNode = sampleNodes[i];1084sampleNode.getOutputNode().disconnect();1085sampleNode.getOutputNode().connect(this.getInputNode());1086}1087}10881089/**1090* Process to enable solo.1091* @returns {void}1092*/1093_enableSolo() {1094this.isSolo = true;1095GodotAudio.busSolo = this;1096this._soloNode.gain.value = 1;1097const otherBuses = GodotAudio.buses.filter(1098(otherBus) => otherBus !== this1099);1100for (let i = 0; i < otherBuses.length; i++) {1101const otherBus = otherBuses[i];1102otherBus._soloNode.gain.value = 0;1103}1104}11051106/**1107* Process to disable solo.1108* @returns {void}1109*/1110_disableSolo() {1111this.isSolo = false;1112GodotAudio.busSolo = null;1113this._soloNode.gain.value = 1;1114const otherBuses = GodotAudio.buses.filter(1115(otherBus) => otherBus !== this1116);1117for (let i = 0; i < otherBuses.length; i++) {1118const otherBus = otherBuses[i];1119otherBus._soloNode.gain.value = 1;1120}1121}1122}11231124const _GodotAudio = {1125$GodotAudio__deps: ['$GodotRuntime', '$GodotOS'],1126$GodotAudio: {1127/**1128* Max number of volume channels.1129*/1130MAX_VOLUME_CHANNELS: 8,11311132/**1133* Represents the index of each sound channel relative to the engine.1134*/1135GodotChannel: Object.freeze({1136CHANNEL_L: 0,1137CHANNEL_R: 1,1138CHANNEL_C: 3,1139CHANNEL_LFE: 4,1140CHANNEL_RL: 5,1141CHANNEL_RR: 6,1142CHANNEL_SL: 7,1143CHANNEL_SR: 8,1144}),11451146/**1147* Represents the index of each sound channel relative to the Web Audio API.1148*/1149WebChannel: Object.freeze({1150CHANNEL_L: 0,1151CHANNEL_R: 1,1152CHANNEL_SL: 2,1153CHANNEL_SR: 3,1154CHANNEL_C: 4,1155CHANNEL_LFE: 5,1156}),11571158// `Sample` class1159/**1160* Registry of `Sample`s.1161* @type {Map<string, Sample>}1162*/1163samples: null,1164Sample,11651166// `SampleNodeBus` class1167SampleNodeBus,11681169// `SampleNode` class1170/**1171* Registry of `SampleNode`s.1172* @type {Map<string, SampleNode>}1173*/1174sampleNodes: null,1175SampleNode,1176deleteSampleNode: (pSampleNodeId) => {1177GodotAudio.sampleNodes.delete(pSampleNodeId);1178if (GodotAudio.sampleFinishedCallback == null) {1179return;1180}1181const sampleNodeIdPtr = GodotRuntime.allocString(pSampleNodeId);1182GodotAudio.sampleFinishedCallback(sampleNodeIdPtr);1183GodotRuntime.free(sampleNodeIdPtr);1184},11851186// `Bus` class1187/**1188* Registry of `Bus`es.1189* @type {Array<Bus>}1190*/1191buses: null,1192/**1193* Reference to the current bus in solo mode.1194* @type {Bus | null}1195*/1196busSolo: null,1197Bus,11981199/**1200* Callback to signal that a sample has finished.1201* @type {(playbackObjectIdPtr: number) => void | null}1202*/1203sampleFinishedCallback: null,12041205/** @type {AudioContext} */1206ctx: null,1207input: null,1208driver: null,1209interval: 0,12101211/** @type {Promise} */1212audioPositionWorkletPromise: null,1213/** @type {Array<AudioWorkletNode>} */1214audioPositionWorkletNodes: null,12151216/**1217* Converts linear volume to Db.1218* @param {number} linear Linear value to convert.1219* @returns {number}1220*/1221linear_to_db: function (linear) {1222// eslint-disable-next-line no-loss-of-precision1223return Math.log(linear) * 8.6858896380650365530225783783321;1224},1225/**1226* Converts Db volume to linear.1227* @param {number} db Db value to convert.1228* @returns {number}1229*/1230db_to_linear: function (db) {1231// eslint-disable-next-line no-loss-of-precision1232return Math.exp(db * 0.11512925464970228420089957273422);1233},12341235init: function (mix_rate, latency, onstatechange, onlatencyupdate) {1236// Initialize classes static values.1237GodotAudio.samples = new Map();1238GodotAudio.sampleNodes = new Map();1239GodotAudio.buses = [];1240GodotAudio.busSolo = null;1241GodotAudio.audioPositionWorkletNodes = [];12421243const opts = {};1244// If mix_rate is 0, let the browser choose.1245if (mix_rate) {1246GodotAudio.sampleRate = mix_rate;1247opts['sampleRate'] = mix_rate;1248}1249// Do not specify, leave 'interactive' for good performance.1250// opts['latencyHint'] = latency / 1000;1251const ctx = new (window.AudioContext || window.webkitAudioContext)(opts);1252GodotAudio.ctx = ctx;1253ctx.onstatechange = function () {1254let state = 0;1255switch (ctx.state) {1256case 'suspended':1257state = 0;1258break;1259case 'running':1260state = 1;1261break;1262case 'closed':1263state = 2;1264break;1265default:1266// Do nothing.1267}1268onstatechange(state);1269};1270ctx.onstatechange(); // Immediately notify state.1271// Update computed latency1272GodotAudio.interval = setInterval(function () {1273let computed_latency = 0;1274if (ctx.baseLatency) {1275computed_latency += GodotAudio.ctx.baseLatency;1276}1277if (ctx.outputLatency) {1278computed_latency += GodotAudio.ctx.outputLatency;1279}1280onlatencyupdate(computed_latency);1281}, 1000);1282GodotOS.atexit(GodotAudio.close_async);12831284const path = GodotConfig.locate_file('godot.audio.position.worklet.js');1285GodotAudio.audioPositionWorkletPromise = ctx.audioWorklet.addModule(path);12861287return ctx.destination.channelCount;1288},12891290create_input: function (callback) {1291if (GodotAudio.input) {1292return 0; // Already started.1293}1294function gotMediaInput(stream) {1295try {1296GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream);1297callback(GodotAudio.input);1298} catch (e) {1299GodotRuntime.error('Failed creating input.', e);1300}1301}1302if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {1303navigator.mediaDevices.getUserMedia({1304'audio': true,1305}).then(gotMediaInput, function (e) {1306GodotRuntime.error('Error getting user media.', e);1307});1308} else {1309if (!navigator.getUserMedia) {1310navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;1311}1312if (!navigator.getUserMedia) {1313GodotRuntime.error('getUserMedia not available.');1314return 1;1315}1316navigator.getUserMedia({1317'audio': true,1318}, gotMediaInput, function (e) {1319GodotRuntime.print(e);1320});1321}1322return 0;1323},13241325close_async: function (resolve, reject) {1326const ctx = GodotAudio.ctx;1327GodotAudio.ctx = null;1328// Audio was not initialized.1329if (!ctx) {1330resolve();1331return;1332}1333// Remove latency callback1334if (GodotAudio.interval) {1335clearInterval(GodotAudio.interval);1336GodotAudio.interval = 0;1337}1338// Disconnect input, if it was started.1339if (GodotAudio.input) {1340GodotAudio.input.disconnect();1341GodotAudio.input = null;1342}1343// Disconnect output1344let closed = Promise.resolve();1345if (GodotAudio.driver) {1346closed = GodotAudio.driver.close();1347}1348closed.then(function () {1349return ctx.close();1350}).then(function () {1351ctx.onstatechange = null;1352resolve();1353}).catch(function (e) {1354ctx.onstatechange = null;1355GodotRuntime.error('Error closing AudioContext', e);1356resolve();1357});1358},13591360/**1361* Triggered when a sample node needs to start.1362* @param {string} playbackObjectId The unique id of the sample playback1363* @param {string} streamObjectId The unique id of the stream1364* @param {number} busIndex Index of the bus currently binded to the sample playback1365* @param {SampleNodeOptions | undefined} startOptions Optional params.1366* @returns {void}1367*/1368start_sample: function (1369playbackObjectId,1370streamObjectId,1371busIndex,1372startOptions1373) {1374GodotAudio.SampleNode.stopSampleNode(playbackObjectId);1375GodotAudio.SampleNode.create(1376{1377busIndex,1378id: playbackObjectId,1379streamObjectId,1380},1381startOptions1382);1383},13841385/**1386* Triggered when a sample node needs to be stopped.1387* @param {string} playbackObjectId Id of the sample playback1388* @returns {void}1389*/1390stop_sample: function (playbackObjectId) {1391GodotAudio.SampleNode.stopSampleNode(playbackObjectId);1392},13931394/**1395* Triggered when a sample node needs to be paused or unpaused.1396* @param {string} playbackObjectId Id of the sample playback1397* @param {boolean} pause State of the pause1398* @returns {void}1399*/1400sample_set_pause: function (playbackObjectId, pause) {1401GodotAudio.SampleNode.pauseSampleNode(playbackObjectId, pause);1402},14031404/**1405* Triggered when a sample node needs its pitch scale to be updated.1406* @param {string} playbackObjectId Id of the sample playback1407* @param {number} pitchScale Pitch scale of the sample playback1408* @returns {void}1409*/1410update_sample_pitch_scale: function (playbackObjectId, pitchScale) {1411const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(playbackObjectId);1412if (sampleNode == null) {1413return;1414}1415sampleNode.setPitchScale(pitchScale);1416},14171418/**1419* Triggered when a sample node volumes need to be updated.1420* @param {string} playbackObjectId Id of the sample playback1421* @param {Array<number>} busIndexes Indexes of the buses that need to be updated1422* @param {Float32Array} volumes Array of the volumes1423* @returns {void}1424*/1425sample_set_volumes_linear: function (playbackObjectId, busIndexes, volumes) {1426const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(playbackObjectId);1427if (sampleNode == null) {1428return;1429}1430const buses = busIndexes.map((busIndex) => GodotAudio.Bus.getBus(busIndex));1431sampleNode.setVolumes(buses, volumes);1432},14331434/**1435* Triggered when the bus count changes.1436* @param {number} count Number of buses1437* @returns {void}1438*/1439set_sample_bus_count: function (count) {1440GodotAudio.Bus.setCount(count);1441},14421443/**1444* Triggered when a bus needs to be removed.1445* @param {number} index Bus index1446* @returns {void}1447*/1448remove_sample_bus: function (index) {1449const bus = GodotAudio.Bus.getBusOrNull(index);1450if (bus == null) {1451return;1452}1453bus.clear();1454},14551456/**1457* Triggered when a bus needs to be at the desired position.1458* @param {number} atPos Position to add the bus1459* @returns {void}1460*/1461add_sample_bus: function (atPos) {1462GodotAudio.Bus.addAt(atPos);1463},14641465/**1466* Triggered when a bus needs to be moved.1467* @param {number} busIndex Index of the bus to move1468* @param {number} toPos Index of the new position of the bus1469* @returns {void}1470*/1471move_sample_bus: function (busIndex, toPos) {1472GodotAudio.Bus.move(busIndex, toPos);1473},14741475/**1476* Triggered when the "send" value of a bus changes.1477* @param {number} busIndex Index of the bus to update the "send" value1478* @param {number} sendIndex Index of the bus that is the new "send"1479* @returns {void}1480*/1481set_sample_bus_send: function (busIndex, sendIndex) {1482const bus = GodotAudio.Bus.getBusOrNull(busIndex);1483if (bus == null) {1484// Cannot send from an invalid bus.1485return;1486}1487let targetBus = GodotAudio.Bus.getBusOrNull(sendIndex);1488if (targetBus == null) {1489// Send to master.1490targetBus = GodotAudio.Bus.getBus(0);1491}1492bus.setSend(targetBus);1493},14941495/**1496* Triggered when a bus needs its volume db to be updated.1497* @param {number} busIndex Index of the bus to update its volume db1498* @param {number} volumeDb Volume of the bus1499* @returns {void}1500*/1501set_sample_bus_volume_db: function (busIndex, volumeDb) {1502const bus = GodotAudio.Bus.getBusOrNull(busIndex);1503if (bus == null) {1504return;1505}1506bus.setVolumeDb(volumeDb);1507},15081509/**1510* Triggered when a bus needs to update its solo status1511* @param {number} busIndex Index of the bus to update its solo status1512* @param {boolean} enable Status of the solo1513* @returns {void}1514*/1515set_sample_bus_solo: function (busIndex, enable) {1516const bus = GodotAudio.Bus.getBusOrNull(busIndex);1517if (bus == null) {1518return;1519}1520bus.solo(enable);1521},15221523/**1524* Triggered when a bus needs to update its mute status1525* @param {number} busIndex Index of the bus to update its mute status1526* @param {boolean} enable Status of the mute1527* @returns {void}1528*/1529set_sample_bus_mute: function (busIndex, enable) {1530const bus = GodotAudio.Bus.getBusOrNull(busIndex);1531if (bus == null) {1532return;1533}1534bus.mute(enable);1535},1536},15371538godot_audio_is_available__sig: 'i',1539godot_audio_is_available__proxy: 'sync',1540godot_audio_is_available: function () {1541if (!(window.AudioContext || window.webkitAudioContext)) {1542return 0;1543}1544return 1;1545},15461547godot_audio_has_worklet__proxy: 'sync',1548godot_audio_has_worklet__sig: 'i',1549godot_audio_has_worklet: function () {1550return GodotAudio.ctx && GodotAudio.ctx.audioWorklet ? 1 : 0;1551},15521553godot_audio_has_script_processor__proxy: 'sync',1554godot_audio_has_script_processor__sig: 'i',1555godot_audio_has_script_processor: function () {1556return GodotAudio.ctx && GodotAudio.ctx.createScriptProcessor ? 1 : 0;1557},15581559godot_audio_init__proxy: 'sync',1560godot_audio_init__sig: 'iiiii',1561godot_audio_init: function (1562p_mix_rate,1563p_latency,1564p_state_change,1565p_latency_update1566) {1567const statechange = GodotRuntime.get_func(p_state_change);1568const latencyupdate = GodotRuntime.get_func(p_latency_update);1569const mix_rate = GodotRuntime.getHeapValue(p_mix_rate, 'i32');1570const channels = GodotAudio.init(1571mix_rate,1572p_latency,1573statechange,1574latencyupdate1575);1576GodotRuntime.setHeapValue(p_mix_rate, GodotAudio.ctx.sampleRate, 'i32');1577return channels;1578},15791580godot_audio_resume__proxy: 'sync',1581godot_audio_resume__sig: 'v',1582godot_audio_resume: function () {1583if (GodotAudio.ctx && GodotAudio.ctx.state !== 'running') {1584GodotAudio.ctx.resume();1585}1586},15871588godot_audio_input_start__proxy: 'sync',1589godot_audio_input_start__sig: 'i',1590godot_audio_input_start: function () {1591return GodotAudio.create_input(function (input) {1592input.connect(GodotAudio.driver.get_node());1593});1594},15951596godot_audio_input_stop__proxy: 'sync',1597godot_audio_input_stop__sig: 'v',1598godot_audio_input_stop: function () {1599if (GodotAudio.input) {1600const tracks = GodotAudio.input['mediaStream']['getTracks']();1601for (let i = 0; i < tracks.length; i++) {1602tracks[i]['stop']();1603}1604GodotAudio.input.disconnect();1605GodotAudio.input = null;1606}1607},16081609godot_audio_sample_stream_is_registered__proxy: 'sync',1610godot_audio_sample_stream_is_registered__sig: 'ii',1611/**1612* Returns if the sample stream is registered1613* @param {number} streamObjectIdStrPtr Pointer of the streamObjectId1614* @returns {number}1615*/1616godot_audio_sample_stream_is_registered: function (streamObjectIdStrPtr) {1617const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr);1618return Number(GodotAudio.Sample.getSampleOrNull(streamObjectId) != null);1619},16201621godot_audio_sample_register_stream__proxy: 'sync',1622godot_audio_sample_register_stream__sig: 'viiiiiii',1623/**1624* Registers a stream.1625* @param {number} streamObjectIdStrPtr StreamObjectId pointer1626* @param {number} framesPtr Frames pointer1627* @param {number} framesTotal Frames total value1628* @param {number} loopModeStrPtr Loop mode pointer1629* @param {number} loopBegin Loop begin value1630* @param {number} loopEnd Loop end value1631* @returns {void}1632*/1633godot_audio_sample_register_stream: function (1634streamObjectIdStrPtr,1635framesPtr,1636framesTotal,1637loopModeStrPtr,1638loopBegin,1639loopEnd1640) {1641const BYTES_PER_FLOAT32 = 4;1642const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr);1643const loopMode = GodotRuntime.parseString(loopModeStrPtr);1644const numberOfChannels = 2;1645const sampleRate = GodotAudio.ctx.sampleRate;16461647/** @type {Float32Array} */1648const subLeft = GodotRuntime.heapSub(HEAPF32, framesPtr, framesTotal);1649/** @type {Float32Array} */1650const subRight = GodotRuntime.heapSub(1651HEAPF32,1652framesPtr + framesTotal * BYTES_PER_FLOAT32,1653framesTotal1654);16551656const audioBuffer = GodotAudio.ctx.createBuffer(1657numberOfChannels,1658framesTotal,1659sampleRate1660);1661audioBuffer.copyToChannel(new Float32Array(subLeft), 0, 0);1662audioBuffer.copyToChannel(new Float32Array(subRight), 1, 0);16631664GodotAudio.Sample.create(1665{1666id: streamObjectId,1667audioBuffer,1668},1669{1670loopBegin,1671loopEnd,1672loopMode,1673numberOfChannels,1674sampleRate,1675}1676);1677},16781679godot_audio_sample_unregister_stream__proxy: 'sync',1680godot_audio_sample_unregister_stream__sig: 'vi',1681/**1682* Unregisters a stream.1683* @param {number} streamObjectIdStrPtr StreamObjectId pointer1684* @returns {void}1685*/1686godot_audio_sample_unregister_stream: function (streamObjectIdStrPtr) {1687const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr);1688const sample = GodotAudio.Sample.getSampleOrNull(streamObjectId);1689if (sample != null) {1690sample.clear();1691}1692},16931694godot_audio_sample_start__proxy: 'sync',1695godot_audio_sample_start__sig: 'viiiifi',1696/**1697* Starts a sample.1698* @param {number} playbackObjectIdStrPtr Playback object id pointer1699* @param {number} streamObjectIdStrPtr Stream object id pointer1700* @param {number} busIndex Bus index1701* @param {number} offset Sample offset1702* @param {number} pitchScale Pitch scale1703* @param {number} volumePtr Volume pointer1704* @returns {void}1705*/1706godot_audio_sample_start: function (1707playbackObjectIdStrPtr,1708streamObjectIdStrPtr,1709busIndex,1710offset,1711pitchScale,1712volumePtr1713) {1714/** @type {string} */1715const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr);1716/** @type {string} */1717const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr);1718/** @type {Float32Array} */1719const volume = GodotRuntime.heapSub(HEAPF32, volumePtr, 8);1720/** @type {SampleNodeOptions} */1721const startOptions = {1722offset,1723volume,1724playbackRate: 1,1725pitchScale,1726start: true,1727};1728GodotAudio.start_sample(1729playbackObjectId,1730streamObjectId,1731busIndex,1732startOptions1733);1734},17351736godot_audio_sample_stop__proxy: 'sync',1737godot_audio_sample_stop__sig: 'vi',1738/**1739* Stops a sample from playing.1740* @param {number} playbackObjectIdStrPtr Playback object id pointer1741* @returns {void}1742*/1743godot_audio_sample_stop: function (playbackObjectIdStrPtr) {1744const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr);1745GodotAudio.stop_sample(playbackObjectId);1746},17471748godot_audio_sample_set_pause__proxy: 'sync',1749godot_audio_sample_set_pause__sig: 'vii',1750/**1751* Sets the pause state of a sample.1752* @param {number} playbackObjectIdStrPtr Playback object id pointer1753* @param {number} pause Pause state1754*/1755godot_audio_sample_set_pause: function (playbackObjectIdStrPtr, pause) {1756const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr);1757GodotAudio.sample_set_pause(playbackObjectId, Boolean(pause));1758},17591760godot_audio_sample_is_active__proxy: 'sync',1761godot_audio_sample_is_active__sig: 'ii',1762/**1763* Returns if the sample is active.1764* @param {number} playbackObjectIdStrPtr Playback object id pointer1765* @returns {number}1766*/1767godot_audio_sample_is_active: function (playbackObjectIdStrPtr) {1768const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr);1769return Number(GodotAudio.sampleNodes.has(playbackObjectId));1770},17711772godot_audio_get_sample_playback_position__proxy: 'sync',1773godot_audio_get_sample_playback_position__sig: 'di',1774/**1775* Returns the position of the playback position.1776* @param {number} playbackObjectIdStrPtr Playback object id pointer1777* @returns {number}1778*/1779godot_audio_get_sample_playback_position: function (playbackObjectIdStrPtr) {1780const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr);1781const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(playbackObjectId);1782if (sampleNode == null) {1783return 0;1784}1785return sampleNode.getPlaybackPosition();1786},17871788godot_audio_sample_update_pitch_scale__proxy: 'sync',1789godot_audio_sample_update_pitch_scale__sig: 'vii',1790/**1791* Updates the pitch scale of a sample.1792* @param {number} playbackObjectIdStrPtr Playback object id pointer1793* @param {number} pitchScale Pitch scale value1794* @returns {void}1795*/1796godot_audio_sample_update_pitch_scale: function (1797playbackObjectIdStrPtr,1798pitchScale1799) {1800const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr);1801GodotAudio.update_sample_pitch_scale(playbackObjectId, pitchScale);1802},18031804godot_audio_sample_set_volumes_linear__proxy: 'sync',1805godot_audio_sample_set_volumes_linear__sig: 'vii',1806/**1807* Sets the volumes linear of each mentioned bus for the sample.1808* @param {number} playbackObjectIdStrPtr Playback object id pointer1809* @param {number} busesPtr Buses array pointer1810* @param {number} busesSize Buses array size1811* @param {number} volumesPtr Volumes array pointer1812* @param {number} volumesSize Volumes array size1813* @returns {void}1814*/1815godot_audio_sample_set_volumes_linear: function (1816playbackObjectIdStrPtr,1817busesPtr,1818busesSize,1819volumesPtr,1820volumesSize1821) {1822/** @type {string} */1823const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr);18241825/** @type {Uint32Array} */1826const buses = GodotRuntime.heapSub(HEAP32, busesPtr, busesSize);1827/** @type {Float32Array} */1828const volumes = GodotRuntime.heapSub(HEAPF32, volumesPtr, volumesSize);18291830GodotAudio.sample_set_volumes_linear(1831playbackObjectId,1832Array.from(buses),1833volumes1834);1835},18361837godot_audio_sample_bus_set_count__proxy: 'sync',1838godot_audio_sample_bus_set_count__sig: 'vi',1839/**1840* Sets the bus count.1841* @param {number} count Bus count1842* @returns {void}1843*/1844godot_audio_sample_bus_set_count: function (count) {1845GodotAudio.set_sample_bus_count(count);1846},18471848godot_audio_sample_bus_remove__proxy: 'sync',1849godot_audio_sample_bus_remove__sig: 'vi',1850/**1851* Removes a bus.1852* @param {number} index Index of the bus to remove1853* @returns {void}1854*/1855godot_audio_sample_bus_remove: function (index) {1856GodotAudio.remove_sample_bus(index);1857},18581859godot_audio_sample_bus_add__proxy: 'sync',1860godot_audio_sample_bus_add__sig: 'vi',1861/**1862* Adds a bus at the defined position.1863* @param {number} atPos Position to add the bus1864* @returns {void}1865*/1866godot_audio_sample_bus_add: function (atPos) {1867GodotAudio.add_sample_bus(atPos);1868},18691870godot_audio_sample_bus_move__proxy: 'sync',1871godot_audio_sample_bus_move__sig: 'vii',1872/**1873* Moves the bus from a position to another.1874* @param {number} fromPos Position of the bus to move1875* @param {number} toPos Final position of the bus1876* @returns {void}1877*/1878godot_audio_sample_bus_move: function (fromPos, toPos) {1879GodotAudio.move_sample_bus(fromPos, toPos);1880},18811882godot_audio_sample_bus_set_send__proxy: 'sync',1883godot_audio_sample_bus_set_send__sig: 'vii',1884/**1885* Sets the "send" of a bus.1886* @param {number} bus Position of the bus to set the send1887* @param {number} sendIndex Position of the "send" bus1888* @returns {void}1889*/1890godot_audio_sample_bus_set_send: function (bus, sendIndex) {1891GodotAudio.set_sample_bus_send(bus, sendIndex);1892},18931894godot_audio_sample_bus_set_volume_db__proxy: 'sync',1895godot_audio_sample_bus_set_volume_db__sig: 'vii',1896/**1897* Sets the volume db of a bus.1898* @param {number} bus Position of the bus to set the volume db1899* @param {number} volumeDb Volume db to set1900* @returns {void}1901*/1902godot_audio_sample_bus_set_volume_db: function (bus, volumeDb) {1903GodotAudio.set_sample_bus_volume_db(bus, volumeDb);1904},19051906godot_audio_sample_bus_set_solo__proxy: 'sync',1907godot_audio_sample_bus_set_solo__sig: 'vii',1908/**1909* Sets the state of solo for a bus1910* @param {number} bus Position of the bus to set the solo state1911* @param {number} enable State of the solo1912* @returns {void}1913*/1914godot_audio_sample_bus_set_solo: function (bus, enable) {1915GodotAudio.set_sample_bus_solo(bus, Boolean(enable));1916},19171918godot_audio_sample_bus_set_mute__proxy: 'sync',1919godot_audio_sample_bus_set_mute__sig: 'vii',1920/**1921* Sets the state of mute for a bus1922* @param {number} bus Position of the bus to set the mute state1923* @param {number} enable State of the mute1924* @returns {void}1925*/1926godot_audio_sample_bus_set_mute: function (bus, enable) {1927GodotAudio.set_sample_bus_mute(bus, Boolean(enable));1928},19291930godot_audio_sample_set_finished_callback__proxy: 'sync',1931godot_audio_sample_set_finished_callback__sig: 'vi',1932/**1933* Sets the finished callback1934* @param {Number} callbackPtr Finished callback pointer1935* @returns {void}1936*/1937godot_audio_sample_set_finished_callback: function (callbackPtr) {1938GodotAudio.sampleFinishedCallback = GodotRuntime.get_func(callbackPtr);1939},1940};19411942autoAddDeps(_GodotAudio, '$GodotAudio');1943mergeInto(LibraryManager.library, _GodotAudio);19441945/**1946* The AudioWorklet API driver, used when threads are available.1947*/1948const GodotAudioWorklet = {1949$GodotAudioWorklet__deps: ['$GodotAudio', '$GodotConfig'],1950$GodotAudioWorklet: {1951promise: null,1952worklet: null,1953ring_buffer: null,19541955create: function (channels) {1956const path = GodotConfig.locate_file('godot.audio.worklet.js');1957GodotAudioWorklet.promise = GodotAudio.ctx.audioWorklet1958.addModule(path)1959.then(function () {1960GodotAudioWorklet.worklet = new AudioWorkletNode(1961GodotAudio.ctx,1962'godot-processor',1963{1964outputChannelCount: [channels],1965}1966);1967return Promise.resolve();1968});1969GodotAudio.driver = GodotAudioWorklet;1970},19711972start: function (in_buf, out_buf, state) {1973GodotAudioWorklet.promise.then(function () {1974const node = GodotAudioWorklet.worklet;1975node.connect(GodotAudio.ctx.destination);1976node.port.postMessage({1977'cmd': 'start',1978'data': [state, in_buf, out_buf],1979});1980node.port.onmessage = function (event) {1981GodotRuntime.error(event.data);1982};1983});1984},19851986start_no_threads: function (1987p_out_buf,1988p_out_size,1989out_callback,1990p_in_buf,1991p_in_size,1992in_callback1993) {1994function RingBuffer() {1995let wpos = 0;1996let rpos = 0;1997let pending_samples = 0;1998const wbuf = new Float32Array(p_out_size);19992000function send(port) {2001if (pending_samples === 0) {2002return;2003}2004const buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size);2005const size = buffer.length;2006const tot_sent = pending_samples;2007out_callback(wpos, pending_samples);2008if (wpos + pending_samples >= size) {2009const high = size - wpos;2010wbuf.set(buffer.subarray(wpos, size));2011pending_samples -= high;2012wpos = 0;2013}2014if (pending_samples > 0) {2015wbuf.set(2016buffer.subarray(wpos, wpos + pending_samples),2017tot_sent - pending_samples2018);2019}2020port.postMessage({ 'cmd': 'chunk', 'data': wbuf.subarray(0, tot_sent) });2021wpos += pending_samples;2022pending_samples = 0;2023}2024this.receive = function (recv_buf) {2025const buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size);2026const from = rpos;2027let to_write = recv_buf.length;2028let high = 0;2029if (rpos + to_write >= p_in_size) {2030high = p_in_size - rpos;2031buffer.set(recv_buf.subarray(0, high), rpos);2032to_write -= high;2033rpos = 0;2034}2035if (to_write) {2036buffer.set(recv_buf.subarray(high, to_write), rpos);2037}2038in_callback(from, recv_buf.length);2039rpos += to_write;2040};2041this.consumed = function (size, port) {2042pending_samples += size;2043send(port);2044};2045}2046GodotAudioWorklet.ring_buffer = new RingBuffer();2047GodotAudioWorklet.promise.then(function () {2048const node = GodotAudioWorklet.worklet;2049const buffer = GodotRuntime.heapSlice(HEAPF32, p_out_buf, p_out_size);2050node.connect(GodotAudio.ctx.destination);2051node.port.postMessage({2052'cmd': 'start_nothreads',2053'data': [buffer, p_in_size],2054});2055node.port.onmessage = function (event) {2056if (!GodotAudioWorklet.worklet) {2057return;2058}2059if (event.data['cmd'] === 'read') {2060const read = event.data['data'];2061GodotAudioWorklet.ring_buffer.consumed(2062read,2063GodotAudioWorklet.worklet.port2064);2065} else if (event.data['cmd'] === 'input') {2066const buf = event.data['data'];2067if (buf.length > p_in_size) {2068GodotRuntime.error('Input chunk is too big');2069return;2070}2071GodotAudioWorklet.ring_buffer.receive(buf);2072} else {2073GodotRuntime.error(event.data);2074}2075};2076});2077},20782079get_node: function () {2080return GodotAudioWorklet.worklet;2081},20822083close: function () {2084return new Promise(function (resolve, reject) {2085if (GodotAudioWorklet.promise === null) {2086return;2087}2088const p = GodotAudioWorklet.promise;2089p.then(function () {2090GodotAudioWorklet.worklet.port.postMessage({2091'cmd': 'stop',2092'data': null,2093});2094GodotAudioWorklet.worklet.disconnect();2095GodotAudioWorklet.worklet.port.onmessage = null;2096GodotAudioWorklet.worklet = null;2097GodotAudioWorklet.promise = null;2098resolve();2099}).catch(function (err) {2100// Aborted?2101GodotRuntime.error(err);2102});2103});2104},2105},21062107godot_audio_worklet_create__proxy: 'sync',2108godot_audio_worklet_create__sig: 'ii',2109godot_audio_worklet_create: function (channels) {2110try {2111GodotAudioWorklet.create(channels);2112} catch (e) {2113GodotRuntime.error('Error starting AudioDriverWorklet', e);2114return 1;2115}2116return 0;2117},21182119godot_audio_worklet_start__proxy: 'sync',2120godot_audio_worklet_start__sig: 'viiiii',2121godot_audio_worklet_start: function (2122p_in_buf,2123p_in_size,2124p_out_buf,2125p_out_size,2126p_state2127) {2128const out_buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size);2129const in_buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size);2130const state = GodotRuntime.heapSub(HEAP32, p_state, 4);2131GodotAudioWorklet.start(in_buffer, out_buffer, state);2132},21332134godot_audio_worklet_start_no_threads__proxy: 'sync',2135godot_audio_worklet_start_no_threads__sig: 'viiiiii',2136godot_audio_worklet_start_no_threads: function (2137p_out_buf,2138p_out_size,2139p_out_callback,2140p_in_buf,2141p_in_size,2142p_in_callback2143) {2144const out_callback = GodotRuntime.get_func(p_out_callback);2145const in_callback = GodotRuntime.get_func(p_in_callback);2146GodotAudioWorklet.start_no_threads(2147p_out_buf,2148p_out_size,2149out_callback,2150p_in_buf,2151p_in_size,2152in_callback2153);2154},21552156godot_audio_worklet_state_wait__sig: 'iiii',2157godot_audio_worklet_state_wait: function (2158p_state,2159p_idx,2160p_expected,2161p_timeout2162) {2163Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout);2164return Atomics.load(HEAP32, (p_state >> 2) + p_idx);2165},21662167godot_audio_worklet_state_add__sig: 'iiii',2168godot_audio_worklet_state_add: function (p_state, p_idx, p_value) {2169return Atomics.add(HEAP32, (p_state >> 2) + p_idx, p_value);2170},21712172godot_audio_worklet_state_get__sig: 'iii',2173godot_audio_worklet_state_get: function (p_state, p_idx) {2174return Atomics.load(HEAP32, (p_state >> 2) + p_idx);2175},2176};21772178autoAddDeps(GodotAudioWorklet, '$GodotAudioWorklet');2179mergeInto(LibraryManager.library, GodotAudioWorklet);21802181/*2182* The ScriptProcessorNode API, used as a fallback if AudioWorklet is not available.2183*/2184const GodotAudioScript = {2185$GodotAudioScript__deps: ['$GodotAudio'],2186$GodotAudioScript: {2187script: null,21882189create: function (buffer_length, channel_count) {2190GodotAudioScript.script = GodotAudio.ctx.createScriptProcessor(2191buffer_length,21922,2193channel_count2194);2195GodotAudio.driver = GodotAudioScript;2196return GodotAudioScript.script.bufferSize;2197},21982199start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess) {2200GodotAudioScript.script.onaudioprocess = function (event) {2201// Read input2202const inb = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size);2203const input = event.inputBuffer;2204if (GodotAudio.input) {2205const inlen = input.getChannelData(0).length;2206for (let ch = 0; ch < 2; ch++) {2207const data = input.getChannelData(ch);2208for (let s = 0; s < inlen; s++) {2209inb[s * 2 + ch] = data[s];2210}2211}2212}22132214// Let Godot process the input/output.2215onprocess();22162217// Write the output.2218const outb = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size);2219const output = event.outputBuffer;2220const channels = output.numberOfChannels;2221for (let ch = 0; ch < channels; ch++) {2222const data = output.getChannelData(ch);2223// Loop through samples and assign computed values.2224for (let sample = 0; sample < data.length; sample++) {2225data[sample] = outb[sample * channels + ch];2226}2227}2228};2229GodotAudioScript.script.connect(GodotAudio.ctx.destination);2230},22312232get_node: function () {2233return GodotAudioScript.script;2234},22352236close: function () {2237return new Promise(function (resolve, reject) {2238GodotAudioScript.script.disconnect();2239GodotAudioScript.script.onaudioprocess = null;2240GodotAudioScript.script = null;2241resolve();2242});2243},2244},22452246godot_audio_script_create__proxy: 'sync',2247godot_audio_script_create__sig: 'iii',2248godot_audio_script_create: function (buffer_length, channel_count) {2249const buf_len = GodotRuntime.getHeapValue(buffer_length, 'i32');2250try {2251const out_len = GodotAudioScript.create(buf_len, channel_count);2252GodotRuntime.setHeapValue(buffer_length, out_len, 'i32');2253} catch (e) {2254GodotRuntime.error('Error starting AudioDriverScriptProcessor', e);2255return 1;2256}2257return 0;2258},22592260godot_audio_script_start__proxy: 'sync',2261godot_audio_script_start__sig: 'viiiii',2262godot_audio_script_start: function (2263p_in_buf,2264p_in_size,2265p_out_buf,2266p_out_size,2267p_cb2268) {2269const onprocess = GodotRuntime.get_func(p_cb);2270GodotAudioScript.start(2271p_in_buf,2272p_in_size,2273p_out_buf,2274p_out_size,2275onprocess2276);2277},2278};22792280autoAddDeps(GodotAudioScript, '$GodotAudioScript');2281mergeInto(LibraryManager.library, GodotAudioScript);228222832284