mirror of
				https://github.com/godotengine/godot.git
				synced 2025-10-31 21:51:22 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			344 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			344 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*************************************************************************/
 | |
| /*  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);
 | 
