mirror of
https://github.com/project-slippi/cpal.git
synced 2025-10-06 00:02:40 +02:00
Remove stdweb in favor of js-sys, web-sys, and wasm-bindgen (#713)
This commit is contained in:
@@ -50,7 +50,10 @@ coreaudio-rs = { version = "0.10", default-features = false, features = ["audio_
|
||||
coreaudio-rs = { version = "0.10", default-features = false, features = ["audio_unit", "core_audio", "audio_toolbox"] }
|
||||
|
||||
[target.'cfg(target_os = "emscripten")'.dependencies]
|
||||
stdweb = { version = "0.4.20", default-features = false }
|
||||
wasm-bindgen = { version = "0.2.58" }
|
||||
wasm-bindgen-futures = "0.4.33"
|
||||
js-sys = { version = "0.3.35" }
|
||||
web-sys = { version = "0.3.35", features = [ "AudioContext", "AudioContextOptions", "AudioBuffer", "AudioBufferSourceNode", "AudioNode", "AudioDestinationNode", "Window", "AudioContextState"] }
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
wasm-bindgen = { version = "0.2.58", optional = true }
|
||||
|
@@ -1,12 +1,9 @@
|
||||
use std::mem;
|
||||
use std::os::raw::c_void;
|
||||
use std::slice::from_raw_parts;
|
||||
use js_sys::Float32Array;
|
||||
use std::time::Duration;
|
||||
use stdweb;
|
||||
use stdweb::unstable::TryInto;
|
||||
use stdweb::web::set_timeout;
|
||||
use stdweb::web::TypedArray;
|
||||
use stdweb::Reference;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
use web_sys::AudioContext;
|
||||
|
||||
use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use crate::{
|
||||
@@ -30,9 +27,11 @@ pub struct Devices(bool);
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Device;
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone)]
|
||||
pub struct Stream {
|
||||
// A reference to an `AudioContext` object.
|
||||
audio_ctxt_ref: Reference,
|
||||
audio_ctxt: AudioContext,
|
||||
}
|
||||
|
||||
// Index within the `streams` array of the events loop.
|
||||
@@ -54,7 +53,6 @@ const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;
|
||||
|
||||
impl Host {
|
||||
pub fn new() -> Result<Self, crate::HostUnavailable> {
|
||||
stdweb::initialize();
|
||||
Ok(Host)
|
||||
}
|
||||
}
|
||||
@@ -186,7 +184,7 @@ impl DeviceTrait for Device {
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
_error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
@@ -209,12 +207,8 @@ impl DeviceTrait for Device {
|
||||
};
|
||||
|
||||
// Create the stream.
|
||||
let audio_ctxt_ref = js!(return new AudioContext()).into_reference().unwrap();
|
||||
let stream = Stream { audio_ctxt_ref };
|
||||
|
||||
// Specify the callback.
|
||||
let mut user_data = (self, data_callback, error_callback);
|
||||
let user_data_ptr = &mut user_data as *mut (_, _, _);
|
||||
let audio_ctxt = AudioContext::new().expect("webaudio is not present on this system");
|
||||
let stream = Stream { audio_ctxt };
|
||||
|
||||
// Use `set_timeout` to invoke a Rust callback repeatedly.
|
||||
//
|
||||
@@ -223,15 +217,12 @@ impl DeviceTrait for Device {
|
||||
// See also: The call to `set_timeout` at the end of the `audio_callback_fn` which creates
|
||||
// the loop.
|
||||
set_timeout(
|
||||
|| {
|
||||
audio_callback_fn::<D, E>(
|
||||
user_data_ptr as *mut c_void,
|
||||
config,
|
||||
sample_format,
|
||||
buffer_size_frames,
|
||||
)
|
||||
},
|
||||
10,
|
||||
stream.clone(),
|
||||
data_callback,
|
||||
config,
|
||||
sample_format,
|
||||
buffer_size_frames as u32,
|
||||
);
|
||||
|
||||
Ok(stream)
|
||||
@@ -240,108 +231,141 @@ impl DeviceTrait for Device {
|
||||
|
||||
impl StreamTrait for Stream {
|
||||
fn play(&self) -> Result<(), PlayStreamError> {
|
||||
let audio_ctxt = &self.audio_ctxt_ref;
|
||||
js!(@{audio_ctxt}.resume());
|
||||
let future = JsFuture::from(
|
||||
self.audio_ctxt
|
||||
.resume()
|
||||
.expect("Could not resume the stream"),
|
||||
);
|
||||
spawn_local(async {
|
||||
match future.await {
|
||||
Ok(value) => assert!(value.is_undefined()),
|
||||
Err(value) => panic!("AudioContext.resume() promise was rejected: {:?}", value),
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<(), PauseStreamError> {
|
||||
let audio_ctxt = &self.audio_ctxt_ref;
|
||||
js!(@{audio_ctxt}.suspend());
|
||||
let future = JsFuture::from(
|
||||
self.audio_ctxt
|
||||
.suspend()
|
||||
.expect("Could not suspend the stream"),
|
||||
);
|
||||
spawn_local(async {
|
||||
match future.await {
|
||||
Ok(value) => assert!(value.is_undefined()),
|
||||
Err(value) => panic!("AudioContext.suspend() promise was rejected: {:?}", value),
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// The first argument of the callback function (a `void*`) is a cast pointer to `self`
|
||||
// and to the `callback` parameter that was passed to `run`.
|
||||
fn audio_callback_fn<D, E>(
|
||||
user_data_ptr: *mut c_void,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
buffer_size_frames: usize,
|
||||
) where
|
||||
fn audio_callback_fn<D>(
|
||||
mut data_callback: D,
|
||||
) -> impl FnOnce(Stream, StreamConfig, SampleFormat, u32)
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
let num_channels = config.channels as usize;
|
||||
let sample_rate = config.sample_rate.0;
|
||||
let buffer_size_samples = buffer_size_frames * num_channels;
|
||||
|
||||
unsafe {
|
||||
let user_data_ptr2 = user_data_ptr as *mut (&Stream, D, E);
|
||||
let user_data = &mut *user_data_ptr2;
|
||||
let (ref stream, ref mut data_cb, ref mut _err_cb) = user_data;
|
||||
let audio_ctxt = &stream.audio_ctxt_ref;
|
||||
|stream, config, sample_format, buffer_size_frames| {
|
||||
let sample_rate = config.sample_rate.0;
|
||||
let buffer_size_samples = buffer_size_frames * config.channels as u32;
|
||||
let audio_ctxt = &stream.audio_ctxt;
|
||||
|
||||
// TODO: We should be re-using a buffer.
|
||||
let mut temporary_buffer = vec![0f32; buffer_size_samples];
|
||||
let mut temporary_buffer = vec![0f32; buffer_size_samples as usize];
|
||||
|
||||
{
|
||||
let len = temporary_buffer.len();
|
||||
let data = temporary_buffer.as_mut_ptr() as *mut ();
|
||||
let mut data = Data::from_parts(data, len, sample_format);
|
||||
|
||||
let now_secs: f64 = js!(@{audio_ctxt}.getOutputTimestamp().currentTime)
|
||||
.try_into()
|
||||
.expect("failed to retrieve Value as f64");
|
||||
let mut data = unsafe { Data::from_parts(data, len, sample_format) };
|
||||
let now_secs: f64 = audio_ctxt.current_time();
|
||||
let callback = crate::StreamInstant::from_secs_f64(now_secs);
|
||||
// TODO: Use proper latency instead. Currently, unsupported on most browsers though, so
|
||||
// we estimate based on buffer size instead. Probably should use this, but it's only
|
||||
// supported by firefox (2020-04-28).
|
||||
// let latency_secs: f64 = js!(@{audio_ctxt}.outputLatency).try_into().unwrap();
|
||||
// let latency_secs: f64 = audio_ctxt.outputLatency.try_into().unwrap();
|
||||
let buffer_duration = frames_to_duration(len, sample_rate as usize);
|
||||
let playback = callback
|
||||
.add(buffer_duration)
|
||||
.expect("`playback` occurs beyond representation supported by `StreamInstant`");
|
||||
let timestamp = crate::OutputStreamTimestamp { callback, playback };
|
||||
let info = OutputCallbackInfo { timestamp };
|
||||
data_cb(&mut data, &info);
|
||||
data_callback(&mut data, &info);
|
||||
}
|
||||
|
||||
// TODO: directly use a TypedArray<f32> once this is supported by stdweb
|
||||
let typed_array = {
|
||||
let f32_slice = temporary_buffer.as_slice();
|
||||
let u8_slice: &[u8] = from_raw_parts(
|
||||
f32_slice.as_ptr() as *const _,
|
||||
f32_slice.len() * mem::size_of::<f32>(),
|
||||
);
|
||||
let typed_array: TypedArray<u8> = u8_slice.into();
|
||||
typed_array
|
||||
};
|
||||
let typed_array: Float32Array = temporary_buffer.as_slice().into();
|
||||
|
||||
debug_assert_eq!(temporary_buffer.len() % num_channels as usize, 0);
|
||||
debug_assert_eq!(temporary_buffer.len() % config.channels as usize, 0);
|
||||
|
||||
js!(
|
||||
var src_buffer = new Float32Array(@{typed_array}.buffer);
|
||||
var context = @{audio_ctxt};
|
||||
var buffer_size_frames = @{buffer_size_frames as u32};
|
||||
var num_channels = @{num_channels as u32};
|
||||
var sample_rate = sample_rate;
|
||||
|
||||
var buffer = context.createBuffer(num_channels, buffer_size_frames, sample_rate);
|
||||
for (var channel = 0; channel < num_channels; ++channel) {
|
||||
var buffer_content = buffer.getChannelData(channel);
|
||||
for (var i = 0; i < buffer_size_frames; ++i) {
|
||||
buffer_content[i] = src_buffer[i * num_channels + channel];
|
||||
}
|
||||
let src_buffer = Float32Array::new(typed_array.buffer().as_ref());
|
||||
let context = audio_ctxt;
|
||||
let buffer = context
|
||||
.create_buffer(
|
||||
config.channels as u32,
|
||||
buffer_size_frames as u32,
|
||||
sample_rate as f32,
|
||||
)
|
||||
.expect("Buffer could not be created");
|
||||
for channel in 0..config.channels {
|
||||
let mut buffer_content = buffer
|
||||
.get_channel_data(channel as u32)
|
||||
.expect("Should be impossible");
|
||||
for (i, buffer_content_item) in buffer_content.iter_mut().enumerate() {
|
||||
*buffer_content_item =
|
||||
src_buffer.get_index(i as u32 * config.channels as u32 + channel as u32);
|
||||
}
|
||||
}
|
||||
|
||||
var node = context.createBufferSource();
|
||||
node.buffer = buffer;
|
||||
node.connect(context.destination);
|
||||
node.start();
|
||||
);
|
||||
let node = context
|
||||
.create_buffer_source()
|
||||
.expect("The buffer source node could not be created");
|
||||
node.set_buffer(Some(&buffer));
|
||||
context
|
||||
.destination()
|
||||
.connect_with_audio_node(&node)
|
||||
.expect("Could not connect the audio node to the destination");
|
||||
node.start().expect("Could not start the audio node");
|
||||
|
||||
// TODO: handle latency better ; right now we just use setInterval with the amount of sound
|
||||
// data that is in each buffer ; this is obviously bad, and also the schedule is too tight
|
||||
// and there may be underflows
|
||||
set_timeout(
|
||||
|| audio_callback_fn::<D, E>(user_data_ptr, config, sample_format, buffer_size_frames),
|
||||
buffer_size_frames as u32 * 1000 / sample_rate,
|
||||
1000 * buffer_size_frames as i32 / sample_rate as i32,
|
||||
stream.clone().clone(),
|
||||
data_callback,
|
||||
&config,
|
||||
sample_format,
|
||||
buffer_size_frames as u32,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_timeout<D>(
|
||||
time: i32,
|
||||
stream: Stream,
|
||||
data_callback: D,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
buffer_size_frames: u32,
|
||||
) where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
{
|
||||
let window = web_sys::window().expect("Not in a window somehow?");
|
||||
window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_4(
|
||||
&Closure::once_into_js(audio_callback_fn(data_callback))
|
||||
.dyn_ref::<js_sys::Function>()
|
||||
.expect("The function was somehow not a function"),
|
||||
time,
|
||||
&stream.into(),
|
||||
&((*config).clone()).into(),
|
||||
&Closure::once_into_js(move || sample_format),
|
||||
&buffer_size_frames.into(),
|
||||
)
|
||||
.expect("The timeout could not be set");
|
||||
}
|
||||
|
||||
impl Default for Devices {
|
||||
fn default() -> Devices {
|
||||
// We produce an empty iterator if the WebAudio API isn't available.
|
||||
@@ -377,14 +401,7 @@ fn default_output_device() -> Option<Device> {
|
||||
|
||||
// Detects whether the `AudioContext` global variable is available.
|
||||
fn is_webaudio_available() -> bool {
|
||||
stdweb::initialize();
|
||||
js!(if (!AudioContext) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
})
|
||||
.try_into()
|
||||
.unwrap()
|
||||
AudioContext::new().is_ok()
|
||||
}
|
||||
|
||||
// Whether or not the given stream configuration is valid for building a stream.
|
||||
|
31
src/lib.rs
31
src/lib.rs
@@ -148,8 +148,12 @@
|
||||
// Extern crate declarations with `#[macro_use]` must unfortunately be at crate root.
|
||||
#[cfg(target_os = "emscripten")]
|
||||
#[macro_use]
|
||||
extern crate stdweb;
|
||||
extern crate wasm_bindgen;
|
||||
#[cfg(target_os = "emscripten")]
|
||||
extern crate js_sys;
|
||||
extern crate thiserror;
|
||||
#[cfg(target_os = "emscripten")]
|
||||
extern crate web_sys;
|
||||
|
||||
pub use error::*;
|
||||
pub use platform::{
|
||||
@@ -160,6 +164,8 @@ pub use samples_formats::{FromSample, Sample, SampleFormat, SizedSample, I24, I4
|
||||
use std::convert::TryInto;
|
||||
use std::ops::{Div, Mul};
|
||||
use std::time::Duration;
|
||||
#[cfg(target_os = "emscripten")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
mod error;
|
||||
mod host;
|
||||
@@ -177,6 +183,7 @@ pub type OutputDevices<I> = std::iter::Filter<I, fn(&<I as Iterator>::Item) -> b
|
||||
pub type ChannelCount = u16;
|
||||
|
||||
/// The number of samples processed per second for a single channel of audio.
|
||||
#[cfg_attr(target_os = "emscripten", wasm_bindgen)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct SampleRate(pub u32);
|
||||
|
||||
@@ -210,15 +217,33 @@ pub type FrameCount = u32;
|
||||
/// large, leading to latency issues. If low latency is desired, Fixed(BufferSize)
|
||||
/// should be used in accordance with the SupportedBufferSize range produced by
|
||||
/// the SupportedStreamConfig API.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum BufferSize {
|
||||
Default,
|
||||
Fixed(FrameCount),
|
||||
}
|
||||
|
||||
#[cfg(target_os = "emscripten")]
|
||||
impl wasm_bindgen::describe::WasmDescribe for BufferSize {
|
||||
fn describe() {}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "emscripten")]
|
||||
impl wasm_bindgen::convert::IntoWasmAbi for BufferSize {
|
||||
type Abi = wasm_bindgen::convert::WasmOption<u32>;
|
||||
fn into_abi(self) -> Self::Abi {
|
||||
match self {
|
||||
Self::Default => None,
|
||||
Self::Fixed(fc) => Some(fc),
|
||||
}
|
||||
.into_abi()
|
||||
}
|
||||
}
|
||||
|
||||
/// The set of parameters used to describe how to open a stream.
|
||||
///
|
||||
/// The sample format is omitted in favour of using a sample type.
|
||||
#[cfg_attr(target_os = "emscripten", wasm_bindgen)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct StreamConfig {
|
||||
pub channels: ChannelCount,
|
||||
@@ -267,6 +292,7 @@ pub struct SupportedStreamConfig {
|
||||
///
|
||||
/// Raw input stream callbacks receive `&Data`, while raw output stream callbacks expect `&mut
|
||||
/// Data`.
|
||||
#[cfg_attr(target_os = "emscripten", wasm_bindgen)]
|
||||
#[derive(Debug)]
|
||||
pub struct Data {
|
||||
data: *mut (),
|
||||
@@ -327,6 +353,7 @@ pub struct InputCallbackInfo {
|
||||
}
|
||||
|
||||
/// Information relevant to a single call to the user's output stream data callback.
|
||||
#[cfg_attr(target_os = "emscripten", wasm_bindgen)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OutputCallbackInfo {
|
||||
timestamp: OutputStreamTimestamp,
|
||||
|
@@ -1,8 +1,11 @@
|
||||
use std::{fmt::Display, mem};
|
||||
#[cfg(target_os = "emscripten")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
pub use dasp_sample::{FromSample, Sample, I24, I48, U24, U48};
|
||||
|
||||
/// Format that each sample has.
|
||||
#[cfg_attr(target_os = "emscripten", wasm_bindgen)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum SampleFormat {
|
||||
|
Reference in New Issue
Block a user