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 · 439 lines · 14.1 KBRust Blame HistoryRaw
Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago1//! Emscripten backend implementation.
2//!
3//! Default backend on Emscripten.
4
5use js_sys::Float32Array;
6use std::panic::AssertUnwindSafe;
7use std::time::Duration;
8use wasm_bindgen::prelude::*;
9use wasm_bindgen::JsCast;
10use wasm_bindgen_futures::{spawn_local, JsFuture};
11use web_sys::AudioContext;
12
13use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
14use crate::{
15 BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription,
16 DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, DevicesError,
17 InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat,
18 SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig,
19 SupportedStreamConfigRange, SupportedStreamConfigsError,
20};
21
22// The emscripten backend currently works by instantiating an `AudioContext` object per `Stream`.
23// Creating a stream creates a new `AudioContext`. Destroying a stream destroys it. Creation of a
24// `Host` instance initializes the `stdweb` context.
25
26/// The default emscripten host type.
27#[derive(Debug)]
28pub struct Host;
29
30/// Content is false if the iterator is empty.
31pub struct Devices(bool);
32
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct Device;
35
36#[wasm_bindgen]
37#[derive(Clone)]
38pub struct Stream {
39 // A reference to an `AudioContext` object.
40 audio_ctxt: AudioContext,
41}
42
43// WASM runs in a single-threaded environment, so Send and Sync are safe by design.
44unsafe impl Send for Stream {}
45unsafe impl Sync for Stream {}
46
47// Compile-time assertion that Stream is Send and Sync
48crate::assert_stream_send!(Stream);
49crate::assert_stream_sync!(Stream);
50
51pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs};
52
53const MIN_CHANNELS: u16 = 1;
54const MAX_CHANNELS: u16 = 32;
55const MIN_SAMPLE_RATE: SampleRate = 8_000;
56const MAX_SAMPLE_RATE: SampleRate = 96_000;
57const DEFAULT_SAMPLE_RATE: SampleRate = 44_100;
58const MIN_BUFFER_SIZE: u32 = 1;
59const MAX_BUFFER_SIZE: u32 = u32::MAX;
60const DEFAULT_BUFFER_SIZE: usize = 2048;
61const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;
62
63impl Host {
64 pub fn new() -> Result<Self, crate::HostUnavailable> {
65 Ok(Host)
66 }
67}
68
69impl Devices {
70 fn new() -> Result<Self, DevicesError> {
71 Ok(Self::default())
72 }
73}
74
75impl Device {
76 fn description(&self) -> Result<DeviceDescription, DeviceNameError> {
77 Ok(DeviceDescriptionBuilder::new("Default Device".to_string())
78 .direction(crate::DeviceDirection::Output)
79 .build())
80 }
81
82 fn id(&self) -> Result<DeviceId, DeviceIdError> {
83 Ok(DeviceId(
84 crate::platform::HostId::Emscripten,
85 "default".to_string(),
86 ))
87 }
88
89 fn supported_input_configs(
90 &self,
91 ) -> Result<SupportedInputConfigs, SupportedStreamConfigsError> {
92 unimplemented!();
93 }
94
95 fn supported_output_configs(
96 &self,
97 ) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
98 let buffer_size = SupportedBufferSize::Range {
99 min: MIN_BUFFER_SIZE,
100 max: MAX_BUFFER_SIZE,
101 };
102 let configs: Vec<_> = (MIN_CHANNELS..=MAX_CHANNELS)
103 .map(|channels| SupportedStreamConfigRange {
104 channels,
105 min_sample_rate: MIN_SAMPLE_RATE,
106 max_sample_rate: MAX_SAMPLE_RATE,
107 buffer_size,
108 sample_format: SUPPORTED_SAMPLE_FORMAT,
109 })
110 .collect();
111 Ok(configs.into_iter())
112 }
113
114 fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
115 unimplemented!();
116 }
117
118 fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
119 const EXPECT: &str = "expected at least one valid webaudio stream config";
120 let config = self
121 .supported_output_configs()
122 .expect(EXPECT)
123 .max_by(|a, b| a.cmp_default_heuristics(b))
124 .unwrap()
125 .with_sample_rate(DEFAULT_SAMPLE_RATE);
126
127 Ok(config)
128 }
129}
130
131impl HostTrait for Host {
132 type Devices = Devices;
133 type Device = Device;
134
135 fn is_available() -> bool {
136 // Assume this host is always available on emscripten.
137 true
138 }
139
140 fn devices(&self) -> Result<Self::Devices, DevicesError> {
141 Devices::new()
142 }
143
144 fn default_input_device(&self) -> Option<Self::Device> {
145 default_input_device()
146 }
147
148 fn default_output_device(&self) -> Option<Self::Device> {
149 default_output_device()
150 }
151}
152
153impl DeviceTrait for Device {
154 type SupportedInputConfigs = SupportedInputConfigs;
155 type SupportedOutputConfigs = SupportedOutputConfigs;
156 type Stream = Stream;
157
158 fn description(&self) -> Result<DeviceDescription, DeviceNameError> {
159 Device::description(self)
160 }
161
162 fn id(&self) -> Result<DeviceId, DeviceIdError> {
163 Device::id(self)
164 }
165
166 fn supported_input_configs(
167 &self,
168 ) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
169 Device::supported_input_configs(self)
170 }
171
172 fn supported_output_configs(
173 &self,
174 ) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> {
175 Device::supported_output_configs(self)
176 }
177
178 fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
179 Device::default_input_config(self)
180 }
181
182 fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
183 Device::default_output_config(self)
184 }
185
186 fn build_input_stream_raw<D, E>(
187 &self,
188 _config: StreamConfig,
189 _sample_format: SampleFormat,
190 _data_callback: D,
191 _error_callback: E,
192 _timeout: Option<Duration>,
193 ) -> Result<Self::Stream, BuildStreamError>
194 where
195 D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
196 E: FnMut(StreamError) + Send + 'static,
197 {
198 unimplemented!()
199 }
200
201 fn build_output_stream_raw<D, E>(
202 &self,
203 config: StreamConfig,
204 sample_format: SampleFormat,
205 data_callback: D,
206 _error_callback: E,
207 _timeout: Option<Duration>,
208 ) -> Result<Self::Stream, BuildStreamError>
209 where
210 D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
211 E: FnMut(StreamError) + Send + 'static,
212 {
213 if !valid_config(config, sample_format) {
214 return Err(BuildStreamError::StreamConfigNotSupported);
215 }
216
217 let buffer_size_frames = match config.buffer_size {
218 BufferSize::Fixed(v) => {
219 if !(MIN_BUFFER_SIZE..=MAX_BUFFER_SIZE).contains(&v) {
220 return Err(BuildStreamError::StreamConfigNotSupported);
221 }
222 v as usize
223 }
224 BufferSize::Default => DEFAULT_BUFFER_SIZE,
225 };
226
227 // Create the stream.
228 let audio_ctxt = AudioContext::new().expect("webaudio is not present on this system");
229 let stream = Stream { audio_ctxt };
230
231 // Use `set_timeout` to invoke a Rust callback repeatedly.
232 //
233 // The job of this callback is to fill the content of the audio buffers.
234 //
235 // See also: The call to `set_timeout` at the end of the `audio_callback_fn` which creates
236 // the loop.
237 let data_callback = AssertUnwindSafe(data_callback);
238 set_timeout(
239 10,
240 stream.clone(),
241 data_callback,
242 config,
243 sample_format,
244 buffer_size_frames as u32,
245 );
246
247 Ok(stream)
248 }
249}
250
251impl StreamTrait for Stream {
252 fn play(&self) -> Result<(), PlayStreamError> {
253 let future = JsFuture::from(
254 self.audio_ctxt
255 .resume()
256 .expect("Could not resume the stream"),
257 );
258 spawn_local(async {
259 match future.await {
260 Ok(value) => assert!(value.is_undefined()),
261 Err(value) => panic!("AudioContext.resume() promise was rejected: {:?}", value),
262 }
263 });
264 Ok(())
265 }
266
267 fn pause(&self) -> Result<(), PauseStreamError> {
268 let future = JsFuture::from(
269 self.audio_ctxt
270 .suspend()
271 .expect("Could not suspend the stream"),
272 );
273 spawn_local(async {
274 match future.await {
275 Ok(value) => assert!(value.is_undefined()),
276 Err(value) => panic!("AudioContext.suspend() promise was rejected: {:?}", value),
277 }
278 });
279 Ok(())
280 }
281}
282
283fn audio_callback_fn<D>(
284 mut data_callback: AssertUnwindSafe<D>,
285) -> impl FnOnce(Stream, StreamConfig, SampleFormat, u32)
286where
287 D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
288{
289 |stream, config, sample_format, buffer_size_frames| {
290 let sample_rate = config.sample_rate;
291 let buffer_size_samples = buffer_size_frames * config.channels as u32;
292 let audio_ctxt = &stream.audio_ctxt;
293
294 // TODO: We should be re-using a buffer.
295 let mut temporary_buffer = vec![0f32; buffer_size_samples as usize];
296
297 {
298 let len = temporary_buffer.len();
299 let data = temporary_buffer.as_mut_ptr() as *mut ();
300 let mut data = unsafe { Data::from_parts(data, len, sample_format) };
301 let now_secs: f64 = audio_ctxt.current_time();
302 let callback = crate::StreamInstant::from_secs_f64(now_secs);
303 // TODO: Use proper latency instead. Currently, unsupported on most browsers though, so
304 // we estimate based on buffer size instead. Probably should use this, but it's only
305 // supported by firefox (2020-04-28).
306 // let latency_secs: f64 = audio_ctxt.outputLatency.try_into().unwrap();
307 let buffer_duration = frames_to_duration(len, sample_rate as usize);
308 let playback = callback
309 .add(buffer_duration)
310 .expect("`playback` occurs beyond representation supported by `StreamInstant`");
311 let timestamp = crate::OutputStreamTimestamp { callback, playback };
312 let info = OutputCallbackInfo { timestamp };
313 data_callback(&mut data, &info);
314 }
315
316 let typed_array: Float32Array = temporary_buffer.as_slice().into();
317
318 debug_assert_eq!(temporary_buffer.len() % config.channels as usize, 0);
319
320 let src_buffer = Float32Array::new(typed_array.buffer().as_ref());
321 let context = audio_ctxt;
322 let buffer = context
323 .create_buffer(
324 config.channels as u32,
325 buffer_size_frames,
326 sample_rate as f32,
327 )
328 .expect("Buffer could not be created");
329 for channel in 0..config.channels {
330 let mut buffer_content = buffer
331 .get_channel_data(channel as u32)
332 .expect("Should be impossible");
333 for (i, buffer_content_item) in buffer_content.iter_mut().enumerate() {
334 *buffer_content_item =
335 src_buffer.get_index(i as u32 * config.channels as u32 + channel as u32);
336 }
337 }
338
339 let node = context
340 .create_buffer_source()
341 .expect("The buffer source node could not be created");
342 node.set_buffer(Some(&buffer));
343 context
344 .destination()
345 .connect_with_audio_node(&node)
346 .expect("Could not connect the audio node to the destination");
347 node.start().expect("Could not start the audio node");
348
349 // TODO: handle latency better ; right now we just use setInterval with the amount of sound
350 // data that is in each buffer ; this is obviously bad, and also the schedule is too tight
351 // and there may be underflows
352 set_timeout(
353 1000 * buffer_size_frames as i32 / sample_rate as i32,
354 stream.clone().clone(),
355 data_callback,
356 config,
357 sample_format,
358 buffer_size_frames,
359 );
360 }
361}
362
363fn set_timeout<D>(
364 time: i32,
365 stream: Stream,
366 data_callback: AssertUnwindSafe<D>,
367 config: StreamConfig,
368 sample_format: SampleFormat,
369 buffer_size_frames: u32,
370) where
371 D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
372{
373 let window = web_sys::window().expect("Not in a window somehow?");
374 window
375 .set_timeout_with_callback_and_timeout_and_arguments_4(
376 Closure::once_into_js(audio_callback_fn(data_callback))
377 .dyn_ref::<js_sys::Function>()
378 .expect("The function was somehow not a function"),
379 time,
380 &stream.into(),
381 &config.into(),
382 &Closure::once_into_js(move || sample_format),
383 &buffer_size_frames.into(),
384 )
385 .expect("The timeout could not be set");
386}
387
388impl Default for Devices {
389 fn default() -> Devices {
390 // We produce an empty iterator if the WebAudio API isn't available.
391 Devices(is_webaudio_available())
392 }
393}
394impl Iterator for Devices {
395 type Item = Device;
396
397 fn next(&mut self) -> Option<Device> {
398 if self.0 {
399 self.0 = false;
400 Some(Device)
401 } else {
402 None
403 }
404 }
405}
406
407fn default_input_device() -> Option<Device> {
408 unimplemented!();
409}
410
411fn default_output_device() -> Option<Device> {
412 if is_webaudio_available() {
413 Some(Device)
414 } else {
415 None
416 }
417}
418
419// Detects whether the `AudioContext` global variable is available.
420fn is_webaudio_available() -> bool {
421 AudioContext::new().is_ok()
422}
423
424// Whether or not the given stream configuration is valid for building a stream.
425fn valid_config(conf: StreamConfig, sample_format: SampleFormat) -> bool {
426 conf.channels <= MAX_CHANNELS
427 && conf.channels >= MIN_CHANNELS
428 && conf.sample_rate <= MAX_SAMPLE_RATE
429 && conf.sample_rate >= MIN_SAMPLE_RATE
430 && sample_format == SUPPORTED_SAMPLE_FORMAT
431}
432
433// Convert the given duration in frames at the given sample rate to a `std::time::Duration`.
434fn frames_to_duration(frames: usize, rate: usize) -> std::time::Duration {
435 let secsf = frames as f64 / rate as f64;
436 let secs = secsf as u64;
437 let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32;
438 std::time::Duration::new(secs, nanos)
439}