12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267 |
- /**************************************************************************/
- /* 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 | undefined} 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 | undefined} 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 | undefined} 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 | undefined} 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).catch((err) => {
- const newErr = new Error('Failed to create PositionWorklet.');
- newErr.cause = err;
- GodotRuntime.error(newErr);
- });
- }
- /**
- * 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
- */
- async connectPositionWorklet(start) {
- await GodotAudio.audioPositionWorkletPromise;
- if (this.isCanceled) {
- return;
- }
- this._source.connect(this.getPositionWorklet());
- if (start) {
- this.start();
- }
- }
- /**
- * Get a AudioWorkletProcessor
- * @returns {AudioWorkletNode}
- */
- getPositionWorklet() {
- if (this._positionWorklet != null) {
- return this._positionWorklet;
- }
- this._positionWorklet = new AudioWorkletNode(
- GodotAudio.ctx,
- 'godot-position-reporting-processor'
- );
- this._positionWorklet.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 this._positionWorklet;
- }
- /**
- * 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.port.postMessage({ type: 'ended' });
- 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;
- if (this._positionWorklet != null) {
- this._positionWorklet.port.postMessage({ type: 'clear' });
- this._source.connect(this._positionWorklet);
- }
- this._source.start(this.startTime, this.offset + pauseTime);
- this.isStarted = true;
- }
- /**
- * Pauses the `SampleNode`.
- * @returns {void}
- */
- _pause() {
- if (!this.isStarted) {
- return;
- }
- 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,
- /** @type {Promise} */
- audioPositionWorkletPromise: null,
- /**
- * 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);
- const path = GodotConfig.locate_file('godot.audio.position.worklet.js');
- GodotAudio.audioPositionWorkletPromise = ctx.audioWorklet.addModule(path);
- 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 | undefined} 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);
|