diff --git a/Cargo.toml b/Cargo.toml
index ecf4cf002..52432757c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,10 +10,22 @@ edition = "2021"
rust-version = "1.70"
[features]
+default = []
asio = ["asio-sys", "num-traits"] # Only available on Windows. See README for setup instructions.
oboe-shared-stdcxx = ["oboe/shared-stdcxx"] # Only available on Android. See README for what it does.
+# Only available on web when atomics are enabled. See README for what it does.
+web_audio_worklet = [
+ "wasm-bindgen-futures",
+ "web-sys/Blob",
+ "web-sys/BlobPropertyBag",
+ "web-sys/Url",
+ "web-sys/AudioWorklet",
+ "web-sys/AudioWorkletNode",
+ "web-sys/AudioWorkletNodeOptions",
+]
[dependencies]
+wasm-bindgen-futures = {version = "0.4.33", optional = true}
dasp_sample = "0.11"
[dev-dependencies]
diff --git a/examples/web-audio-worklet-beep/.gitignore b/examples/web-audio-worklet-beep/.gitignore
new file mode 100644
index 000000000..25545b464
--- /dev/null
+++ b/examples/web-audio-worklet-beep/.gitignore
@@ -0,0 +1,3 @@
+Cargo.lock
+/dist
+/target
diff --git a/examples/web-audio-worklet-beep/Cargo.toml b/examples/web-audio-worklet-beep/Cargo.toml
new file mode 100644
index 000000000..783902777
--- /dev/null
+++ b/examples/web-audio-worklet-beep/Cargo.toml
@@ -0,0 +1,40 @@
+[package]
+name = "web-audio-worklet-beep"
+description = "cpal beep example for WebAssembly on an AudioWorklet"
+version = "0.1.0"
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib"]
+
+[profile.release]
+# This makes the compiled code faster and smaller, but it makes compiling slower,
+# so it's only enabled in release mode.
+lto = true
+
+[features]
+# If you uncomment this line, it will enable `wee_alloc`:
+#default = ["wee_alloc"]
+
+[dependencies]
+cpal = { path = "../..", features = ["wasm-bindgen", "web_audio_worklet"] }
+# `gloo` is a utility crate which improves ergonomics over direct `web-sys` usage.
+gloo = "0.11.0"
+# The `wasm-bindgen` crate provides the bare minimum functionality needed
+# to interact with JavaScript.
+wasm-bindgen = "0.2.45"
+
+# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
+# compared to the default allocator's ~10K. However, it is slower than the default
+# allocator, so it's not enabled by default.
+wee_alloc = { version = "0.4.2", optional = true }
+
+# The `console_error_panic_hook` crate provides better debugging of panics by
+# logging them with `console.error`.
+console_error_panic_hook = "0.1.5"
+
+# The `web-sys` crate allows you to interact with the various browser APIs,
+# like the DOM.
+[dependencies.web-sys]
+version = "0.3.22"
+features = ["console", "MouseEvent"]
diff --git a/examples/web-audio-worklet-beep/README.md b/examples/web-audio-worklet-beep/README.md
new file mode 100644
index 000000000..7eacb6ee0
--- /dev/null
+++ b/examples/web-audio-worklet-beep/README.md
@@ -0,0 +1,39 @@
+## How to install
+
+This example requires a nightly version of Rust to enable WebAssembly atomics and to recompile the standard library with atomics enabled.
+
+Note the flags set to configure that in .cargo/config.toml.
+
+This allows Rust to used shared memory and have the audio thread directly read / write to shared memory like a native platform.
+
+To use shared memory the browser requires a specific 'CORS' configuration on the server-side.
+
+Note the flags set to configure that in Trunk.toml.
+
+[trunk](https://trunkrs.dev/) is used to build and serve the example.
+
+```sh
+cargo install --locked trunk
+# -- or --
+cargo binstall trunk
+```
+
+## How to run in debug mode
+
+```sh
+# Builds the project and opens it in a new browser tab. Auto-reloads when the project changes.
+trunk serve --open
+```
+
+## How to build in release mode
+
+```sh
+# Builds the project in release mode and places it into the `dist` folder.
+trunk build --release
+```
+
+## What does each file do?
+
+* `Cargo.toml` contains the standard Rust metadata. You put your Rust dependencies in here. You must change this file with your details (name, description, version, authors, categories)
+
+* The `src` folder contains your Rust code.
diff --git a/examples/web-audio-worklet-beep/Trunk.toml b/examples/web-audio-worklet-beep/Trunk.toml
new file mode 100644
index 000000000..bb27e6853
--- /dev/null
+++ b/examples/web-audio-worklet-beep/Trunk.toml
@@ -0,0 +1,9 @@
+[build]
+target = "index.html"
+dist = "dist"
+
+[serve.headers]
+# see ./assets/_headers for more documentation
+"cross-origin-embedder-policy" = "require-corp"
+"cross-origin-opener-policy" = "same-origin"
+"cross-origin-resource-policy" = "same-site"
diff --git a/examples/web-audio-worklet-beep/index.html b/examples/web-audio-worklet-beep/index.html
new file mode 100644
index 000000000..6a748f05b
--- /dev/null
+++ b/examples/web-audio-worklet-beep/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ cpal AudioWorklet beep example
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/web-audio-worklet-beep/src/lib.rs b/examples/web-audio-worklet-beep/src/lib.rs
new file mode 100644
index 000000000..36fd2e63d
--- /dev/null
+++ b/examples/web-audio-worklet-beep/src/lib.rs
@@ -0,0 +1,114 @@
+use std::{cell::Cell, rc::Rc};
+
+use cpal::{
+ traits::{DeviceTrait, HostTrait, StreamTrait},
+ Stream,
+};
+use wasm_bindgen::prelude::*;
+use web_sys::console;
+
+// When the `wee_alloc` feature is enabled, this uses `wee_alloc` as the global
+// allocator.
+//
+// If you don't want to use `wee_alloc`, you can safely delete this.
+#[cfg(feature = "wee_alloc")]
+#[global_allocator]
+static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
+
+// This is like the `main` function, except for JavaScript.
+#[wasm_bindgen(start)]
+pub fn main_js() -> Result<(), JsValue> {
+ // This provides better error messages in debug mode.
+ // It's disabled in release mode, so it doesn't bloat up the file size.
+ #[cfg(debug_assertions)]
+ console_error_panic_hook::set_once();
+
+ let document = gloo::utils::document();
+ let play_button = document.get_element_by_id("play").unwrap();
+ let stop_button = document.get_element_by_id("stop").unwrap();
+
+ // stream needs to be referenced from the "play" and "stop" closures
+ let stream = Rc::new(Cell::new(None));
+
+ // set up play button
+ {
+ let stream = stream.clone();
+ let closure = Closure::::new(move |_event: web_sys::MouseEvent| {
+ stream.set(Some(beep()));
+ });
+ play_button
+ .add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?;
+ closure.forget();
+ }
+
+ // set up stop button
+ {
+ let closure = Closure::::new(move |_event: web_sys::MouseEvent| {
+ // stop the stream by dropping it
+ stream.take();
+ });
+ stop_button
+ .add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?;
+ closure.forget();
+ }
+
+ Ok(())
+}
+
+fn beep() -> Stream {
+ let host = cpal::host_from_id(cpal::HostId::WebAudioWorklet)
+ .expect("WebAudioWorklet host not available");
+
+ let device = host
+ .default_output_device()
+ .expect("failed to find a default output device");
+ let config = device.default_output_config().unwrap();
+
+ match config.sample_format() {
+ cpal::SampleFormat::F32 => run::(&device, &config.into()),
+ cpal::SampleFormat::I16 => run::(&device, &config.into()),
+ cpal::SampleFormat::U16 => run::(&device, &config.into()),
+ _ => panic!("unsupported sample format"),
+ }
+}
+
+fn run(device: &cpal::Device, config: &cpal::StreamConfig) -> Stream
+where
+ T: cpal::Sample + cpal::SizedSample + cpal::FromSample,
+{
+ let sample_rate = config.sample_rate.0 as f32;
+ let channels = config.channels as usize;
+
+ // Produce a sinusoid of maximum amplitude.
+ let mut sample_clock = 0f32;
+ let mut next_value = move || {
+ sample_clock = (sample_clock + 1.0) % sample_rate;
+ (sample_clock * 440.0 * 2.0 * 3.141592 / sample_rate).sin()
+ };
+
+ let err_fn = |err| console::error_1(&format!("an error occurred on stream: {}", err).into());
+
+ let stream = device
+ .build_output_stream(
+ config,
+ move |data: &mut [T], _| write_data(data, channels, &mut next_value),
+ err_fn,
+ None,
+ )
+ .unwrap();
+ stream.play().unwrap();
+ stream
+}
+
+fn write_data(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32)
+where
+ T: cpal::Sample + cpal::FromSample,
+{
+ for frame in output.chunks_mut(channels) {
+ let sample = next_sample();
+ let value = T::from_sample::(sample);
+ for sample in frame.iter_mut() {
+ *sample = value;
+ }
+ }
+}
diff --git a/src/host/mod.rs b/src/host/mod.rs
index 8de06cbe0..7be297573 100644
--- a/src/host/mod.rs
+++ b/src/host/mod.rs
@@ -26,5 +26,12 @@ pub(crate) mod null;
pub(crate) mod oboe;
#[cfg(windows)]
pub(crate) mod wasapi;
+#[cfg(all(
+ target_arch = "wasm32",
+ feature = "wasm-bindgen",
+ feature = "web_audio_worklet",
+ target_feature = "atomics"
+))]
+pub(crate) mod web_audio_worklet;
#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
pub(crate) mod webaudio;
diff --git a/src/host/web_audio_worklet/dependent_module.rs b/src/host/web_audio_worklet/dependent_module.rs
new file mode 100644
index 000000000..8fd14d58d
--- /dev/null
+++ b/src/host/web_audio_worklet/dependent_module.rs
@@ -0,0 +1,50 @@
+// This file is taken from here: https://github.com/rustwasm/wasm-bindgen/blob/main/examples/wasm-audio-worklet/src/dependent_module.rs
+// See this issue for a further explanation of what this file does: https://github.com/rustwasm/wasm-bindgen/issues/3019
+
+use js_sys::{wasm_bindgen, Array, JsString};
+use wasm_bindgen::prelude::*;
+use web_sys::{Blob, BlobPropertyBag, Url};
+
+// This is a not-so-clean approach to get the current bindgen ES module URL
+// in Rust. This will fail at run time on bindgen targets not using ES modules.
+#[wasm_bindgen]
+extern "C" {
+ #[wasm_bindgen]
+ type ImportMeta;
+
+ #[wasm_bindgen(method, getter)]
+ fn url(this: &ImportMeta) -> JsString;
+
+ #[wasm_bindgen(thread_local_v2, js_namespace = import, js_name = meta)]
+ static IMPORT_META: ImportMeta;
+}
+
+pub fn on_the_fly(code: &str) -> Result {
+ // Generate the import of the bindgen ES module, assuming `--target web`.
+ let header = format!(
+ "import init, * as bindgen from '{}';\n\n",
+ IMPORT_META.with(ImportMeta::url),
+ );
+
+ let options = BlobPropertyBag::new();
+ options.set_type("text/javascript");
+ Url::create_object_url_with_blob(&Blob::new_with_str_sequence_and_options(
+ &Array::of2(&JsValue::from(header.as_str()), &JsValue::from(code)),
+ &options,
+ )?)
+}
+
+// dependent_module! takes a local file name to a JS module as input and
+// returns a URL to a slightly modified module in run time. This modified module
+// has an additional import statement in the header that imports the current
+// bindgen JS module under the `bindgen` alias, and the separate init function.
+// How this URL is produced does not matter for the macro user. on_the_fly
+// creates a blob URL in run time. A better, more sophisticated solution
+// would add wasm_bindgen support to put such a module in pkg/ during build time
+// and return a URL to this file instead (described in #3019).
+#[macro_export]
+macro_rules! dependent_module {
+ ($file_name:expr) => {
+ $crate::host::web_audio_worklet::dependent_module::on_the_fly(include_str!($file_name))
+ };
+}
diff --git a/src/host/web_audio_worklet/mod.rs b/src/host/web_audio_worklet/mod.rs
new file mode 100644
index 000000000..2e029f7a9
--- /dev/null
+++ b/src/host/web_audio_worklet/mod.rs
@@ -0,0 +1,383 @@
+mod dependent_module;
+use js_sys::wasm_bindgen;
+
+use crate::dependent_module;
+use wasm_bindgen::prelude::*;
+
+use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
+use crate::{
+ BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError,
+ DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError,
+ SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize,
+ SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError,
+};
+use std::time::Duration;
+
+/// Content is false if the iterator is empty.
+pub struct Devices(bool);
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Device;
+
+pub struct Host;
+
+pub struct Stream {
+ audio_context: web_sys::AudioContext,
+}
+
+pub type SupportedInputConfigs = ::std::vec::IntoIter;
+pub type SupportedOutputConfigs = ::std::vec::IntoIter;
+
+const MIN_CHANNELS: u16 = 1;
+const MAX_CHANNELS: u16 = 32;
+const MIN_SAMPLE_RATE: SampleRate = SampleRate(8_000);
+const MAX_SAMPLE_RATE: SampleRate = SampleRate(96_000);
+const DEFAULT_SAMPLE_RATE: SampleRate = SampleRate(44_100);
+const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;
+
+impl Host {
+ pub fn new() -> Result {
+ Ok(Host)
+ }
+}
+
+impl HostTrait for Host {
+ type Devices = Devices;
+ type Device = Device;
+
+ fn is_available() -> bool {
+ if let Ok(audio_context_is_defined) = js_sys::eval("typeof AudioWorklet !== 'undefined'") {
+ audio_context_is_defined.as_bool().unwrap()
+ } else {
+ false
+ }
+ }
+
+ fn devices(&self) -> Result {
+ Devices::new()
+ }
+
+ fn default_input_device(&self) -> Option {
+ // TODO
+ None
+ }
+
+ fn default_output_device(&self) -> Option {
+ Some(Device)
+ }
+}
+
+impl Devices {
+ fn new() -> Result {
+ Ok(Self::default())
+ }
+}
+
+impl DeviceTrait for Device {
+ type SupportedInputConfigs = SupportedInputConfigs;
+ type SupportedOutputConfigs = SupportedOutputConfigs;
+ type Stream = Stream;
+
+ #[inline]
+ fn name(&self) -> Result {
+ Ok("Default Device".to_owned())
+ }
+
+ #[inline]
+ fn supported_input_configs(
+ &self,
+ ) -> Result {
+ // TODO
+ Ok(Vec::new().into_iter())
+ }
+
+ #[inline]
+ fn supported_output_configs(
+ &self,
+ ) -> Result {
+ let buffer_size = SupportedBufferSize::Unknown;
+
+ // In actuality the number of supported channels cannot be fully known until
+ // the browser attempts to initialized the AudioWorklet.
+
+ let configs: Vec<_> = (MIN_CHANNELS..=MAX_CHANNELS)
+ .map(|channels| SupportedStreamConfigRange {
+ channels,
+ min_sample_rate: MIN_SAMPLE_RATE,
+ max_sample_rate: MAX_SAMPLE_RATE,
+ buffer_size: buffer_size.clone(),
+ sample_format: SUPPORTED_SAMPLE_FORMAT,
+ })
+ .collect();
+ Ok(configs.into_iter())
+ }
+
+ #[inline]
+ fn default_input_config(&self) -> Result {
+ // TODO
+ Err(DefaultStreamConfigError::StreamTypeNotSupported)
+ }
+
+ #[inline]
+ fn default_output_config(&self) -> Result {
+ const EXPECT: &str = "expected at least one valid webaudio stream config";
+ let config = self
+ .supported_output_configs()
+ .expect(EXPECT)
+ .max_by(|a, b| a.cmp_default_heuristics(b))
+ .unwrap()
+ .with_sample_rate(DEFAULT_SAMPLE_RATE);
+
+ Ok(config)
+ }
+
+ fn build_input_stream_raw(
+ &self,
+ _config: &StreamConfig,
+ _sample_format: SampleFormat,
+ _data_callback: D,
+ _error_callback: E,
+ _timeout: Option,
+ ) -> Result
+ where
+ D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
+ E: FnMut(StreamError) + Send + 'static,
+ {
+ // TODO
+ Err(BuildStreamError::StreamConfigNotSupported)
+ }
+
+ /// Create an output stream.
+ fn build_output_stream_raw(
+ &self,
+ config: &StreamConfig,
+ sample_format: SampleFormat,
+ mut data_callback: D,
+ mut error_callback: E,
+ _timeout: Option,
+ ) -> Result
+ where
+ D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
+ E: FnMut(StreamError) + Send + 'static,
+ {
+ if !valid_config(config, sample_format) {
+ return Err(BuildStreamError::StreamConfigNotSupported);
+ }
+
+ let config = config.clone();
+
+ let stream_opts = web_sys::AudioContextOptions::new();
+ stream_opts.set_sample_rate(config.sample_rate.0 as f32);
+
+ let audio_context = web_sys::AudioContext::new_with_context_options(&stream_opts).map_err(
+ |err| -> BuildStreamError {
+ let description = format!("{:?}", err);
+ let err = BackendSpecificError { description };
+ err.into()
+ },
+ )?;
+
+ let destination = audio_context.destination();
+
+ // If possible, set the destination's channel_count to the given config.channel.
+ // If not, fallback on the default destination channel_count to keep previous behavior
+ // and do not return an error.
+ if config.channels as u32 <= destination.max_channel_count() {
+ destination.set_channel_count(config.channels as u32);
+ }
+
+ let ctx = audio_context.clone();
+ wasm_bindgen_futures::spawn_local(async move {
+ let result: Result<(), JsValue> = (async move || {
+ let mod_url = dependent_module!("worklet.js")?;
+ wasm_bindgen_futures::JsFuture::from(ctx.audio_worklet()?.add_module(&mod_url)?)
+ .await?;
+
+ let options = web_sys::AudioWorkletNodeOptions::new();
+
+ let js_array = js_sys::Array::new();
+ js_array.push(&JsValue::from_f64(destination.channel_count() as _));
+
+ options.set_output_channel_count(&js_array);
+ options.set_number_of_inputs(0);
+
+ options.set_processor_options(Some(&js_sys::Array::of3(
+ &wasm_bindgen::module(),
+ &wasm_bindgen::memory(),
+ &WasmAudioProcessor::new(Box::new(
+ move |interleaved_data, frame_size, sample_rate, now| {
+ let data = interleaved_data.as_mut_ptr() as *mut ();
+ let mut data = unsafe {
+ Data::from_parts(data, interleaved_data.len(), sample_format)
+ };
+
+ let callback = crate::StreamInstant::from_secs_f64(now);
+
+ let buffer_duration =
+ frames_to_duration(frame_size as _, SampleRate(sample_rate as u32));
+ 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_callback)(&mut data, &info);
+ },
+ ))
+ .pack()
+ .into(),
+ )));
+ // This name 'CpalProcessor' must match the name registered in worklet.js
+ let audio_worklet_node =
+ web_sys::AudioWorkletNode::new_with_options(&ctx, "CpalProcessor", &options)?;
+
+ audio_worklet_node.connect_with_audio_node(&destination)?;
+ Ok(())
+ })()
+ .await;
+
+ if let Err(e) = result {
+ let description = if let Some(string_value) = e.as_string() {
+ string_value
+ } else {
+ format!("Browser error initializing stream: {:?}", e)
+ };
+
+ error_callback(StreamError::BackendSpecific {
+ err: BackendSpecificError { description },
+ })
+ }
+ });
+
+ Ok(Stream { audio_context })
+ }
+}
+
+impl StreamTrait for Stream {
+ fn play(&self) -> Result<(), PlayStreamError> {
+ match self.audio_context.resume() {
+ Ok(_) => Ok(()),
+ Err(err) => {
+ let description = format!("{:?}", err);
+ let err = BackendSpecificError { description };
+ Err(err.into())
+ }
+ }
+ }
+
+ fn pause(&self) -> Result<(), PauseStreamError> {
+ match self.audio_context.suspend() {
+ Ok(_) => Ok(()),
+ Err(err) => {
+ let description = format!("{:?}", err);
+ let err = BackendSpecificError { description };
+ Err(err.into())
+ }
+ }
+ }
+}
+
+impl Drop for Stream {
+ fn drop(&mut self) {
+ let _ = self.audio_context.close();
+ }
+}
+
+impl Default for Devices {
+ fn default() -> Devices {
+ Devices(true)
+ }
+}
+
+impl Iterator for Devices {
+ type Item = Device;
+ #[inline]
+ fn next(&mut self) -> Option {
+ if self.0 {
+ self.0 = false;
+ Some(Device)
+ } else {
+ None
+ }
+ }
+}
+
+// Whether or not the given stream configuration is valid for building a stream.
+fn valid_config(conf: &StreamConfig, sample_format: SampleFormat) -> bool {
+ conf.channels <= MAX_CHANNELS
+ && conf.channels >= MIN_CHANNELS
+ && conf.sample_rate <= MAX_SAMPLE_RATE
+ && conf.sample_rate >= MIN_SAMPLE_RATE
+ && sample_format == SUPPORTED_SAMPLE_FORMAT
+}
+
+// Convert the given duration in frames at the given sample rate to a `std::time::Duration`.
+fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration {
+ let secsf = frames as f64 / rate.0 as f64;
+ let secs = secsf as u64;
+ let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32;
+ std::time::Duration::new(secs, nanos)
+}
+
+/// WasmAudioProcessor provides an interface for the Javascript code
+/// running in the AudioWorklet to interact with Rust.
+#[wasm_bindgen]
+pub struct WasmAudioProcessor {
+ #[wasm_bindgen(skip)]
+ interleaved_buffer: Vec,
+ #[wasm_bindgen(skip)]
+ // Passes in an interleaved scratch buffer, frame size, sample rate, and current time.
+ callback: Box,
+}
+
+impl WasmAudioProcessor {
+ pub fn new(callback: Box) -> Self {
+ Self {
+ interleaved_buffer: Vec::new(),
+ callback,
+ }
+ }
+}
+
+#[wasm_bindgen]
+impl WasmAudioProcessor {
+ pub fn process(
+ &mut self,
+ channels: u32,
+ frame_size: u32,
+ sample_rate: u32,
+ current_time: f64,
+ ) -> u32 {
+ let frame_size = frame_size as usize;
+
+ // Ensure there's enough space in the output buffer
+ // This likely only occurs once, or very few times.
+ let interleaved_buffer_size = channels as usize * frame_size;
+ self.interleaved_buffer.resize(
+ interleaved_buffer_size.max(self.interleaved_buffer.len()),
+ 0.0,
+ );
+
+ (self.callback)(
+ &mut self.interleaved_buffer[..interleaved_buffer_size],
+ frame_size as u32,
+ sample_rate,
+ current_time,
+ );
+
+ // Returns a pointer to the raw interleaved buffer to Javascript so
+ // it can deinterleave it into the output buffers.
+ //
+ // Deinterleaving is done on the Javascript side because it's simpler and it may be faster.
+ // Doing it this way avoids an extra copy and the JS deinterleaving code
+ // is likely heavily optimized by the browser's JS engine,
+ // although I have not tested that assumption.
+ self.interleaved_buffer.as_mut_ptr() as _
+ }
+
+ pub fn pack(self) -> usize {
+ Box::into_raw(Box::new(self)) as usize
+ }
+ pub unsafe fn unpack(val: usize) -> Self {
+ *Box::from_raw(val as *mut _)
+ }
+}
diff --git a/src/host/web_audio_worklet/worklet.js b/src/host/web_audio_worklet/worklet.js
new file mode 100644
index 000000000..234707a8a
--- /dev/null
+++ b/src/host/web_audio_worklet/worklet.js
@@ -0,0 +1,30 @@
+registerProcessor("CpalProcessor", class WasmProcessor extends AudioWorkletProcessor {
+ constructor(options) {
+ super();
+ let [module, memory, handle] = options.processorOptions;
+ bindgen.initSync({ module, memory });
+ this.processor = bindgen.WasmAudioProcessor.unpack(handle);
+ this.wasm_memory = new Float32Array(memory.buffer);
+ }
+ process(inputs, outputs) {
+ const channels = outputs[0];
+ const channels_count = channels.length;
+ const frame_size = channels[0].length;
+
+ const interleaved_ptr = this.processor.process(channels_count, frame_size, sampleRate, currentTime);
+
+ const FLOAT32_SIZE_BYTES = 4;
+ const interleaved_start = interleaved_ptr / FLOAT32_SIZE_BYTES;
+ const interleaved = this.wasm_memory.subarray(interleaved_start, interleaved_start + channels_count * frame_size);
+
+ for (let ch = 0; ch < channels_count; ch++) {
+ const channel = channels[ch];
+
+ for (let i = 0, j = ch; i < frame_size; i++, j += channels_count) {
+ channel[i] = interleaved[j];
+ }
+ }
+
+ return true;
+ }
+});
\ No newline at end of file
diff --git a/src/platform/mod.rs b/src/platform/mod.rs
index 65d77ca40..18f4e0894 100644
--- a/src/platform/mod.rs
+++ b/src/platform/mod.rs
@@ -662,7 +662,15 @@ mod platform_impl {
SupportedOutputConfigs as WebAudioSupportedOutputConfigs,
};
- impl_platform_host!(WebAudio webaudio "WebAudio");
+ #[cfg(feature = "web_audio_worklet")]
+ pub use crate::host::webaudio::{
+ Device as WebAudioWorkletDevice, Devices as WebAudioWorkletDevices,
+ Host as WebAudioWorkletHost, Stream as WebAudioWorkletStream,
+ SupportedInputConfigs as WebAudioWorkletSupportedInputConfigs,
+ SupportedOutputConfigs as WebAudioWorkletSupportedOutputConfigs,
+ };
+
+ impl_platform_host!(#[cfg(feature = "web_audio_worklet")] WebAudioWorklet web_audio_worklet "WebAudioWorklet", WebAudio webaudio "WebAudio");
/// The default host for the current compilation target platform.
pub fn default_host() -> Host {