nandi/jolt-nativepublic Fork 0
cfd3e3677bed92e80cfe447469a249cfdc0b1401
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

mod.rs · 576 lines · 21.1 KBRust Blame HistoryRaw
Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago1//! Web Audio backend implementation.
2//!
3//! Default backend on WebAssembly.
4
5extern crate js_sys;
6extern crate wasm_bindgen;
7extern crate web_sys;
8
9use self::wasm_bindgen::prelude::*;
10use self::wasm_bindgen::JsCast;
11use self::web_sys::{AudioContext, AudioContextOptions};
12use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
13use crate::{
14 BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError,
15 DeviceDescription, DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError,
16 DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError,
17 SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize,
18 SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError,
19};
20use std::ops::DerefMut;
21use std::sync::{Arc, Mutex, RwLock};
22use std::time::Duration;
23
24/// Type alias for shared closure handles used in audio callbacks
25type ClosureHandle = Arc<RwLock<Option<Closure<dyn FnMut()>>>>;
26
27/// Content is false if the iterator is empty.
28pub struct Devices(bool);
29
30#[derive(Clone, Debug, PartialEq, Eq, Hash)]
31pub struct Device;
32
33pub struct Host;
34
35pub struct Stream {
36 ctx: Arc<AudioContext>,
37 on_ended_closures: Vec<ClosureHandle>,
38 config: StreamConfig,
39 buffer_size_frames: usize,
40}
41
42// WASM runs in a single-threaded environment, so Send and Sync are safe by design.
43unsafe impl Send for Stream {}
44unsafe impl Sync for Stream {}
45
46// Compile-time assertion that Stream is Send and Sync
47crate::assert_stream_send!(Stream);
48crate::assert_stream_sync!(Stream);
49
50pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs};
51
52const MIN_CHANNELS: u16 = 1;
53const MAX_CHANNELS: u16 = 32;
54const MIN_SAMPLE_RATE: SampleRate = 8_000;
55const MAX_SAMPLE_RATE: SampleRate = 96_000;
56const DEFAULT_SAMPLE_RATE: SampleRate = 44_100;
57const MIN_BUFFER_SIZE: u32 = 1;
58const MAX_BUFFER_SIZE: u32 = u32::MAX;
59const DEFAULT_BUFFER_SIZE: usize = 2048;
60const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;
61
62impl Host {
63 pub fn new() -> Result<Self, crate::HostUnavailable> {
64 Ok(Host)
65 }
66}
67
68impl HostTrait for Host {
69 type Devices = Devices;
70 type Device = Device;
71
72 fn is_available() -> bool {
73 // Assume this host is always available on webaudio.
74 true
75 }
76
77 fn devices(&self) -> Result<Self::Devices, DevicesError> {
78 Devices::new()
79 }
80
81 fn default_input_device(&self) -> Option<Self::Device> {
82 default_input_device()
83 }
84
85 fn default_output_device(&self) -> Option<Self::Device> {
86 default_output_device()
87 }
88}
89
90impl Devices {
91 fn new() -> Result<Self, DevicesError> {
92 Ok(Self::default())
93 }
94}
95
96impl Device {
97 fn description(&self) -> Result<DeviceDescription, DeviceNameError> {
98 Ok(DeviceDescriptionBuilder::new("Default Device".to_string())
99 .direction(crate::DeviceDirection::Output)
100 .build())
101 }
102
103 fn id(&self) -> Result<DeviceId, DeviceIdError> {
104 Ok(DeviceId(
105 crate::platform::HostId::WebAudio,
106 "default".to_string(),
107 ))
108 }
109
110 fn supported_input_configs(
111 &self,
112 ) -> Result<SupportedInputConfigs, SupportedStreamConfigsError> {
113 // TODO
114 Ok(Vec::new().into_iter())
115 }
116
117 fn supported_output_configs(
118 &self,
119 ) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
120 let buffer_size = SupportedBufferSize::Range {
121 min: MIN_BUFFER_SIZE,
122 max: MAX_BUFFER_SIZE,
123 };
124 let configs: Vec<_> = (MIN_CHANNELS..=MAX_CHANNELS)
125 .map(|channels| SupportedStreamConfigRange {
126 channels,
127 min_sample_rate: MIN_SAMPLE_RATE,
128 max_sample_rate: MAX_SAMPLE_RATE,
129 buffer_size,
130 sample_format: SUPPORTED_SAMPLE_FORMAT,
131 })
132 .collect();
133 Ok(configs.into_iter())
134 }
135
136 fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
137 // TODO
138 Err(DefaultStreamConfigError::StreamTypeNotSupported)
139 }
140
141 fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
142 const EXPECT: &str = "expected at least one valid webaudio stream config";
143 let config = self
144 .supported_output_configs()
145 .expect(EXPECT)
146 .max_by(|a, b| a.cmp_default_heuristics(b))
147 .unwrap()
148 .with_sample_rate(DEFAULT_SAMPLE_RATE);
149
150 Ok(config)
151 }
152}
153
154impl DeviceTrait for Device {
155 type SupportedInputConfigs = SupportedInputConfigs;
156 type SupportedOutputConfigs = SupportedOutputConfigs;
157 type Stream = Stream;
158
159 fn description(&self) -> Result<DeviceDescription, DeviceNameError> {
160 Device::description(self)
161 }
162
163 fn id(&self) -> Result<DeviceId, DeviceIdError> {
164 Device::id(self)
165 }
166
167 fn supported_input_configs(
168 &self,
169 ) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
170 Device::supported_input_configs(self)
171 }
172
173 fn supported_output_configs(
174 &self,
175 ) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> {
176 Device::supported_output_configs(self)
177 }
178
179 fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
180 Device::default_input_config(self)
181 }
182
183 fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
184 Device::default_output_config(self)
185 }
186
187 fn build_input_stream_raw<D, E>(
188 &self,
189 _config: StreamConfig,
190 _sample_format: SampleFormat,
191 _data_callback: D,
192 _error_callback: E,
193 _timeout: Option<Duration>,
194 ) -> Result<Self::Stream, BuildStreamError>
195 where
196 D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
197 E: FnMut(StreamError) + Send + 'static,
198 {
199 // TODO
200 Err(BuildStreamError::StreamConfigNotSupported)
201 }
202
203 /// Create an output stream.
204 fn build_output_stream_raw<D, E>(
205 &self,
206 config: StreamConfig,
207 sample_format: SampleFormat,
208 data_callback: D,
209 _error_callback: E,
210 _timeout: Option<Duration>,
211 ) -> Result<Self::Stream, BuildStreamError>
212 where
213 D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
214 E: FnMut(StreamError) + Send + 'static,
215 {
216 if !valid_config(config, sample_format) {
217 return Err(BuildStreamError::StreamConfigNotSupported);
218 }
219
220 let n_channels = config.channels as usize;
221
222 let buffer_size_frames = match config.buffer_size {
223 BufferSize::Fixed(v) => {
224 if !(MIN_BUFFER_SIZE..=MAX_BUFFER_SIZE).contains(&v) {
225 return Err(BuildStreamError::StreamConfigNotSupported);
226 }
227 v as usize
228 }
229 BufferSize::Default => DEFAULT_BUFFER_SIZE,
230 };
231 let buffer_size_samples = buffer_size_frames * n_channels;
232 let buffer_time_step_secs = buffer_time_step_secs(buffer_size_frames, config.sample_rate);
233
234 let data_callback = Arc::new(Mutex::new(Box::new(data_callback)));
235
236 // Create the WebAudio stream.
237 let stream_opts = AudioContextOptions::new();
238 stream_opts.set_sample_rate(config.sample_rate as f32);
239 let ctx = AudioContext::new_with_context_options(&stream_opts).map_err(
240 |err| -> BuildStreamError {
241 let description = format!("{:?}", err);
242 let err = BackendSpecificError { description };
243 err.into()
244 },
245 )?;
246
247 let destination = ctx.destination();
248
249 // If possible, set the destination's channel_count to the given config.channel.
250 // If not, fallback on the default destination channel_count to keep previous behavior
251 // and do not return an error.
252 if config.channels as u32 <= destination.max_channel_count() {
253 destination.set_channel_count(config.channels as u32);
254 }
255
256 // SAFETY: WASM is single-threaded, so Arc is safe even though AudioContext is not Send/Sync
257 #[allow(clippy::arc_with_non_send_sync)]
258 let ctx = Arc::new(ctx);
259
260 // A container for managing the lifecycle of the audio callbacks.
261 let mut on_ended_closures: Vec<ClosureHandle> = Vec::new();
262
263 // A cursor keeping track of the current time at which new frames should be scheduled.
264 let time = Arc::new(RwLock::new(0f64));
265
266 // baseLatency is fixed for the lifetime of the AudioContext.
267 let base_latency_secs = js_sys::Reflect::get(ctx.as_ref(), &JsValue::from("baseLatency"))
268 .ok()
269 .and_then(|v| v.as_f64())
270 .unwrap_or(0.0);
271
272 // Create a set of closures / callbacks which will continuously fetch and schedule sample
273 // playback. Starting with two workers, e.g. a front and back buffer so that audio frames
274 // can be fetched in the background.
275 for _i in 0..2 {
276 let data_callback_handle = data_callback.clone();
277 let ctx_handle = ctx.clone();
278 let time_handle = time.clone();
279
280 // A set of temporary buffers to be used for intermediate sample transformation steps.
281 let mut temporary_buffer = vec![0f32; buffer_size_samples];
282 let mut temporary_channel_buffer = vec![0f32; buffer_size_frames];
283
284 #[cfg(target_feature = "atomics")]
285 let temporary_channel_array_view: js_sys::Float32Array;
286 #[cfg(target_feature = "atomics")]
287 {
288 let temporary_channel_array = js_sys::ArrayBuffer::new(
289 (std::mem::size_of::<f32>() * buffer_size_frames) as u32,
290 );
291 temporary_channel_array_view = js_sys::Float32Array::new(&temporary_channel_array);
292 }
293
294 // Create a webaudio buffer which will be reused to avoid allocations.
295 let ctx_buffer = ctx
296 .create_buffer(
297 config.channels as u32,
298 buffer_size_frames as u32,
299 config.sample_rate as f32,
300 )
301 .map_err(|err| -> BuildStreamError {
302 let description = format!("{:?}", err);
303 let err = BackendSpecificError { description };
304 err.into()
305 })?;
306
307 // A self reference to this closure for passing to future audio event calls.
308 // SAFETY: WASM is single-threaded, so Arc is safe even though Closure is not Send/Sync
309 #[allow(clippy::arc_with_non_send_sync)]
310 let on_ended_closure: ClosureHandle = Arc::new(RwLock::new(None));
311 let on_ended_closure_handle = on_ended_closure.clone();
312
313 on_ended_closure
314 .write()
315 .unwrap()
316 .replace(Closure::wrap(Box::new(move || {
317 let now = ctx_handle.current_time();
318 let time_at_start_of_buffer = {
319 let time_at_start_of_buffer = time_handle
320 .read()
321 .expect("Unable to get a read lock on the time cursor");
322 // Synchronise first buffer as necessary (eg. keep the time value
323 // referenced to the context clock).
324 if *time_at_start_of_buffer > 0.0 {
325 *time_at_start_of_buffer
326 } else {
327 // Schedule the first buffer far enough ahead for the browser's
328 // internal audio pipeline (baseLatency) plus one full buffer of
329 // data, so playback starts underrun-free at any buffer size.
330 now + base_latency_secs + buffer_time_step_secs
331 }
332 };
333
334 // Populate the sample data into an interleaved temporary buffer.
335 {
336 let len = temporary_buffer.len();
337 let data = temporary_buffer.as_mut_ptr() as *mut ();
338 let mut data = unsafe { Data::from_parts(data, len, sample_format) };
339 let mut data_callback = data_callback_handle.lock().unwrap();
340 // outputLatency can change at runtime, so read it each callback.
341 let output_latency_secs = js_sys::Reflect::get(
342 ctx_handle.as_ref(),
343 &JsValue::from("outputLatency"),
344 )
345 .ok()
346 .and_then(|v| v.as_f64())
347 .unwrap_or(0.0);
348 let total_hw_latency_secs = {
349 let sum = base_latency_secs + output_latency_secs;
350 if sum.is_finite() {
351 sum.max(0.0)
352 } else {
353 0.0
354 }
355 };
356 let callback = crate::StreamInstant::from_secs_f64(now);
357 let playback = crate::StreamInstant::from_secs_f64(
358 time_at_start_of_buffer + total_hw_latency_secs,
359 );
360 let timestamp = crate::OutputStreamTimestamp { callback, playback };
361 let info = OutputCallbackInfo { timestamp };
362 (data_callback.deref_mut())(&mut data, &info);
363 }
364
365 // Deinterleave the sample data and copy into the audio context buffer.
366 // We do not reference the audio context buffer directly e.g. getChannelData.
367 // As wasm-bindgen only gives us a copy, not a direct reference.
368 for channel in 0..n_channels {
369 for i in 0..buffer_size_frames {
370 temporary_channel_buffer[i] =
371 temporary_buffer[n_channels * i + channel];
372 }
373
374 #[cfg(not(target_feature = "atomics"))]
375 {
376 ctx_buffer
377 .copy_to_channel(&temporary_channel_buffer, channel as i32)
378 .expect(
379 "Unable to write sample data into the audio context buffer",
380 );
381 }
382
383 // copyToChannel cannot be directly copied into from a SharedArrayBuffer,
384 // which WASM memory is backed by if the 'atomics' flag is enabled.
385 // This workaround copies the data into an intermediary buffer first.
386 // There's a chance browsers may eventually relax that requirement.
387 // See this issue: https://github.com/WebAudio/web-audio-api/issues/2565
388 #[cfg(target_feature = "atomics")]
389 {
390 temporary_channel_array_view.copy_from(&temporary_channel_buffer);
391 ctx_buffer
392 .unchecked_ref::<ExternalArrayAudioBuffer>()
393 .copy_to_channel(&temporary_channel_array_view, channel as i32)
394 .expect(
395 "Unable to write sample data into the audio context buffer",
396 );
397 }
398 }
399
400 // Create an AudioBufferSourceNode, schedule it to playback the reused buffer
401 // in the future.
402 let source = ctx_handle
403 .create_buffer_source()
404 .expect("Unable to create a webaudio buffer source");
405 source.set_buffer(Some(&ctx_buffer));
406 source
407 .connect_with_audio_node(&ctx_handle.destination())
408 .expect(
409 "Unable to connect the web audio buffer source to the context destination",
410 );
411 source
412 .add_event_listener_with_callback(
413 "ended",
414 on_ended_closure_handle
415 .read()
416 .unwrap()
417 .as_ref()
418 .unwrap()
419 .as_ref()
420 .unchecked_ref(),
421 )
422 .expect("Failed to add ended event listener");
423
424 source
425 .start_with_when(time_at_start_of_buffer)
426 .expect("Unable to start the webaudio buffer source");
427
428 // Keep track of when the next buffer worth of samples should be played.
429 *time_handle.write().unwrap() = time_at_start_of_buffer + buffer_time_step_secs;
430 }) as Box<dyn FnMut()>));
431
432 on_ended_closures.push(on_ended_closure);
433 }
434
435 Ok(Stream {
436 ctx,
437 on_ended_closures,
438 config,
439 buffer_size_frames,
440 })
441 }
442}
443
444impl Stream {
445 /// Return the [`AudioContext`](https://developer.mozilla.org/docs/Web/API/AudioContext) used
446 /// by this stream.
447 pub fn audio_context(&self) -> &AudioContext {
448 &self.ctx
449 }
450}
451
452impl StreamTrait for Stream {
453 fn play(&self) -> Result<(), PlayStreamError> {
454 let window = web_sys::window().unwrap();
455 match self.ctx.resume() {
456 Ok(_) => {
457 // Begin webaudio playback, initially scheduling the closures to fire on a timeout
458 // event.
459 let mut offset_ms = 10;
460 let time_step_secs =
461 buffer_time_step_secs(self.buffer_size_frames, self.config.sample_rate);
462 let time_step_ms = (time_step_secs * 1_000.0) as i32;
463 for on_ended_closure in self.on_ended_closures.iter() {
464 window
465 .set_timeout_with_callback_and_timeout_and_arguments_0(
466 on_ended_closure
467 .read()
468 .unwrap()
469 .as_ref()
470 .unwrap()
471 .as_ref()
472 .unchecked_ref(),
473 offset_ms,
474 )
475 .unwrap();
476 offset_ms += time_step_ms;
477 }
478 Ok(())
479 }
480 Err(err) => {
481 let description = format!("{:?}", err);
482 let err = BackendSpecificError { description };
483 Err(err.into())
484 }
485 }
486 }
487
488 fn pause(&self) -> Result<(), PauseStreamError> {
489 match self.ctx.suspend() {
490 Ok(_) => Ok(()),
491 Err(err) => {
492 let description = format!("{:?}", err);
493 let err = BackendSpecificError { description };
494 Err(err.into())
495 }
496 }
497 }
498
499 fn buffer_size(&self) -> Option<crate::FrameCount> {
500 Some(self.buffer_size_frames as crate::FrameCount)
501 }
502}
503
504impl Drop for Stream {
505 fn drop(&mut self) {
506 let _ = self.ctx.close();
507 }
508}
509
510impl Default for Devices {
511 fn default() -> Devices {
512 // We produce an empty iterator if the WebAudio API isn't available.
513 Devices(is_webaudio_available())
514 }
515}
516
517impl Iterator for Devices {
518 type Item = Device;
519
520 #[inline]
521 fn next(&mut self) -> Option<Device> {
522 if self.0 {
523 self.0 = false;
524 Some(Device)
525 } else {
526 None
527 }
528 }
529}
530
531fn default_input_device() -> Option<Device> {
532 // TODO
533 None
534}
535
536fn default_output_device() -> Option<Device> {
537 if is_webaudio_available() {
538 Some(Device)
539 } else {
540 None
541 }
542}
543
544// Detects whether the `AudioContext` global variable is available.
545fn is_webaudio_available() -> bool {
546 js_sys::Reflect::get(&js_sys::global(), &JsValue::from("AudioContext"))
547 .unwrap()
548 .is_truthy()
549}
550
551// Whether or not the given stream configuration is valid for building a stream.
552fn valid_config(conf: StreamConfig, sample_format: SampleFormat) -> bool {
553 conf.channels <= MAX_CHANNELS
554 && conf.channels >= MIN_CHANNELS
555 && conf.sample_rate <= MAX_SAMPLE_RATE
556 && conf.sample_rate >= MIN_SAMPLE_RATE
557 && sample_format == SUPPORTED_SAMPLE_FORMAT
558}
559
560fn buffer_time_step_secs(buffer_size_frames: usize, sample_rate: SampleRate) -> f64 {
561 buffer_size_frames as f64 / sample_rate as f64
562}
563
564#[cfg(target_feature = "atomics")]
565#[wasm_bindgen]
566extern "C" {
567 #[wasm_bindgen(js_name = AudioBuffer)]
568 type ExternalArrayAudioBuffer;
569
570 # [wasm_bindgen(catch, method, structural, js_class = "AudioBuffer", js_name = copyToChannel)]
571 pub fn copy_to_channel(
572 this: &ExternalArrayAudioBuffer,
573 source: &js_sys::Float32Array,
574 channel_number: i32,
575 ) -> Result<(), JsValue>;
576}