/*************************************************************************/ /* library_godot_audio.js */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ /* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ /* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* 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. */ /*************************************************************************/ const GodotAudio = { $GodotAudio__deps: ['$GodotRuntime', '$GodotOS'], $GodotAudio: { ctx: null, input: null, driver: null, interval: 0, init: function (mix_rate, latency, onstatechange, onlatencyupdate) { const ctx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: mix_rate, // latencyHint: latency / 1000 // Do not specify, leave 'interactive' for good performance. }); 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; // no default } onstatechange(state); }; ctx.onstatechange(); // Immeditately notify state. // Update computed latency GodotAudio.interval = setInterval(function () { let computed_latency = 0; if (ctx.baseLatency) { computed_latency += GodotAudio.ctx.baseLatency; } if (ctx.outputLatency) { computed_latency += GodotAudio.ctx.outputLatency; } onlatencyupdate(computed_latency); }, 1000); GodotOS.atexit(GodotAudio.close_async); return ctx.destination.channelCount; }, create_input: function (callback) { if (GodotAudio.input) { return; // Already started. } function gotMediaInput(stream) { GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream); callback(GodotAudio.input); } if (navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({ 'audio': true, }).then(gotMediaInput, function (e) { GodotRuntime.print(e); }); } else { if (!navigator.getUserMedia) { navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; } navigator.getUserMedia({ 'audio': true, }, gotMediaInput, function (e) { GodotRuntime.print(e); }); } }, 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(); }); }, }, godot_audio_is_available__proxy: 'sync', godot_audio_is_available: function () { if (!(window.AudioContext || window.webkitAudioContext)) { return 0; } return 1; }, 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); return GodotAudio.init(p_mix_rate, p_latency, statechange, latencyupdate); }, godot_audio_resume: function () { if (GodotAudio.ctx && GodotAudio.ctx.state !== 'running') { GodotAudio.ctx.resume(); } }, godot_audio_capture_start__proxy: 'sync', godot_audio_capture_start: function () { if (GodotAudio.input) { return; // Already started. } GodotAudio.create_input(function (input) { input.connect(GodotAudio.driver.get_node()); }); }, godot_audio_capture_stop__proxy: 'sync', godot_audio_capture_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; } }, }; 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, 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); }; }); }, get_node: function () { return GodotAudioWorklet.worklet; }, close: function () { return new Promise(function (resolve, reject) { GodotAudioWorklet.promise.then(function () { GodotAudioWorklet.worklet.port.postMessage({ 'cmd': 'stop', 'data': null, }); GodotAudioWorklet.worklet.disconnect(); GodotAudioWorklet.worklet = null; GodotAudioWorklet.promise = null; resolve(); }); }); }, }, godot_audio_worklet_create: function (channels) { GodotAudioWorklet.create(channels); }, 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_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: function (p_state, p_idx, p_value) { return Atomics.add(HEAP32, (p_state >> 2) + p_idx, p_value); }, 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 deprecated ScriptProcessorNode API, used when threads are disabled. */ 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: function (buffer_length, channel_count) { return GodotAudioScript.create(buffer_length, channel_count); }, 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);