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 {