Skip to content

Commit e9330cd

Browse files
committed
Add async versions of AudioContext suspend/resume/close
1 parent ad24275 commit e9330cd

File tree

1 file changed

+136
-7
lines changed

1 file changed

+136
-7
lines changed

src/context/online.rs

Lines changed: 136 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use crate::render::graph::Graph;
1313
use crate::MediaElement;
1414
use crate::{AudioRenderCapacity, Event};
1515

16+
use futures::channel::oneshot;
17+
1618
/// Check if the provided sink_id is available for playback
1719
///
1820
/// It should be "", "none" or a valid output `sinkId` returned from [`enumerate_devices_sync`]
@@ -378,8 +380,86 @@ impl AudioContext {
378380
/// This will temporarily halt audio hardware access and reducing CPU/battery usage in the
379381
/// process.
380382
///
381-
/// This function operates synchronously and might block the current thread. An async version
382-
/// is currently not implemented.
383+
/// # Panics
384+
///
385+
/// Will panic if:
386+
///
387+
/// * The audio device is not available
388+
/// * For a `BackendSpecificError`
389+
pub async fn suspend(&self) {
390+
// First, pause rendering via a control message
391+
let (sender, receiver) = oneshot::channel();
392+
let notify = OneshotNotify::Async(sender);
393+
let suspend_msg = ControlMessage::Suspend { notify };
394+
self.base.send_control_msg(suspend_msg);
395+
396+
// Wait for the render thread to have processed the suspend message.
397+
// The AudioContextState will be updated by the render thread.
398+
receiver.await.unwrap();
399+
400+
// Then ask the audio host to suspend the stream
401+
self.backend_manager.lock().unwrap().suspend();
402+
}
403+
404+
/// Resumes the progression of time in an audio context that has previously been
405+
/// suspended/paused.
406+
///
407+
/// # Panics
408+
///
409+
/// Will panic if:
410+
///
411+
/// * The audio device is not available
412+
/// * For a `BackendSpecificError`
413+
pub async fn resume(&self) {
414+
// First ask the audio host to resume the stream
415+
self.backend_manager.lock().unwrap().resume();
416+
417+
// Then, ask to resume rendering via a control message
418+
let (sender, receiver) = oneshot::channel();
419+
let notify = OneshotNotify::Async(sender);
420+
let suspend_msg = ControlMessage::Resume { notify };
421+
self.base.send_control_msg(suspend_msg);
422+
423+
// Wait for the render thread to have processed the resume message
424+
// The AudioContextState will be updated by the render thread.
425+
receiver.await.unwrap();
426+
}
427+
428+
/// Closes the `AudioContext`, releasing the system resources being used.
429+
///
430+
/// This will not automatically release all `AudioContext`-created objects, but will suspend
431+
/// the progression of the currentTime, and stop processing audio data.
432+
///
433+
/// # Panics
434+
///
435+
/// Will panic when this function is called multiple times
436+
pub async fn close(&self) {
437+
// First, stop rendering via a control message
438+
let (sender, receiver) = oneshot::channel();
439+
let notify = OneshotNotify::Async(sender);
440+
let suspend_msg = ControlMessage::Close { notify };
441+
self.base.send_control_msg(suspend_msg);
442+
443+
// Wait for the render thread to have processed the suspend message.
444+
// The AudioContextState will be updated by the render thread.
445+
receiver.await.unwrap();
446+
447+
// Then ask the audio host to close the stream
448+
self.backend_manager.lock().unwrap().close();
449+
450+
// Stop the AudioRenderCapacity collection thread
451+
self.render_capacity.stop();
452+
453+
// TODO stop the event loop <https://github.com/orottier/web-audio-api-rs/issues/421>
454+
}
455+
456+
/// Suspends the progression of time in the audio context.
457+
///
458+
/// This will temporarily halt audio hardware access and reducing CPU/battery usage in the
459+
/// process.
460+
///
461+
/// This function operates synchronously and blocks the current thread until the audio thread
462+
/// has stopped processing.
383463
///
384464
/// # Panics
385465
///
@@ -405,8 +485,8 @@ impl AudioContext {
405485
/// Resumes the progression of time in an audio context that has previously been
406486
/// suspended/paused.
407487
///
408-
/// This function operates synchronously and might block the current thread. An async version
409-
/// is currently not implemented.
488+
/// This function operates synchronously and blocks the current thread until the audio thread
489+
/// has started processing again.
410490
///
411491
/// # Panics
412492
///
@@ -434,13 +514,12 @@ impl AudioContext {
434514
/// This will not automatically release all `AudioContext`-created objects, but will suspend
435515
/// the progression of the currentTime, and stop processing audio data.
436516
///
437-
/// This function operates synchronously and might block the current thread. An async version
438-
/// is currently not implemented.
517+
/// This function operates synchronously and blocks the current thread until the audio thread
518+
/// has stopped processing.
439519
///
440520
/// # Panics
441521
///
442522
/// Will panic when this function is called multiple times
443-
#[allow(clippy::missing_const_for_fn, clippy::unused_self)]
444523
pub fn close_sync(&self) {
445524
// First, stop rendering via a control message
446525
let (sender, receiver) = crossbeam_channel::bounded(0);
@@ -511,3 +590,53 @@ impl AudioContext {
511590
&self.render_capacity
512591
}
513592
}
593+
594+
#[cfg(test)]
595+
mod tests {
596+
use super::*;
597+
use futures::executor;
598+
599+
#[test]
600+
fn test_suspend_resume_close() {
601+
let options = AudioContextOptions {
602+
sink_id: "none".into(),
603+
..AudioContextOptions::default()
604+
};
605+
606+
// construct with 'none' sink_id
607+
let context = AudioContext::new(options);
608+
609+
// allow some time to progress
610+
std::thread::sleep(std::time::Duration::from_millis(1));
611+
612+
executor::block_on(context.suspend());
613+
assert_eq!(context.state(), AudioContextState::Suspended);
614+
let time1 = context.current_time();
615+
assert!(time1 >= 0.);
616+
617+
// allow some time to progress
618+
std::thread::sleep(std::time::Duration::from_millis(1));
619+
let time2 = context.current_time();
620+
assert_eq!(time1, time2); // no progression of time
621+
622+
executor::block_on(context.resume());
623+
assert_eq!(context.state(), AudioContextState::Running);
624+
625+
// allow some time to progress
626+
std::thread::sleep(std::time::Duration::from_millis(1));
627+
628+
let time3 = context.current_time();
629+
assert!(time3 > time2); // time is progressing
630+
631+
executor::block_on(context.close());
632+
assert_eq!(context.state(), AudioContextState::Closed);
633+
634+
let time4 = context.current_time();
635+
636+
// allow some time to progress
637+
std::thread::sleep(std::time::Duration::from_millis(1));
638+
639+
let time5 = context.current_time();
640+
assert_eq!(time5, time4); // no progression of time
641+
}
642+
}

0 commit comments

Comments
 (0)