use std::{
fmt,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
time::Duration,
};
use js_sys::wasm_bindgen;
use wasm_bindgen::prelude::*;
use crate::{
host::frames_to_duration,
traits::{DeviceTrait, HostTrait, StreamTrait},
BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection,
DeviceId, Error, ErrorKind, FrameCount, InputCallbackInfo, OutputCallbackInfo,
OutputStreamTimestamp, SampleFormat, SampleRate, StreamConfig, StreamInstant,
SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange,
};
mod dependent_module;
use crate::dependent_module;
pub struct Devices(bool);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Device;
impl fmt::Display for Device {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let desc = self.description().map_err(|_| fmt::Error)?;
f.write_str(desc.name())
}
}
pub struct Host;
pub struct Stream {
audio_context: web_sys::AudioContext,
buffer_size_frames: Arc<AtomicU64>,
}
pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs};
const MIN_CHANNELS: ChannelCount = 1;
const MAX_CHANNELS: ChannelCount = 32;
const MIN_SAMPLE_RATE: SampleRate = 3_000;
const MAX_SAMPLE_RATE: SampleRate = 768_000;
const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;
const DEFAULT_RENDER_SIZE: u64 = 128;
fn render_quantum_size_supported() -> bool {
(|| -> Option<bool> {
let global = js_sys::global();
let ctor = js_sys::Reflect::get(&global, &JsValue::from("AudioContext")).ok()?;
let proto = js_sys::Reflect::get(&ctor, &JsValue::from("prototype")).ok()?;
js_sys::Reflect::has(&proto, &JsValue::from("renderQuantumSize")).ok()
})()
.unwrap_or(false)
}
fn supported_render_quantum_range(sample_rate: SampleRate) -> SupportedBufferSize {
if render_quantum_size_supported() {
SupportedBufferSize::Range {
min: 1,
max: sample_rate.saturating_mul(6),
}
} else {
SupportedBufferSize::Range {
min: DEFAULT_RENDER_SIZE as FrameCount,
max: DEFAULT_RENDER_SIZE as FrameCount,
}
}
}
impl Host {
pub fn new() -> Result<Self, Error> {
if Self::is_available() {
Ok(Host)
} else {
Err(Error::with_message(
ErrorKind::HostUnavailable,
"AudioWorklet is not available",
))
}
}
}
impl HostTrait for Host {
type Devices = Devices;
type Device = Device;
fn is_available() -> bool {
if let Some(window) = web_sys::window() {
let has_audio_worklet =
js_sys::Reflect::has(&window, &JsValue::from_str("AudioWorklet")).unwrap_or(false);
let cross_origin_isolated =
js_sys::Reflect::get(&window, &JsValue::from_str("crossOriginIsolated"))
.ok()
.and_then(|v| v.as_bool())
.unwrap_or(false);
has_audio_worklet && cross_origin_isolated
} else {
false
}
}
fn devices(&self) -> Result<Self::Devices, Error> {
Devices::new()
}
fn default_input_device(&self) -> Option<Self::Device> {
None
}
fn default_output_device(&self) -> Option<Self::Device> {
if Self::is_available() {
Some(Device)
} else {
None
}
}
}
impl Devices {
fn new() -> Result<Self, Error> {
Ok(Devices(Host::is_available()))
}
}
impl DeviceTrait for Device {
type SupportedInputConfigs = SupportedInputConfigs;
type SupportedOutputConfigs = SupportedOutputConfigs;
type Stream = Stream;
fn description(&self) -> Result<DeviceDescription, Error> {
Ok(DeviceDescriptionBuilder::new("Default Device")
.direction(DeviceDirection::Output)
.build())
}
fn id(&self) -> Result<DeviceId, Error> {
Ok(DeviceId::new(
crate::platform::HostId::AudioWorklet,
"default",
))
}
fn supported_input_configs(&self) -> Result<Self::SupportedInputConfigs, Error> {
Ok(Vec::new().into_iter())
}
fn supported_output_configs(&self) -> Result<Self::SupportedOutputConfigs, Error> {
let configs: Vec<_> = (MIN_CHANNELS..=MAX_CHANNELS)
.flat_map(|channels| {
crate::COMMON_SAMPLE_RATES
.iter()
.copied()
.filter(|&r| (MIN_SAMPLE_RATE..=MAX_SAMPLE_RATE).contains(&r))
.map(move |rate| SupportedStreamConfigRange {
channels,
min_sample_rate: rate,
max_sample_rate: rate,
buffer_size: supported_render_quantum_range(rate),
sample_format: SUPPORTED_SAMPLE_FORMAT,
})
})
.collect();
Ok(configs.into_iter())
}
fn default_input_config(&self) -> Result<SupportedStreamConfig, Error> {
Err(Error::with_message(
ErrorKind::UnsupportedOperation,
"Device does not support input",
))
}
fn default_output_config(&self) -> Result<SupportedStreamConfig, Error> {
let range = self
.supported_output_configs()?
.max_by(|a, b| a.cmp_default_heuristics(b))
.ok_or_else(|| {
Error::with_message(
ErrorKind::UnsupportedConfig,
"No supported output configuration",
)
})?;
let config = range
.try_with_standard_sample_rate()
.unwrap_or_else(|| range.with_max_sample_rate());
Ok(config)
}
fn build_input_stream_raw<D, E>(
&self,
_config: StreamConfig,
_sample_format: SampleFormat,
_data_callback: D,
_error_callback: E,
_timeout: Option<Duration>,
) -> Result<Self::Stream, Error>
where
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
E: FnMut(Error) + Send + 'static,
{
Err(Error::with_message(
ErrorKind::UnsupportedOperation,
"Device does not support input",
))
}
fn build_output_stream_raw<D, E>(
&self,
config: StreamConfig,
sample_format: SampleFormat,
mut data_callback: D,
mut error_callback: E,
_timeout: Option<Duration>,
) -> Result<Self::Stream, Error>
where
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
E: FnMut(Error) + Send + 'static,
{
crate::validate_stream_config(&config)?;
if config.channels > MAX_CHANNELS {
return Err(Error::with_message(
ErrorKind::UnsupportedConfig,
format!(
"Channel count {} exceeds the maximum of {MAX_CHANNELS}",
config.channels
),
));
}
if sample_format != SUPPORTED_SAMPLE_FORMAT {
return Err(Error::with_message(
ErrorKind::UnsupportedConfig,
format!(
"Sample format {sample_format} is not supported; required format is {SUPPORTED_SAMPLE_FORMAT}"
),
));
}
if !(MIN_SAMPLE_RATE..=MAX_SAMPLE_RATE).contains(&config.sample_rate) {
return Err(Error::with_message(
ErrorKind::UnsupportedConfig,
format!(
"Sample rate {} Hz is not in the supported range {MIN_SAMPLE_RATE}..={MAX_SAMPLE_RATE} Hz",
config.sample_rate
),
));
}
if let BufferSize::Fixed(n) = config.buffer_size {
if let SupportedBufferSize::Range { min, max } =
supported_render_quantum_range(config.sample_rate)
{
if !(min..=max).contains(&n) {
return Err(Error::with_message(
ErrorKind::UnsupportedConfig,
format!(
"Buffer size {n} is not in the supported render quantum range {min}..={max}"
),
));
}
}
}
let stream_opts = web_sys::AudioContextOptions::new();
stream_opts.set_sample_rate(config.sample_rate as f32);
if let BufferSize::Fixed(n) = config.buffer_size {
let _ = js_sys::Reflect::set(
stream_opts.as_ref(),
&JsValue::from_str("renderSizeHint"),
&JsValue::from_f64(n as f64),
);
}
let audio_context =
web_sys::AudioContext::new_with_context_options(&stream_opts).map_err(|_| {
Error::with_message(
ErrorKind::UnsupportedConfig,
"Failed to create audio context",
)
})?;
let destination = audio_context.destination();
let actual_render_quantum =
js_sys::Reflect::get(audio_context.as_ref(), &JsValue::from("renderQuantumSize"))
.ok()
.and_then(|v| v.as_f64())
.map(|v| v as u64);
if config.channels as u32 > destination.max_channel_count() {
return Err(Error::with_message(
ErrorKind::UnsupportedConfig,
format!(
"Channel count {} exceeds the destination's maximum of {}",
config.channels,
destination.max_channel_count()
),
));
}
destination.set_channel_count(config.channels as u32);
let initial_quantum = actual_render_quantum.unwrap_or(match config.buffer_size {
BufferSize::Fixed(n) => n as u64,
BufferSize::Default => DEFAULT_RENDER_SIZE,
});
let buffer_size_frames = Arc::new(AtomicU64::new(initial_quantum));
let buffer_size_frames_cb = buffer_size_frames.clone();
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);
let base_latency_secs =
js_sys::Reflect::get(ctx.as_ref(), &JsValue::from("baseLatency"))
.ok()
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let output_latency_secs =
js_sys::Reflect::get(ctx.as_ref(), &JsValue::from("outputLatency"))
.ok()
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let total_output_latency_secs = {
let sum = base_latency_secs + output_latency_secs;
if sum.is_finite() {
sum.max(0.0)
} else {
0.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| {
buffer_size_frames_cb.store(frame_size as u64, Ordering::Relaxed);
let data = interleaved_data.as_mut_ptr() as *mut ();
let mut data = unsafe {
Data::from_parts(data, interleaved_data.len(), sample_format)
};
let callback = StreamInstant::from_secs_f64(now);
let buffer_duration =
frames_to_duration(frame_size as FrameCount, sample_rate);
let playback = callback
+ (buffer_duration
+ Duration::from_secs_f64(total_output_latency_secs));
let timestamp = OutputStreamTimestamp { callback, playback };
let info = OutputCallbackInfo { timestamp };
(data_callback)(&mut data, &info);
},
))
.pack()
.into(),
)));
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(err) = result {
let message = err
.as_string()
.unwrap_or_else(|| "Failed to initialize audio worklet".to_string());
error_callback(Error::with_message(
ErrorKind::UnsupportedOperation,
message,
))
}
});
Ok(Self::Stream {
audio_context,
buffer_size_frames,
})
}
}
impl StreamTrait for Stream {
fn buffer_size(&self) -> Result<FrameCount, Error> {
Ok(self.buffer_size_frames.load(Ordering::Relaxed) as FrameCount)
}
fn play(&self) -> Result<(), Error> {
match self.audio_context.resume() {
Ok(_) => Ok(()),
Err(_) => Err(Error::with_message(
ErrorKind::DeviceNotAvailable,
"Failed to resume audio context",
)),
}
}
fn pause(&self) -> Result<(), Error> {
match self.audio_context.suspend() {
Ok(_) => Ok(()),
Err(_) => Err(Error::with_message(
ErrorKind::DeviceNotAvailable,
"Failed to suspend audio context",
)),
}
}
fn now(&self) -> StreamInstant {
StreamInstant::from_secs_f64(self.audio_context.current_time())
}
}
impl Drop for Stream {
fn drop(&mut self) {
let _ = self.audio_context.close();
}
}
impl Iterator for Devices {
type Item = Device;
fn next(&mut self) -> Option<Self::Item> {
if self.0 {
self.0 = false;
Some(Device)
} else {
None
}
}
}
type AudioProcessorCallback = Box<dyn FnMut(&mut [f32], u32, u32, f64)>;
#[wasm_bindgen]
pub struct WasmAudioProcessor {
interleaved_buffer: Vec<f32>,
callback: AudioProcessorCallback,
}
impl WasmAudioProcessor {
pub fn new(callback: AudioProcessorCallback) -> 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;
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,
);
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 _)
}
}