From c524f630002c90d78387a0dea1125e811d5b6aab Mon Sep 17 00:00:00 2001 From: tomaka Date: Sun, 22 Oct 2017 14:17:25 +0200 Subject: [PATCH] Improve the emscripten backend (#172) * Use the js! macro from stdweb * Rework the Buffer::finish method * Use references from stdweb * Fix emscripten warnings * Rework the run() method to use stdweb * Adjust timings * Add entry in CHANGELOG --- CHANGELOG.md | 2 + Cargo.toml | 3 + src/emscripten/mod.rs | 237 ++++++++++++++++++++---------------------- src/lib.rs | 7 ++ 4 files changed, 126 insertions(+), 123 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 186c401..4f3b503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Changed the emscripten backend to consume less CPU. + # Version 0.5.1 (2017-10-21) - Added `Sample::to_i16()`, `Sample::to_u16()` and `Sample::from`. diff --git a/Cargo.toml b/Cargo.toml index 4b2a097..a4e494d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,6 @@ libc = "0.2" [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] coreaudio-rs = "0.7.0" + +[target.'cfg(target_os = "emscripten")'.dependencies] +stdweb = { version = "0.1.3", default-features = false } diff --git a/src/emscripten/mod.rs b/src/emscripten/mod.rs index 3c8aed8..f096357 100644 --- a/src/emscripten/mod.rs +++ b/src/emscripten/mod.rs @@ -1,7 +1,12 @@ -use std::marker::PhantomData; -use std::os::raw::c_char; -use std::os::raw::c_int; +use std::mem; use std::os::raw::c_void; +use std::slice::from_raw_parts; +use std::sync::Mutex; +use stdweb; +use stdweb::Reference; +use stdweb::unstable::TryInto; +use stdweb::web::set_timeout; +use stdweb::web::TypedArray; use CreationError; use Format; @@ -10,12 +15,6 @@ use Sample; use SupportedFormat; use UnknownTypeBuffer; -extern { - fn emscripten_set_main_loop_arg(_: extern fn(*mut c_void), _: *mut c_void, _: c_int, _: c_int); - fn emscripten_run_script(script: *const c_char); - fn emscripten_run_script_int(script: *const c_char) -> c_int; -} - // The emscripten backend works by having a global variable named `_cpal_audio_contexts`, which // is an array of `AudioContext` objects. A voice ID corresponds to an entry in this array. // @@ -25,121 +24,115 @@ extern { // that is in each buffer ; this is obviously bad, and also the schedule is too tight and there may // be underflows -pub struct EventLoop; +pub struct EventLoop { + voices: Mutex>>, +} + impl EventLoop { #[inline] pub fn new() -> EventLoop { - EventLoop - } + stdweb::initialize(); - #[inline] - pub fn run(&self, mut callback: F) -> ! - where F: FnMut(VoiceId, UnknownTypeBuffer) - { - unsafe { - // The `run` function uses `emscripten_set_main_loop_arg` to invoke a Rust callback - // repeatidely. The job of this callback is to fill the content of the audio buffers. - - // The first argument of the callback function (a `void*`) is a casted pointer to the - // `callback` parameter that was passed to `run`. - - extern "C" fn callback_fn(callback_ptr: *mut c_void) - where F: FnMut(VoiceId, UnknownTypeBuffer) - { - unsafe { - let num_contexts = emscripten_run_script_int("(function() { - if (window._cpal_audio_contexts) - return window._cpal_audio_contexts.length; - else - return 0; - })()\0".as_ptr() as *const _); - - // TODO: this processes all the voices, even those from maybe other event loops - // this is not a problem yet, but may become one in the future? - for voice_id in 0 .. num_contexts { - let callback_ptr = &mut *(callback_ptr as *mut F); - - let buffer = Buffer { - temporary_buffer: vec![0.0; 44100 * 2 / 3], - voice_id: voice_id, - marker: PhantomData, - }; - - callback_ptr(VoiceId(voice_id), ::UnknownTypeBuffer::F32(::Buffer { target: Some(buffer) })); - } - } - } - - let callback_ptr = &mut callback as *mut F as *mut c_void; - emscripten_set_main_loop_arg(callback_fn::, callback_ptr, 3, 1); - - unreachable!() + EventLoop { + voices: Mutex::new(Vec::new()), } } #[inline] - pub fn build_voice(&self, _: &Endpoint, format: &Format) + pub fn run(&self, callback: F) -> ! + where F: FnMut(VoiceId, UnknownTypeBuffer) + { + // The `run` function uses `set_timeout` to invoke a Rust callback repeatidely. The job + // of this callback is to fill the content of the audio buffers. + + // The first argument of the callback function (a `void*`) is a casted pointer to `self` + // and to the `callback` parameter that was passed to `run`. + + fn callback_fn(user_data_ptr: *mut c_void) + where F: FnMut(VoiceId, UnknownTypeBuffer) + { + unsafe { + let user_data_ptr2 = user_data_ptr as *mut (&EventLoop, F); + let user_data = &mut *user_data_ptr2; + let user_cb = &mut user_data.1; + + let voices = user_data.0.voices.lock().unwrap().clone(); + for (voice_id, voice) in voices.iter().enumerate() { + let voice = match voice.as_ref() { + Some(v) => v, + None => continue, + }; + + let buffer = Buffer { + temporary_buffer: vec![0.0; 44100 * 2 / 3], + voice: &voice, + }; + + user_cb(VoiceId(voice_id), ::UnknownTypeBuffer::F32(::Buffer { target: Some(buffer) })); + } + + set_timeout(|| callback_fn::(user_data_ptr), 330); + } + } + + let mut user_data = (self, callback); + let user_data_ptr = &mut user_data as *mut (_, _); + + set_timeout(|| callback_fn::(user_data_ptr as *mut _), 10); + + stdweb::event_loop(); + } + + #[inline] + pub fn build_voice(&self, _: &Endpoint, _format: &Format) -> Result { - // TODO: find an empty element in the array first, instead of pushing at the end, in case - // the user creates and destroys lots of voices? + let voice = js!(return new AudioContext()).into_reference().unwrap(); - let num = unsafe { - emscripten_run_script_int(concat!(r#"(function() { - if (!window._cpal_audio_contexts) - window._cpal_audio_contexts = new Array(); - window._cpal_audio_contexts.push(new AudioContext()); - return window._cpal_audio_contexts.length - 1; - })()"#, "\0").as_ptr() as *const _) + let mut voices = self.voices.lock().unwrap(); + let voice_id = if let Some(pos) = voices.iter().position(|v| v.is_none()) { + voices[pos] = Some(voice); + pos + } else { + let l = voices.len(); + voices.push(Some(voice)); + l }; - Ok(VoiceId(num)) + Ok(VoiceId(voice_id)) } #[inline] pub fn destroy_voice(&self, voice_id: VoiceId) { - unsafe { - let script = format!(" - if (window._cpal_audio_contexts) - window._cpal_audio_contexts[{}] = null;\0", voice_id.0); - emscripten_run_script(script.as_ptr() as *const _) - } + self.voices.lock().unwrap()[voice_id.0] = None; } #[inline] pub fn play(&self, voice_id: VoiceId) { - unsafe { - let script = format!(" - if (window._cpal_audio_contexts) - if (window._cpal_audio_contexts[{v}]) - window._cpal_audio_contexts[{v}].resume();\0", v = voice_id.0); - emscripten_run_script(script.as_ptr() as *const _) - } + let voices = self.voices.lock().unwrap(); + let voice = voices.get(voice_id.0).and_then(|v| v.as_ref()).expect("invalid voice ID"); + js!(@{voice}.resume()); } #[inline] pub fn pause(&self, voice_id: VoiceId) { - unsafe { - let script = format!(" - if (window._cpal_audio_contexts) - if (window._cpal_audio_contexts[{v}]) - window._cpal_audio_contexts[{v}].suspend();\0", v = voice_id.0); - emscripten_run_script(script.as_ptr() as *const _) - } + let voices = self.voices.lock().unwrap(); + let voice = voices.get(voice_id.0).and_then(|v| v.as_ref()).expect("invalid voice ID"); + js!(@{voice}.suspend()); } } -// Index within the `_cpal_audio_contexts` global variable in Javascript. +// Index within the `voices` array of the events loop. #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct VoiceId(c_int); +pub struct VoiceId(usize); // Detects whether the `AudioContext` global variable is available. fn is_webaudio_available() -> bool { - unsafe { - emscripten_run_script_int(concat!(r#"(function() { - if (!AudioContext) { return 0; } else { return 1; } - })()"#, "\0").as_ptr() as *const _) != 0 - } + stdweb::initialize(); + + js!( + if (!AudioContext) { return false; } else { return true; } + ).try_into().unwrap() } // Content is false if the iterator is empty. @@ -201,8 +194,7 @@ pub type SupportedFormatsIterator = ::std::vec::IntoIter; pub struct Buffer<'a, T: 'a> where T: Sample { temporary_buffer: Vec, - voice_id: c_int, - marker: PhantomData<&'a mut T>, + voice: &'a Reference, } impl<'a, T> Buffer<'a, T> where T: Sample { @@ -218,37 +210,36 @@ impl<'a, T> Buffer<'a, T> where T: Sample { #[inline] pub fn finish(self) { - unsafe { - // TODO: **very** slow - let src_data = self.temporary_buffer.iter().map(|&b| b.to_f32().to_string() + ", ").fold(String::new(), |mut a, b| { a.push_str(&b); a }); + // TODO: directly use a TypedArray once this is supported by stdweb - debug_assert_eq!(self.temporary_buffer.len() % 2, 0); // TODO: num channels + let typed_array = { + let t_slice: &[T] = self.temporary_buffer.as_slice(); + let u8_slice: &[u8] = unsafe { from_raw_parts(t_slice.as_ptr() as *const _, t_slice.len() * mem::size_of::()) }; + let typed_array: TypedArray = u8_slice.into(); + typed_array + }; - let script = format!("(function() {{ - if (!window._cpal_audio_contexts) - return; - var context = window._cpal_audio_contexts[{voice_id}]; - if (!context) - return; - var buffer = context.createBuffer({num_channels}, {buf_len} / {num_channels}, 44100); - var src = [{src_data}]; - for (var channel = 0; channel < {num_channels}; ++channel) {{ - var buffer_content = buffer.getChannelData(channel); - for (var i = 0; i < {buf_len} / {num_channels}; ++i) {{ - buffer_content[i] = src[i * {num_channels} + channel]; - }} - }} - var node = context.createBufferSource(); - node.buffer = buffer; - node.connect(context.destination); - node.start(); - }})()\0", - num_channels = 2, - voice_id = self.voice_id, - buf_len = self.temporary_buffer.len(), - src_data = src_data); + let num_channels = 2u32; // TODO: correct value + debug_assert_eq!(self.temporary_buffer.len() % num_channels as usize, 0); - emscripten_run_script(script.as_ptr() as *const _) - } + js!( + var src_buffer = new Float32Array(@{typed_array}.buffer); + var context = @{self.voice}; + var buf_len = @{self.temporary_buffer.len() as u32}; + var num_channels = @{num_channels}; + + var buffer = context.createBuffer(num_channels, buf_len / num_channels, 44100); + for (var channel = 0; channel < num_channels; ++channel) { + var buffer_content = buffer.getChannelData(channel); + for (var i = 0; i < buf_len / num_channels; ++i) { + buffer_content[i] = src_buffer[i * num_channels + channel]; + } + } + + var node = context.createBufferSource(); + node.buffer = buffer; + node.connect(context.destination); + node.start(); + ); } } diff --git a/src/lib.rs b/src/lib.rs index cf36c9f..de86f75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,9 +38,16 @@ from time to time. */ +#![recursion_limit = "512"] + #[macro_use] extern crate lazy_static; +// Extern crate declarations with `#[macro_use]` must unfortunately be at crate root. +#[cfg(target_os = "emscripten")] +#[macro_use] +extern crate stdweb; + pub use samples_formats::{Sample, SampleFormat}; #[cfg(all(not(windows), not(target_os = "linux"), not(target_os = "freebsd"),