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
This commit is contained in:
parent
f7c503ff05
commit
c524f63000
|
@ -1,5 +1,7 @@
|
||||||
# Unreleased
|
# Unreleased
|
||||||
|
|
||||||
|
- Changed the emscripten backend to consume less CPU.
|
||||||
|
|
||||||
# Version 0.5.1 (2017-10-21)
|
# Version 0.5.1 (2017-10-21)
|
||||||
|
|
||||||
- Added `Sample::to_i16()`, `Sample::to_u16()` and `Sample::from`.
|
- Added `Sample::to_i16()`, `Sample::to_u16()` and `Sample::from`.
|
||||||
|
|
|
@ -22,3 +22,6 @@ libc = "0.2"
|
||||||
|
|
||||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
||||||
coreaudio-rs = "0.7.0"
|
coreaudio-rs = "0.7.0"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "emscripten")'.dependencies]
|
||||||
|
stdweb = { version = "0.1.3", default-features = false }
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
use std::marker::PhantomData;
|
use std::mem;
|
||||||
use std::os::raw::c_char;
|
|
||||||
use std::os::raw::c_int;
|
|
||||||
use std::os::raw::c_void;
|
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 CreationError;
|
||||||
use Format;
|
use Format;
|
||||||
|
@ -10,12 +15,6 @@ use Sample;
|
||||||
use SupportedFormat;
|
use SupportedFormat;
|
||||||
use UnknownTypeBuffer;
|
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
|
// 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.
|
// 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
|
// that is in each buffer ; this is obviously bad, and also the schedule is too tight and there may
|
||||||
// be underflows
|
// be underflows
|
||||||
|
|
||||||
pub struct EventLoop;
|
pub struct EventLoop {
|
||||||
|
voices: Mutex<Vec<Option<Reference>>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl EventLoop {
|
impl EventLoop {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new() -> EventLoop {
|
pub fn new() -> EventLoop {
|
||||||
EventLoop
|
stdweb::initialize();
|
||||||
|
|
||||||
|
EventLoop {
|
||||||
|
voices: Mutex::new(Vec::new()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn run<F>(&self, mut callback: F) -> !
|
pub fn run<F>(&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<F>(user_data_ptr: *mut c_void)
|
||||||
where F: FnMut(VoiceId, UnknownTypeBuffer)
|
where F: FnMut(VoiceId, UnknownTypeBuffer)
|
||||||
{
|
{
|
||||||
unsafe {
|
unsafe {
|
||||||
// The `run` function uses `emscripten_set_main_loop_arg` to invoke a Rust callback
|
let user_data_ptr2 = user_data_ptr as *mut (&EventLoop, F);
|
||||||
// repeatidely. The job of this callback is to fill the content of the audio buffers.
|
let user_data = &mut *user_data_ptr2;
|
||||||
|
let user_cb = &mut user_data.1;
|
||||||
|
|
||||||
// The first argument of the callback function (a `void*`) is a casted pointer to the
|
let voices = user_data.0.voices.lock().unwrap().clone();
|
||||||
// `callback` parameter that was passed to `run`.
|
for (voice_id, voice) in voices.iter().enumerate() {
|
||||||
|
let voice = match voice.as_ref() {
|
||||||
extern "C" fn callback_fn<F>(callback_ptr: *mut c_void)
|
Some(v) => v,
|
||||||
where F: FnMut(VoiceId, UnknownTypeBuffer)
|
None => continue,
|
||||||
{
|
};
|
||||||
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 {
|
let buffer = Buffer {
|
||||||
temporary_buffer: vec![0.0; 44100 * 2 / 3],
|
temporary_buffer: vec![0.0; 44100 * 2 / 3],
|
||||||
voice_id: voice_id,
|
voice: &voice,
|
||||||
marker: PhantomData,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
callback_ptr(VoiceId(voice_id), ::UnknownTypeBuffer::F32(::Buffer { target: Some(buffer) }));
|
user_cb(VoiceId(voice_id), ::UnknownTypeBuffer::F32(::Buffer { target: Some(buffer) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_timeout(|| callback_fn::<F>(user_data_ptr), 330);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let callback_ptr = &mut callback as *mut F as *mut c_void;
|
let mut user_data = (self, callback);
|
||||||
emscripten_set_main_loop_arg(callback_fn::<F>, callback_ptr, 3, 1);
|
let user_data_ptr = &mut user_data as *mut (_, _);
|
||||||
|
|
||||||
unreachable!()
|
set_timeout(|| callback_fn::<F>(user_data_ptr as *mut _), 10);
|
||||||
}
|
|
||||||
|
stdweb::event_loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn build_voice(&self, _: &Endpoint, format: &Format)
|
pub fn build_voice(&self, _: &Endpoint, _format: &Format)
|
||||||
-> Result<VoiceId, CreationError>
|
-> Result<VoiceId, CreationError>
|
||||||
{
|
{
|
||||||
// TODO: find an empty element in the array first, instead of pushing at the end, in case
|
let voice = js!(return new AudioContext()).into_reference().unwrap();
|
||||||
// the user creates and destroys lots of voices?
|
|
||||||
|
|
||||||
let num = unsafe {
|
let mut voices = self.voices.lock().unwrap();
|
||||||
emscripten_run_script_int(concat!(r#"(function() {
|
let voice_id = if let Some(pos) = voices.iter().position(|v| v.is_none()) {
|
||||||
if (!window._cpal_audio_contexts)
|
voices[pos] = Some(voice);
|
||||||
window._cpal_audio_contexts = new Array();
|
pos
|
||||||
window._cpal_audio_contexts.push(new AudioContext());
|
} else {
|
||||||
return window._cpal_audio_contexts.length - 1;
|
let l = voices.len();
|
||||||
})()"#, "\0").as_ptr() as *const _)
|
voices.push(Some(voice));
|
||||||
|
l
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(VoiceId(num))
|
Ok(VoiceId(voice_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn destroy_voice(&self, voice_id: VoiceId) {
|
pub fn destroy_voice(&self, voice_id: VoiceId) {
|
||||||
unsafe {
|
self.voices.lock().unwrap()[voice_id.0] = None;
|
||||||
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 _)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn play(&self, voice_id: VoiceId) {
|
pub fn play(&self, voice_id: VoiceId) {
|
||||||
unsafe {
|
let voices = self.voices.lock().unwrap();
|
||||||
let script = format!("
|
let voice = voices.get(voice_id.0).and_then(|v| v.as_ref()).expect("invalid voice ID");
|
||||||
if (window._cpal_audio_contexts)
|
js!(@{voice}.resume());
|
||||||
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 _)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn pause(&self, voice_id: VoiceId) {
|
pub fn pause(&self, voice_id: VoiceId) {
|
||||||
unsafe {
|
let voices = self.voices.lock().unwrap();
|
||||||
let script = format!("
|
let voice = voices.get(voice_id.0).and_then(|v| v.as_ref()).expect("invalid voice ID");
|
||||||
if (window._cpal_audio_contexts)
|
js!(@{voice}.suspend());
|
||||||
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 _)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct VoiceId(c_int);
|
pub struct VoiceId(usize);
|
||||||
|
|
||||||
// Detects whether the `AudioContext` global variable is available.
|
// Detects whether the `AudioContext` global variable is available.
|
||||||
fn is_webaudio_available() -> bool {
|
fn is_webaudio_available() -> bool {
|
||||||
unsafe {
|
stdweb::initialize();
|
||||||
emscripten_run_script_int(concat!(r#"(function() {
|
|
||||||
if (!AudioContext) { return 0; } else { return 1; }
|
js!(
|
||||||
})()"#, "\0").as_ptr() as *const _) != 0
|
if (!AudioContext) { return false; } else { return true; }
|
||||||
}
|
).try_into().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content is false if the iterator is empty.
|
// Content is false if the iterator is empty.
|
||||||
|
@ -201,8 +194,7 @@ pub type SupportedFormatsIterator = ::std::vec::IntoIter<SupportedFormat>;
|
||||||
|
|
||||||
pub struct Buffer<'a, T: 'a> where T: Sample {
|
pub struct Buffer<'a, T: 'a> where T: Sample {
|
||||||
temporary_buffer: Vec<T>,
|
temporary_buffer: Vec<T>,
|
||||||
voice_id: c_int,
|
voice: &'a Reference,
|
||||||
marker: PhantomData<&'a mut T>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T> Buffer<'a, T> where T: Sample {
|
impl<'a, T> Buffer<'a, T> where T: Sample {
|
||||||
|
@ -218,37 +210,36 @@ impl<'a, T> Buffer<'a, T> where T: Sample {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn finish(self) {
|
pub fn finish(self) {
|
||||||
unsafe {
|
// TODO: directly use a TypedArray<f32> once this is supported by stdweb
|
||||||
// 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 });
|
|
||||||
|
|
||||||
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::<T>()) };
|
||||||
|
let typed_array: TypedArray<u8> = u8_slice.into();
|
||||||
|
typed_array
|
||||||
|
};
|
||||||
|
|
||||||
let script = format!("(function() {{
|
let num_channels = 2u32; // TODO: correct value
|
||||||
if (!window._cpal_audio_contexts)
|
debug_assert_eq!(self.temporary_buffer.len() % num_channels as usize, 0);
|
||||||
return;
|
|
||||||
var context = window._cpal_audio_contexts[{voice_id}];
|
js!(
|
||||||
if (!context)
|
var src_buffer = new Float32Array(@{typed_array}.buffer);
|
||||||
return;
|
var context = @{self.voice};
|
||||||
var buffer = context.createBuffer({num_channels}, {buf_len} / {num_channels}, 44100);
|
var buf_len = @{self.temporary_buffer.len() as u32};
|
||||||
var src = [{src_data}];
|
var num_channels = @{num_channels};
|
||||||
for (var channel = 0; channel < {num_channels}; ++channel) {{
|
|
||||||
|
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);
|
var buffer_content = buffer.getChannelData(channel);
|
||||||
for (var i = 0; i < {buf_len} / {num_channels}; ++i) {{
|
for (var i = 0; i < buf_len / num_channels; ++i) {
|
||||||
buffer_content[i] = src[i * {num_channels} + channel];
|
buffer_content[i] = src_buffer[i * num_channels + channel];
|
||||||
}}
|
}
|
||||||
}}
|
}
|
||||||
|
|
||||||
var node = context.createBufferSource();
|
var node = context.createBufferSource();
|
||||||
node.buffer = buffer;
|
node.buffer = buffer;
|
||||||
node.connect(context.destination);
|
node.connect(context.destination);
|
||||||
node.start();
|
node.start();
|
||||||
}})()\0",
|
);
|
||||||
num_channels = 2,
|
|
||||||
voice_id = self.voice_id,
|
|
||||||
buf_len = self.temporary_buffer.len(),
|
|
||||||
src_data = src_data);
|
|
||||||
|
|
||||||
emscripten_run_script(script.as_ptr() as *const _)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,9 +38,16 @@ from time to time.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#![recursion_limit = "512"]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
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};
|
pub use samples_formats::{Sample, SampleFormat};
|
||||||
|
|
||||||
#[cfg(all(not(windows), not(target_os = "linux"), not(target_os = "freebsd"),
|
#[cfg(all(not(windows), not(target_os = "linux"), not(target_os = "freebsd"),
|
||||||
|
|
Loading…
Reference in New Issue