library_godot_audio.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. /**************************************************************************/
  2. /* library_godot_audio.js */
  3. /**************************************************************************/
  4. /* This file is part of: */
  5. /* GODOT ENGINE */
  6. /* https://godotengine.org */
  7. /**************************************************************************/
  8. /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
  9. /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
  10. /* */
  11. /* Permission is hereby granted, free of charge, to any person obtaining */
  12. /* a copy of this software and associated documentation files (the */
  13. /* "Software"), to deal in the Software without restriction, including */
  14. /* without limitation the rights to use, copy, modify, merge, publish, */
  15. /* distribute, sublicense, and/or sell copies of the Software, and to */
  16. /* permit persons to whom the Software is furnished to do so, subject to */
  17. /* the following conditions: */
  18. /* */
  19. /* The above copyright notice and this permission notice shall be */
  20. /* included in all copies or substantial portions of the Software. */
  21. /* */
  22. /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
  23. /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
  24. /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
  25. /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
  26. /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
  27. /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
  28. /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
  29. /**************************************************************************/
  30. const GodotAudio = {
  31. $GodotAudio__deps: ['$GodotRuntime', '$GodotOS'],
  32. $GodotAudio: {
  33. ctx: null,
  34. input: null,
  35. driver: null,
  36. interval: 0,
  37. init: function (mix_rate, latency, onstatechange, onlatencyupdate) {
  38. const opts = {};
  39. // If mix_rate is 0, let the browser choose.
  40. if (mix_rate) {
  41. opts['sampleRate'] = mix_rate;
  42. }
  43. // Do not specify, leave 'interactive' for good performance.
  44. // opts['latencyHint'] = latency / 1000;
  45. const ctx = new (window.AudioContext || window.webkitAudioContext)(opts);
  46. GodotAudio.ctx = ctx;
  47. ctx.onstatechange = function () {
  48. let state = 0;
  49. switch (ctx.state) {
  50. case 'suspended':
  51. state = 0;
  52. break;
  53. case 'running':
  54. state = 1;
  55. break;
  56. case 'closed':
  57. state = 2;
  58. break;
  59. // no default
  60. }
  61. onstatechange(state);
  62. };
  63. ctx.onstatechange(); // Immediately notify state.
  64. // Update computed latency
  65. GodotAudio.interval = setInterval(function () {
  66. let computed_latency = 0;
  67. if (ctx.baseLatency) {
  68. computed_latency += GodotAudio.ctx.baseLatency;
  69. }
  70. if (ctx.outputLatency) {
  71. computed_latency += GodotAudio.ctx.outputLatency;
  72. }
  73. onlatencyupdate(computed_latency);
  74. }, 1000);
  75. GodotOS.atexit(GodotAudio.close_async);
  76. return ctx.destination.channelCount;
  77. },
  78. create_input: function (callback) {
  79. if (GodotAudio.input) {
  80. return 0; // Already started.
  81. }
  82. function gotMediaInput(stream) {
  83. try {
  84. GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream);
  85. callback(GodotAudio.input);
  86. } catch (e) {
  87. GodotRuntime.error('Failed creating input.', e);
  88. }
  89. }
  90. if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
  91. navigator.mediaDevices.getUserMedia({
  92. 'audio': true,
  93. }).then(gotMediaInput, function (e) {
  94. GodotRuntime.error('Error getting user media.', e);
  95. });
  96. } else {
  97. if (!navigator.getUserMedia) {
  98. navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
  99. }
  100. if (!navigator.getUserMedia) {
  101. GodotRuntime.error('getUserMedia not available.');
  102. return 1;
  103. }
  104. navigator.getUserMedia({
  105. 'audio': true,
  106. }, gotMediaInput, function (e) {
  107. GodotRuntime.print(e);
  108. });
  109. }
  110. return 0;
  111. },
  112. close_async: function (resolve, reject) {
  113. const ctx = GodotAudio.ctx;
  114. GodotAudio.ctx = null;
  115. // Audio was not initialized.
  116. if (!ctx) {
  117. resolve();
  118. return;
  119. }
  120. // Remove latency callback
  121. if (GodotAudio.interval) {
  122. clearInterval(GodotAudio.interval);
  123. GodotAudio.interval = 0;
  124. }
  125. // Disconnect input, if it was started.
  126. if (GodotAudio.input) {
  127. GodotAudio.input.disconnect();
  128. GodotAudio.input = null;
  129. }
  130. // Disconnect output
  131. let closed = Promise.resolve();
  132. if (GodotAudio.driver) {
  133. closed = GodotAudio.driver.close();
  134. }
  135. closed.then(function () {
  136. return ctx.close();
  137. }).then(function () {
  138. ctx.onstatechange = null;
  139. resolve();
  140. }).catch(function (e) {
  141. ctx.onstatechange = null;
  142. GodotRuntime.error('Error closing AudioContext', e);
  143. resolve();
  144. });
  145. },
  146. },
  147. godot_audio_is_available__sig: 'i',
  148. godot_audio_is_available__proxy: 'sync',
  149. godot_audio_is_available: function () {
  150. if (!(window.AudioContext || window.webkitAudioContext)) {
  151. return 0;
  152. }
  153. return 1;
  154. },
  155. godot_audio_has_worklet__proxy: 'sync',
  156. godot_audio_has_worklet__sig: 'i',
  157. godot_audio_has_worklet: function () {
  158. return (GodotAudio.ctx && GodotAudio.ctx.audioWorklet) ? 1 : 0;
  159. },
  160. godot_audio_has_script_processor__proxy: 'sync',
  161. godot_audio_has_script_processor__sig: 'i',
  162. godot_audio_has_script_processor: function () {
  163. return (GodotAudio.ctx && GodotAudio.ctx.createScriptProcessor) ? 1 : 0;
  164. },
  165. godot_audio_init__proxy: 'sync',
  166. godot_audio_init__sig: 'iiiii',
  167. godot_audio_init: function (p_mix_rate, p_latency, p_state_change, p_latency_update) {
  168. const statechange = GodotRuntime.get_func(p_state_change);
  169. const latencyupdate = GodotRuntime.get_func(p_latency_update);
  170. const mix_rate = GodotRuntime.getHeapValue(p_mix_rate, 'i32');
  171. const channels = GodotAudio.init(mix_rate, p_latency, statechange, latencyupdate);
  172. GodotRuntime.setHeapValue(p_mix_rate, GodotAudio.ctx.sampleRate, 'i32');
  173. return channels;
  174. },
  175. godot_audio_resume__proxy: 'sync',
  176. godot_audio_resume__sig: 'v',
  177. godot_audio_resume: function () {
  178. if (GodotAudio.ctx && GodotAudio.ctx.state !== 'running') {
  179. GodotAudio.ctx.resume();
  180. }
  181. },
  182. godot_audio_input_start__proxy: 'sync',
  183. godot_audio_input_start__sig: 'i',
  184. godot_audio_input_start: function () {
  185. return GodotAudio.create_input(function (input) {
  186. input.connect(GodotAudio.driver.get_node());
  187. });
  188. },
  189. godot_audio_input_stop__proxy: 'sync',
  190. godot_audio_input_stop__sig: 'v',
  191. godot_audio_input_stop: function () {
  192. if (GodotAudio.input) {
  193. const tracks = GodotAudio.input['mediaStream']['getTracks']();
  194. for (let i = 0; i < tracks.length; i++) {
  195. tracks[i]['stop']();
  196. }
  197. GodotAudio.input.disconnect();
  198. GodotAudio.input = null;
  199. }
  200. },
  201. };
  202. autoAddDeps(GodotAudio, '$GodotAudio');
  203. mergeInto(LibraryManager.library, GodotAudio);
  204. /**
  205. * The AudioWorklet API driver, used when threads are available.
  206. */
  207. const GodotAudioWorklet = {
  208. $GodotAudioWorklet__deps: ['$GodotAudio', '$GodotConfig'],
  209. $GodotAudioWorklet: {
  210. promise: null,
  211. worklet: null,
  212. ring_buffer: null,
  213. create: function (channels) {
  214. const path = GodotConfig.locate_file('godot.audio.worklet.js');
  215. GodotAudioWorklet.promise = GodotAudio.ctx.audioWorklet.addModule(path).then(function () {
  216. GodotAudioWorklet.worklet = new AudioWorkletNode(
  217. GodotAudio.ctx,
  218. 'godot-processor',
  219. {
  220. 'outputChannelCount': [channels],
  221. }
  222. );
  223. return Promise.resolve();
  224. });
  225. GodotAudio.driver = GodotAudioWorklet;
  226. },
  227. start: function (in_buf, out_buf, state) {
  228. GodotAudioWorklet.promise.then(function () {
  229. const node = GodotAudioWorklet.worklet;
  230. node.connect(GodotAudio.ctx.destination);
  231. node.port.postMessage({
  232. 'cmd': 'start',
  233. 'data': [state, in_buf, out_buf],
  234. });
  235. node.port.onmessage = function (event) {
  236. GodotRuntime.error(event.data);
  237. };
  238. });
  239. },
  240. start_no_threads: function (p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback) {
  241. function RingBuffer() {
  242. let wpos = 0;
  243. let rpos = 0;
  244. let pending_samples = 0;
  245. const wbuf = new Float32Array(p_out_size);
  246. function send(port) {
  247. if (pending_samples === 0) {
  248. return;
  249. }
  250. const buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size);
  251. const size = buffer.length;
  252. const tot_sent = pending_samples;
  253. out_callback(wpos, pending_samples);
  254. if (wpos + pending_samples >= size) {
  255. const high = size - wpos;
  256. wbuf.set(buffer.subarray(wpos, size));
  257. pending_samples -= high;
  258. wpos = 0;
  259. }
  260. if (pending_samples > 0) {
  261. wbuf.set(buffer.subarray(wpos, wpos + pending_samples), tot_sent - pending_samples);
  262. }
  263. port.postMessage({ 'cmd': 'chunk', 'data': wbuf.subarray(0, tot_sent) });
  264. wpos += pending_samples;
  265. pending_samples = 0;
  266. }
  267. this.receive = function (recv_buf) {
  268. const buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size);
  269. const from = rpos;
  270. let to_write = recv_buf.length;
  271. let high = 0;
  272. if (rpos + to_write >= p_in_size) {
  273. high = p_in_size - rpos;
  274. buffer.set(recv_buf.subarray(0, high), rpos);
  275. to_write -= high;
  276. rpos = 0;
  277. }
  278. if (to_write) {
  279. buffer.set(recv_buf.subarray(high, to_write), rpos);
  280. }
  281. in_callback(from, recv_buf.length);
  282. rpos += to_write;
  283. };
  284. this.consumed = function (size, port) {
  285. pending_samples += size;
  286. send(port);
  287. };
  288. }
  289. GodotAudioWorklet.ring_buffer = new RingBuffer();
  290. GodotAudioWorklet.promise.then(function () {
  291. const node = GodotAudioWorklet.worklet;
  292. const buffer = GodotRuntime.heapSlice(HEAPF32, p_out_buf, p_out_size);
  293. node.connect(GodotAudio.ctx.destination);
  294. node.port.postMessage({
  295. 'cmd': 'start_nothreads',
  296. 'data': [buffer, p_in_size],
  297. });
  298. node.port.onmessage = function (event) {
  299. if (!GodotAudioWorklet.worklet) {
  300. return;
  301. }
  302. if (event.data['cmd'] === 'read') {
  303. const read = event.data['data'];
  304. GodotAudioWorklet.ring_buffer.consumed(read, GodotAudioWorklet.worklet.port);
  305. } else if (event.data['cmd'] === 'input') {
  306. const buf = event.data['data'];
  307. if (buf.length > p_in_size) {
  308. GodotRuntime.error('Input chunk is too big');
  309. return;
  310. }
  311. GodotAudioWorklet.ring_buffer.receive(buf);
  312. } else {
  313. GodotRuntime.error(event.data);
  314. }
  315. };
  316. });
  317. },
  318. get_node: function () {
  319. return GodotAudioWorklet.worklet;
  320. },
  321. close: function () {
  322. return new Promise(function (resolve, reject) {
  323. if (GodotAudioWorklet.promise === null) {
  324. return;
  325. }
  326. const p = GodotAudioWorklet.promise;
  327. p.then(function () {
  328. GodotAudioWorklet.worklet.port.postMessage({
  329. 'cmd': 'stop',
  330. 'data': null,
  331. });
  332. GodotAudioWorklet.worklet.disconnect();
  333. GodotAudioWorklet.worklet.port.onmessage = null;
  334. GodotAudioWorklet.worklet = null;
  335. GodotAudioWorklet.promise = null;
  336. resolve();
  337. }).catch(function (err) {
  338. // Aborted?
  339. GodotRuntime.error(err);
  340. });
  341. });
  342. },
  343. },
  344. godot_audio_worklet_create__proxy: 'sync',
  345. godot_audio_worklet_create__sig: 'ii',
  346. godot_audio_worklet_create: function (channels) {
  347. try {
  348. GodotAudioWorklet.create(channels);
  349. } catch (e) {
  350. GodotRuntime.error('Error starting AudioDriverWorklet', e);
  351. return 1;
  352. }
  353. return 0;
  354. },
  355. godot_audio_worklet_start__proxy: 'sync',
  356. godot_audio_worklet_start__sig: 'viiiii',
  357. godot_audio_worklet_start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, p_state) {
  358. const out_buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size);
  359. const in_buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size);
  360. const state = GodotRuntime.heapSub(HEAP32, p_state, 4);
  361. GodotAudioWorklet.start(in_buffer, out_buffer, state);
  362. },
  363. godot_audio_worklet_start_no_threads__proxy: 'sync',
  364. godot_audio_worklet_start_no_threads__sig: 'viiiiii',
  365. 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) {
  366. const out_callback = GodotRuntime.get_func(p_out_callback);
  367. const in_callback = GodotRuntime.get_func(p_in_callback);
  368. GodotAudioWorklet.start_no_threads(p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback);
  369. },
  370. godot_audio_worklet_state_wait__sig: 'iiii',
  371. godot_audio_worklet_state_wait: function (p_state, p_idx, p_expected, p_timeout) {
  372. Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout);
  373. return Atomics.load(HEAP32, (p_state >> 2) + p_idx);
  374. },
  375. godot_audio_worklet_state_add__sig: 'iiii',
  376. godot_audio_worklet_state_add: function (p_state, p_idx, p_value) {
  377. return Atomics.add(HEAP32, (p_state >> 2) + p_idx, p_value);
  378. },
  379. godot_audio_worklet_state_get__sig: 'iii',
  380. godot_audio_worklet_state_get: function (p_state, p_idx) {
  381. return Atomics.load(HEAP32, (p_state >> 2) + p_idx);
  382. },
  383. };
  384. autoAddDeps(GodotAudioWorklet, '$GodotAudioWorklet');
  385. mergeInto(LibraryManager.library, GodotAudioWorklet);
  386. /*
  387. * The deprecated ScriptProcessorNode API, used when threads are disabled.
  388. */
  389. const GodotAudioScript = {
  390. $GodotAudioScript__deps: ['$GodotAudio'],
  391. $GodotAudioScript: {
  392. script: null,
  393. create: function (buffer_length, channel_count) {
  394. GodotAudioScript.script = GodotAudio.ctx.createScriptProcessor(buffer_length, 2, channel_count);
  395. GodotAudio.driver = GodotAudioScript;
  396. return GodotAudioScript.script.bufferSize;
  397. },
  398. start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess) {
  399. GodotAudioScript.script.onaudioprocess = function (event) {
  400. // Read input
  401. const inb = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size);
  402. const input = event.inputBuffer;
  403. if (GodotAudio.input) {
  404. const inlen = input.getChannelData(0).length;
  405. for (let ch = 0; ch < 2; ch++) {
  406. const data = input.getChannelData(ch);
  407. for (let s = 0; s < inlen; s++) {
  408. inb[s * 2 + ch] = data[s];
  409. }
  410. }
  411. }
  412. // Let Godot process the input/output.
  413. onprocess();
  414. // Write the output.
  415. const outb = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size);
  416. const output = event.outputBuffer;
  417. const channels = output.numberOfChannels;
  418. for (let ch = 0; ch < channels; ch++) {
  419. const data = output.getChannelData(ch);
  420. // Loop through samples and assign computed values.
  421. for (let sample = 0; sample < data.length; sample++) {
  422. data[sample] = outb[sample * channels + ch];
  423. }
  424. }
  425. };
  426. GodotAudioScript.script.connect(GodotAudio.ctx.destination);
  427. },
  428. get_node: function () {
  429. return GodotAudioScript.script;
  430. },
  431. close: function () {
  432. return new Promise(function (resolve, reject) {
  433. GodotAudioScript.script.disconnect();
  434. GodotAudioScript.script.onaudioprocess = null;
  435. GodotAudioScript.script = null;
  436. resolve();
  437. });
  438. },
  439. },
  440. godot_audio_script_create__proxy: 'sync',
  441. godot_audio_script_create__sig: 'iii',
  442. godot_audio_script_create: function (buffer_length, channel_count) {
  443. const buf_len = GodotRuntime.getHeapValue(buffer_length, 'i32');
  444. try {
  445. const out_len = GodotAudioScript.create(buf_len, channel_count);
  446. GodotRuntime.setHeapValue(buffer_length, out_len, 'i32');
  447. } catch (e) {
  448. GodotRuntime.error('Error starting AudioDriverScriptProcessor', e);
  449. return 1;
  450. }
  451. return 0;
  452. },
  453. godot_audio_script_start__proxy: 'sync',
  454. godot_audio_script_start__sig: 'viiiii',
  455. godot_audio_script_start: function (p_in_buf, p_in_size, p_out_buf, p_out_size, p_cb) {
  456. const onprocess = GodotRuntime.get_func(p_cb);
  457. GodotAudioScript.start(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess);
  458. },
  459. };
  460. autoAddDeps(GodotAudioScript, '$GodotAudioScript');
  461. mergeInto(LibraryManager.library, GodotAudioScript);