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 · 453 lines · 15.6 KBRust Blame HistoryRaw
Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago1//! Audio Worklet backend implementation.
2//!
3//! Available on WebAssembly with the `audioworklet` feature. Requires atomics support.
4//! See the `audioworklet-beep` example for setup instructions.
5
6mod dependent_module;
7use js_sys::wasm_bindgen;
8
9use crate::dependent_module;
10use wasm_bindgen::prelude::*;
11
12use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
13use crate::{
14 BackendSpecificError, BuildStreamError, ChannelCount, 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};
20
21use std::time::Duration;
22
23/// Content is false if the iterator is empty.
24pub struct Devices(bool);
25
26#[derive(Clone, Debug, PartialEq, Eq, Hash)]
27pub struct Device;
28
29pub struct Host;
30
31pub struct Stream {
32 audio_context: web_sys::AudioContext,
33}
34
35pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs};
36
37const MIN_CHANNELS: ChannelCount = 1;
38const MAX_CHANNELS: ChannelCount = 32;
39const MIN_SAMPLE_RATE: SampleRate = 8_000;
40const MAX_SAMPLE_RATE: SampleRate = 96_000;
41const DEFAULT_SAMPLE_RATE: SampleRate = 44_100;
42const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;
43
44impl Host {
45 pub fn new() -> Result<Self, crate::HostUnavailable> {
46 if Self::is_available() {
47 Ok(Host)
48 } else {
49 Err(crate::HostUnavailable)
50 }
51 }
52}
53
54impl HostTrait for Host {
55 type Devices = Devices;
56 type Device = Device;
57
58 fn is_available() -> bool {
59 if let Some(window) = web_sys::window() {
60 let has_audio_worklet =
61 js_sys::Reflect::has(&window, &JsValue::from_str("AudioWorklet")).unwrap_or(false);
62
63 let cross_origin_isolated =
64 js_sys::Reflect::get(&window, &JsValue::from_str("crossOriginIsolated"))
65 .ok()
66 .and_then(|v| v.as_bool())
67 .unwrap_or(false);
68
69 has_audio_worklet && cross_origin_isolated
70 } else {
71 false
72 }
73 }
74
75 fn devices(&self) -> Result<Self::Devices, DevicesError> {
76 Devices::new()
77 }
78
79 fn default_input_device(&self) -> Option<Self::Device> {
80 // TODO
81 None
82 }
83
84 fn default_output_device(&self) -> Option<Self::Device> {
85 Some(Device)
86 }
87}
88
89impl Devices {
90 fn new() -> Result<Self, DevicesError> {
91 Ok(Self::default())
92 }
93}
94
95impl DeviceTrait for Device {
96 type SupportedInputConfigs = SupportedInputConfigs;
97 type SupportedOutputConfigs = SupportedOutputConfigs;
98 type Stream = Stream;
99
100 #[inline]
101 fn description(&self) -> Result<DeviceDescription, DeviceNameError> {
102 Ok(DeviceDescriptionBuilder::new("Default Device".to_string())
103 .direction(crate::DeviceDirection::Output)
104 .build())
105 }
106
107 #[inline]
108 fn id(&self) -> Result<DeviceId, DeviceIdError> {
109 Ok(DeviceId(
110 crate::platform::HostId::AudioWorklet,
111 "default".to_string(),
112 ))
113 }
114
115 #[inline]
116 fn supported_input_configs(
117 &self,
118 ) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
119 // TODO
120 Ok(Vec::new().into_iter())
121 }
122
123 #[inline]
124 fn supported_output_configs(
125 &self,
126 ) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> {
127 let buffer_size = SupportedBufferSize::Unknown;
128
129 // In actuality the number of supported channels cannot be fully known until
130 // the browser attempts to initialized the AudioWorklet.
131
132 let configs: Vec<_> = (MIN_CHANNELS..=MAX_CHANNELS)
133 .map(|channels| SupportedStreamConfigRange {
134 channels,
135 min_sample_rate: MIN_SAMPLE_RATE,
136 max_sample_rate: MAX_SAMPLE_RATE,
137 buffer_size,
138 sample_format: SUPPORTED_SAMPLE_FORMAT,
139 })
140 .collect();
141 Ok(configs.into_iter())
142 }
143
144 #[inline]
145 fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
146 // TODO
147 Err(DefaultStreamConfigError::StreamTypeNotSupported)
148 }
149
150 #[inline]
151 fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
152 const EXPECT: &str = "expected at least one valid webaudio stream config";
153 let config = self
154 .supported_output_configs()
155 .expect(EXPECT)
156 .max_by(|a, b| a.cmp_default_heuristics(b))
157 .unwrap()
158 .with_sample_rate(DEFAULT_SAMPLE_RATE);
159
160 Ok(config)
161 }
162
163 fn build_input_stream_raw<D, E>(
164 &self,
165 _config: StreamConfig,
166 _sample_format: SampleFormat,
167 _data_callback: D,
168 _error_callback: E,
169 _timeout: Option<Duration>,
170 ) -> Result<Self::Stream, BuildStreamError>
171 where
172 D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
173 E: FnMut(StreamError) + Send + 'static,
174 {
175 // TODO
176 Err(BuildStreamError::StreamConfigNotSupported)
177 }
178
179 /// Create an output stream.
180 fn build_output_stream_raw<D, E>(
181 &self,
182 config: StreamConfig,
183 sample_format: SampleFormat,
184 mut data_callback: D,
185 mut error_callback: E,
186 _timeout: Option<Duration>,
187 ) -> Result<Self::Stream, BuildStreamError>
188 where
189 D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
190 E: FnMut(StreamError) + Send + 'static,
191 {
192 if !valid_config(config, sample_format) {
193 return Err(BuildStreamError::StreamConfigNotSupported);
194 }
195
196 let stream_opts = web_sys::AudioContextOptions::new();
197 stream_opts.set_sample_rate(config.sample_rate as f32);
198
199 let audio_context = web_sys::AudioContext::new_with_context_options(&stream_opts).map_err(
200 |err| -> BuildStreamError {
201 let description = format!("{err:?}");
202 let err = BackendSpecificError { description };
203 err.into()
204 },
205 )?;
206
207 let destination = audio_context.destination();
208
209 // If possible, set the destination's channel_count to the given config.channel.
210 // If not, fallback on the default destination channel_count to keep previous behavior
211 // and do not return an error.
212 if config.channels as u32 <= destination.max_channel_count() {
213 destination.set_channel_count(config.channels as u32);
214 }
215
216 let ctx = audio_context.clone();
217 wasm_bindgen_futures::spawn_local(async move {
218 let result: Result<(), JsValue> = async move {
219 let mod_url = dependent_module!("worklet.js")?;
220 wasm_bindgen_futures::JsFuture::from(ctx.audio_worklet()?.add_module(&mod_url)?)
221 .await?;
222
223 let options = web_sys::AudioWorkletNodeOptions::new();
224
225 let js_array = js_sys::Array::new();
226 js_array.push(&JsValue::from_f64(destination.channel_count() as _));
227
228 options.set_output_channel_count(&js_array);
229 options.set_number_of_inputs(0);
230
231 // Capture audio output latency here: the closure runs in a separate worker and cannot access AudioContext properties directly.
232 // While baseLatency is fixed for the context lifetime, outputLatency can change but not be re-read from inside the worklet;
233 // we snapshot it here.
234 let base_latency_secs =
235 js_sys::Reflect::get(ctx.as_ref(), &JsValue::from("baseLatency"))
236 .ok()
237 .and_then(|v| v.as_f64())
238 .unwrap_or(0.0);
239 let output_latency_secs =
240 js_sys::Reflect::get(ctx.as_ref(), &JsValue::from("outputLatency"))
241 .ok()
242 .and_then(|v| v.as_f64())
243 .unwrap_or(0.0);
244 let total_output_latency_secs = {
245 let sum = base_latency_secs + output_latency_secs;
246 if sum.is_finite() { sum.max(0.0) } else { 0.0 }
247 };
248
249 options.set_processor_options(Some(&js_sys::Array::of3(
250 &wasm_bindgen::module(),
251 &wasm_bindgen::memory(),
252 &WasmAudioProcessor::new(Box::new(
253 move |interleaved_data, frame_size, sample_rate, now| {
254 let data = interleaved_data.as_mut_ptr() as *mut ();
255 let mut data = unsafe {
256 Data::from_parts(data, interleaved_data.len(), sample_format)
257 };
258
259 let callback = crate::StreamInstant::from_secs_f64(now);
260 let buffer_duration = frames_to_duration(frame_size as _, sample_rate);
261 let playback = callback
262 .add(buffer_duration + Duration::from_secs_f64(total_output_latency_secs))
263 .expect(
264 "`playback` occurs beyond representation supported by `StreamInstant`",
265 );
266 let timestamp = crate::OutputStreamTimestamp { callback, playback };
267 let info = OutputCallbackInfo { timestamp };
268 (data_callback)(&mut data, &info);
269 },
270 ))
271 .pack()
272 .into(),
273 )));
274 // This name 'CpalProcessor' must match the name registered in worklet.js
275 let audio_worklet_node =
276 web_sys::AudioWorkletNode::new_with_options(&ctx, "CpalProcessor", &options)?;
277
278 audio_worklet_node.connect_with_audio_node(&destination)?;
279 Ok(())
280 }
281 .await;
282
283 if let Err(err) = result {
284 let description = if let Some(string_value) = err.as_string() {
285 string_value
286 } else {
287 format!("Browser error initializing stream: {err:?}")
288 };
289
290 error_callback(StreamError::BackendSpecific {
291 err: BackendSpecificError { description },
292 })
293 }
294 });
295
296 Ok(Stream { audio_context })
297 }
298}
299
300impl StreamTrait for Stream {
301 fn play(&self) -> Result<(), PlayStreamError> {
302 match self.audio_context.resume() {
303 Ok(_) => Ok(()),
304 Err(err) => {
305 let description = format!("{err:?}");
306 let err = BackendSpecificError { description };
307 Err(err.into())
308 }
309 }
310 }
311
312 fn pause(&self) -> Result<(), PauseStreamError> {
313 match self.audio_context.suspend() {
314 Ok(_) => Ok(()),
315 Err(err) => {
316 let description = format!("{err:?}");
317 let err = BackendSpecificError { description };
318 Err(err.into())
319 }
320 }
321 }
322}
323
324impl Drop for Stream {
325 fn drop(&mut self) {
326 let _ = self.audio_context.close();
327 }
328}
329
330impl Default for Devices {
331 fn default() -> Devices {
332 Devices(true)
333 }
334}
335
336impl Iterator for Devices {
337 type Item = Device;
338 #[inline]
339 fn next(&mut self) -> Option<Device> {
340 if self.0 {
341 self.0 = false;
342 Some(Device)
343 } else {
344 None
345 }
346 }
347}
348
349// Whether or not the given stream configuration is valid for building a stream.
350fn valid_config(conf: StreamConfig, sample_format: SampleFormat) -> bool {
351 conf.channels <= MAX_CHANNELS
352 && conf.channels >= MIN_CHANNELS
353 && conf.sample_rate <= MAX_SAMPLE_RATE
354 && conf.sample_rate >= MIN_SAMPLE_RATE
355 && sample_format == SUPPORTED_SAMPLE_FORMAT
356}
357
358// Convert the given duration in frames at the given sample rate to a `std::time::Duration`.
359fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration {
360 let secsf = frames as f64 / rate as f64;
361 let secs = secsf as u64;
362 let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32;
363 std::time::Duration::new(secs, nanos)
364}
365
366type AudioProcessorCallback = Box<dyn FnMut(&mut [f32], u32, u32, f64)>;
367
368/// WasmAudioProcessor provides an interface for the Javascript code
369/// running in the AudioWorklet to interact with Rust.
370#[wasm_bindgen]
371pub struct WasmAudioProcessor {
372 #[wasm_bindgen(skip)]
373 interleaved_buffer: Vec<f32>,
374 #[wasm_bindgen(skip)]
375 // Passes in an interleaved scratch buffer, frame size, sample rate, and current time.
376 callback: AudioProcessorCallback,
377}
378
379impl WasmAudioProcessor {
380 pub fn new(callback: AudioProcessorCallback) -> Self {
381 Self {
382 interleaved_buffer: Vec::new(),
383 callback,
384 }
385 }
386}
387
388#[wasm_bindgen]
389impl WasmAudioProcessor {
390 pub fn process(
391 &mut self,
392 channels: u32,
393 frame_size: u32,
394 sample_rate: u32,
395 current_time: f64,
396 ) -> u32 {
397 let frame_size = frame_size as usize;
398
399 // Ensure there's enough space in the output buffer
400 // This likely only occurs once, or very few times.
401 let interleaved_buffer_size = channels as usize * frame_size;
402 self.interleaved_buffer.resize(
403 interleaved_buffer_size.max(self.interleaved_buffer.len()),
404 0.0,
405 );
406
407 (self.callback)(
408 &mut self.interleaved_buffer[..interleaved_buffer_size],
409 frame_size as u32,
410 sample_rate,
411 current_time,
412 );
413
414 // Returns a pointer to the raw interleaved buffer to Javascript so
415 // it can deinterleave it into the output buffers.
416 //
417 // Deinterleaving is done on the Javascript side because it's simpler and it may be faster.
418 // Doing it this way avoids an extra copy and the JS deinterleaving code
419 // is likely heavily optimized by the browser's JS engine,
420 // although I have not tested that assumption.
421 self.interleaved_buffer.as_mut_ptr() as _
422 }
423
424 /// Converts this `WasmAudioProcessor` into a raw pointer (as `usize`) for FFI use.
425 ///
426 /// # Purpose
427 /// This function is intended to transfer ownership of the processor instance to the caller,
428 /// typically for passing between Rust and JavaScript via WebAssembly.
429 ///
430 /// # Relationship with [`unpack`]
431 /// The returned pointer must be passed to [`unpack`] exactly once to recover the original
432 /// `WasmAudioProcessor` instance. Failing to do so will result in a memory leak. Calling
433 /// [`unpack`] more than once or using the pointer after it has been unpacked will result in
434 /// undefined behavior.
435 ///
436 /// # Safety and Lifetime
437 /// After calling `pack`, the caller is responsible for ensuring that `unpack` is called
438 /// exactly once, and that the pointer is not used after being unpacked. This function
439 /// should be used with care, as improper use can lead to memory safety issues.
440 ///
441 /// [`unpack`]: Self::unpack
442 pub fn pack(self) -> usize {
443 Box::into_raw(Box::new(self)) as usize
444 }
445 /// # Safety
446 ///
447 /// The `val` parameter must be a value previously returned by `Self::pack`.
448 /// It must not have already been unpacked or deallocated, and must not be used after this call.
449 /// Using an invalid or already-consumed pointer will result in undefined behavior.
450 pub unsafe fn unpack(val: usize) -> Self {
451 *Box::from_raw(val as *mut _)
452 }
453}