[Web] Rename JavaScript platform to Web.
Also rename export name from "HTML5" to "Web".
This commit is contained in:
484
platform/web/js/libs/library_godot_audio.js
Normal file
484
platform/web/js/libs/library_godot_audio.js
Normal file
@ -0,0 +1,484 @@
|
||||
/*************************************************************************/
|
||||
/* library_godot_audio.js */
|
||||
/*************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/*************************************************************************/
|
||||
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
|
||||
/* Copyright (c) 2014-2022 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 opts = {};
|
||||
// If mix_rate is 0, let the browser choose.
|
||||
if (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;
|
||||
|
||||
// no default
|
||||
}
|
||||
onstatechange(state);
|
||||
};
|
||||
ctx.onstatechange(); // Immediately notify state.
|
||||
// Update computed latency
|
||||
GodotAudio.interval = setInterval(function () {
|
||||
let computed_latency = 0;
|
||||
if (ctx.baseLatency) {
|
||||
computed_latency += GodotAudio.ctx.baseLatency;
|
||||
}
|
||||
if (ctx.outputLatency) {
|
||||
computed_latency += GodotAudio.ctx.outputLatency;
|
||||
}
|
||||
onlatencyupdate(computed_latency);
|
||||
}, 1000);
|
||||
GodotOS.atexit(GodotAudio.close_async);
|
||||
return ctx.destination.channelCount;
|
||||
},
|
||||
|
||||
create_input: function (callback) {
|
||||
if (GodotAudio.input) {
|
||||
return 0; // Already started.
|
||||
}
|
||||
function gotMediaInput(stream) {
|
||||
try {
|
||||
GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream);
|
||||
callback(GodotAudio.input);
|
||||
} catch (e) {
|
||||
GodotRuntime.error('Failed creaating 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();
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
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__sig: 'i',
|
||||
godot_audio_has_worklet: function () {
|
||||
return (GodotAudio.ctx && GodotAudio.ctx.audioWorklet) ? 1 : 0;
|
||||
},
|
||||
|
||||
godot_audio_has_script_processor__sig: 'i',
|
||||
godot_audio_has_script_processor: function () {
|
||||
return (GodotAudio.ctx && GodotAudio.ctx.createScriptProcessor) ? 1 : 0;
|
||||
},
|
||||
|
||||
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__sig: 'v',
|
||||
godot_audio_resume: function () {
|
||||
if (GodotAudio.ctx && GodotAudio.ctx.state !== 'running') {
|
||||
GodotAudio.ctx.resume();
|
||||
}
|
||||
},
|
||||
|
||||
godot_audio_capture_start__proxy: 'sync',
|
||||
godot_audio_capture_start__sig: 'i',
|
||||
godot_audio_capture_start: function () {
|
||||
return GodotAudio.create_input(function (input) {
|
||||
input.connect(GodotAudio.driver.get_node());
|
||||
});
|
||||
},
|
||||
|
||||
godot_audio_capture_stop__proxy: 'sync',
|
||||
godot_audio_capture_stop__sig: 'v',
|
||||
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,
|
||||
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;
|
||||
}
|
||||
GodotAudioWorklet.promise.then(function () {
|
||||
GodotAudioWorklet.worklet.port.postMessage({
|
||||
'cmd': 'stop',
|
||||
'data': null,
|
||||
});
|
||||
GodotAudioWorklet.worklet.disconnect();
|
||||
GodotAudioWorklet.worklet = null;
|
||||
GodotAudioWorklet.promise = null;
|
||||
resolve();
|
||||
}).catch(function (err) { /* aborted? */ });
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
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__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__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 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__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__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);
|
||||
Reference in New Issue
Block a user