Skip to main content

web_audio_api/context/
online.rs

1//! The `AudioContext` type and constructor options
2use std::error::Error;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::{Arc, Mutex};
5
6#[cfg(feature = "diagnostics")]
7use crate::context::{AudioBackendDiagnostics, AudioContextDiagnostics};
8use crate::context::{AudioContextState, BaseAudioContext, ConcreteBaseAudioContext};
9#[cfg(feature = "diagnostics")]
10use crate::events::EventPayload;
11use crate::events::{EventDispatch, EventHandler, EventLoop, EventType};
12use crate::io::{self, AudioBackendManager, ControlThreadInit, NoneBackend, RenderThreadInit};
13use crate::media_devices::{enumerate_devices_sync, MediaDeviceInfoKind};
14use crate::media_streams::{MediaStream, MediaStreamTrack};
15use crate::message::{ControlMessage, OneshotNotify};
16use crate::node::{self, AudioNodeOptions};
17use crate::render::graph::Graph;
18use crate::MediaElement;
19use crate::{is_valid_sample_rate, AudioPlaybackStats, AudioRenderCapacity, Event};
20
21use futures_channel::oneshot;
22
23/// Check if the provided sink_id is available for playback
24///
25/// It should be "", "none" or a valid output `sinkId` returned from [`enumerate_devices_sync`]
26fn is_valid_sink_id(sink_id: &str) -> bool {
27    if sink_id.is_empty() || sink_id == "none" {
28        true
29    } else {
30        enumerate_devices_sync()
31            .into_iter()
32            .filter(|d| d.kind() == MediaDeviceInfoKind::AudioOutput)
33            .any(|d| d.device_id() == sink_id)
34    }
35}
36
37#[derive(Debug)]
38enum AudioContextError {
39    SinkNotFound { sink_id: String },
40    InvalidSampleRate { sample_rate: f32 },
41    Backend { error: io::AudioBackendError },
42}
43
44impl std::fmt::Display for AudioContextError {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        match self {
47            Self::SinkNotFound { sink_id } => {
48                write!(f, "NotFoundError - Invalid sinkId: {sink_id:?}")
49            }
50            Self::InvalidSampleRate { sample_rate } => {
51                write!(
52                    f,
53                    "NotSupportedError - Invalid sample rate: {sample_rate}, should be in the range [3000.0, 768000.0]"
54                )
55            }
56            Self::Backend { error } => write!(f, "InvalidStateError - {error}"),
57        }
58    }
59}
60
61impl Error for AudioContextError {}
62
63impl From<io::AudioBackendError> for AudioContextError {
64    fn from(error: io::AudioBackendError) -> Self {
65        Self::Backend { error }
66    }
67}
68
69/// Identify the type of playback, which affects tradeoffs
70/// between audio output latency and power consumption
71#[derive(Copy, Clone, Debug, Default)]
72pub enum AudioContextLatencyCategory {
73    /// Balance audio output latency and power consumption.
74    Balanced,
75    /// Provide the lowest audio output latency possible without glitching. This is the default.
76    #[default]
77    Interactive,
78    /// Prioritize sustained playback without interruption over audio output latency.
79    ///
80    /// Lowest power consumption.
81    Playback,
82    /// Specify the number of seconds of latency
83    ///
84    /// This latency is not guaranteed to be applied, it depends on the audio hardware capabilities
85    Custom(f64),
86}
87
88#[derive(Copy, Clone, Debug)]
89#[non_exhaustive]
90/// This allows users to ask for a particular render quantum size.
91///
92/// Currently, only the default value is available
93#[derive(Default)]
94pub enum AudioContextRenderSizeCategory {
95    /// The default value of 128 frames
96    #[default]
97    Default,
98}
99
100/// Specify the playback configuration for the [`AudioContext`] constructor.
101///
102/// All fields are optional and will default to the value best suited for interactive playback on
103/// your hardware configuration.
104///
105/// For future compatibility, it is best to construct a default implementation of this struct and
106/// set the fields you would like to override:
107/// ```
108/// use web_audio_api::context::AudioContextOptions;
109///
110/// // Request a sample rate of 44.1 kHz, leave other fields to their default values
111/// let opts = AudioContextOptions {
112///     sample_rate: Some(44100.),
113///     ..AudioContextOptions::default()
114/// };
115#[derive(Clone, Debug, Default)]
116pub struct AudioContextOptions {
117    /// Identify the type of playback, which affects tradeoffs between audio output latency and
118    /// power consumption.
119    pub latency_hint: AudioContextLatencyCategory,
120
121    /// Sample rate of the audio context and audio output hardware. Use `None` for a default value.
122    pub sample_rate: Option<f32>,
123
124    /// The audio output device
125    /// - use `""` for the default audio output device
126    /// - use `"none"` to process the audio graph without playing through an audio output device.
127    /// - use `"sinkId"` to use the specified audio sink id, obtained with [`enumerate_devices_sync`]
128    pub sink_id: String,
129
130    /// Option to request a default, optimized or specific render quantum size. It is a hint that might not be honored.
131    pub render_size_hint: AudioContextRenderSizeCategory,
132}
133
134/// This interface represents an audio graph whose `AudioDestinationNode` is routed to a real-time
135/// output device that produces a signal directed at the user.
136// the naming comes from the web audio specification
137#[allow(clippy::module_name_repetitions)]
138pub struct AudioContext {
139    /// represents the underlying `BaseAudioContext`
140    base: ConcreteBaseAudioContext,
141    /// audio backend (play/pause functionality)
142    backend_manager: Mutex<Box<dyn AudioBackendManager>>,
143    /// Provider for rendering performance metrics
144    render_capacity: AudioRenderCapacity,
145    /// Provider for playback statistics
146    playback_stats: AudioPlaybackStats,
147    /// true while the render thread has not yet processed its initial Startup message
148    startup_pending: std::sync::Arc<AtomicBool>,
149    /// Initializer for the render thread (when restart is required)
150    render_thread_init: RenderThreadInit,
151}
152
153impl std::fmt::Debug for AudioContext {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        f.debug_struct("AudioContext")
156            .field("sink_id", &self.sink_id())
157            .field("base_latency", &self.base_latency())
158            .field("output_latency", &self.output_latency())
159            .field("base", &self.base())
160            .finish_non_exhaustive()
161    }
162}
163
164impl Drop for AudioContext {
165    fn drop(&mut self) {
166        // Continue playing the stream if the AudioContext goes out of scope
167        if self.state() == AudioContextState::Running {
168            let tombstone = Box::new(NoneBackend::void());
169            let original = std::mem::replace(self.backend_manager.get_mut().unwrap(), tombstone);
170            Box::leak(original);
171        }
172    }
173}
174
175impl BaseAudioContext for AudioContext {
176    fn base(&self) -> &ConcreteBaseAudioContext {
177        &self.base
178    }
179}
180
181impl Default for AudioContext {
182    fn default() -> Self {
183        Self::new(AudioContextOptions::default())
184    }
185}
186
187impl AudioContext {
188    /// Creates and returns a new `AudioContext` object.
189    ///
190    /// This will play live audio on the default output device.
191    ///
192    /// ```no_run
193    /// use web_audio_api::context::{AudioContext, AudioContextOptions};
194    ///
195    /// // Request a sample rate of 44.1 kHz and default latency (buffer size 128, if available)
196    /// let opts = AudioContextOptions {
197    ///     sample_rate: Some(44100.),
198    ///     ..AudioContextOptions::default()
199    /// };
200    ///
201    /// // Setup the audio context that will emit to your speakers
202    /// let context = AudioContext::new(opts);
203    ///
204    /// // Alternatively, use the default constructor to get the best settings for your hardware
205    /// // let context = AudioContext::default();
206    /// ```
207    ///
208    /// # Panics
209    ///
210    /// The `AudioContext` constructor will panic when an invalid `sinkId` is provided in the
211    /// `AudioContextOptions`, when the sample rate is outside the valid range [3000.0, 768000.0],
212    /// or when the selected audio backend cannot create or start the output stream. Use
213    /// [`Self::try_new`] to handle these errors without panicking.
214    #[must_use]
215    pub fn new(options: AudioContextOptions) -> Self {
216        Self::try_new_inner(options).unwrap_or_else(|e| panic!("{e}"))
217    }
218
219    /// Creates and returns a new `AudioContext` object.
220    ///
221    /// This will play live audio on the requested output device and returns backend errors instead
222    /// of panicking when the stream cannot be created.
223    ///
224    /// # Errors
225    ///
226    /// Returns an error when the sink id is invalid, the sample rate is outside the valid range
227    /// [3000.0, 768000.0], or when the selected audio backend cannot create or start the output
228    /// stream.
229    pub fn try_new(options: AudioContextOptions) -> Result<Self, Box<dyn Error>> {
230        Self::try_new_inner(options).map_err(Into::into)
231    }
232
233    fn try_new_inner(options: AudioContextOptions) -> Result<Self, AudioContextError> {
234        // https://webaudio.github.io/web-audio-api/#validating-sink-identifier
235        if !is_valid_sink_id(&options.sink_id) {
236            return Err(AudioContextError::SinkNotFound {
237                sink_id: options.sink_id,
238            });
239        }
240
241        // Validate sample_rate if provided
242        // https://webaudio.github.io/web-audio-api/#sample-rates
243        if let Some(sample_rate) = options.sample_rate {
244            if !is_valid_sample_rate(sample_rate) {
245                return Err(AudioContextError::InvalidSampleRate { sample_rate });
246            }
247        }
248
249        // Set up the audio output thread
250        let (control_thread_init, render_thread_init) = io::thread_init();
251        let startup_pending = Arc::clone(&render_thread_init.startup_pending);
252        let backend = io::build_output(options, render_thread_init.clone())?;
253
254        let ControlThreadInit {
255            state,
256            frames_played,
257            stats,
258            ctrl_msg_send,
259            event_send,
260            event_recv,
261        } = control_thread_init;
262
263        // Construct the audio Graph and hand it to the render thread
264        let (node_id_producer, node_id_consumer) = llq::Queue::new().split();
265        let graph = Graph::new(node_id_producer);
266        let message = ControlMessage::Startup { graph };
267        ctrl_msg_send.send(message).unwrap();
268
269        // Set up the event loop thread that handles the events spawned by the render thread
270        let event_loop = EventLoop::new(event_recv);
271
272        // Put everything together in the BaseAudioContext (shared with offline context)
273        let base = ConcreteBaseAudioContext::new(
274            backend.sample_rate(),
275            backend.number_of_channels(),
276            state,
277            frames_played,
278            ctrl_msg_send,
279            event_send,
280            event_loop.clone(),
281            false,
282            node_id_consumer,
283        );
284
285        // Setup AudioRenderCapacity for this context
286        let render_capacity = AudioRenderCapacity::new(base.clone(), stats.clone());
287        let playback_stats = AudioPlaybackStats::new(base.clone(), stats);
288
289        // As the final step, spawn a thread for the event loop. If we do this earlier we may miss
290        // event handling of the initial events that are emitted right after render thread
291        // construction.
292        event_loop.run_in_thread();
293
294        Ok(Self {
295            base,
296            backend_manager: Mutex::new(backend),
297            render_capacity,
298            playback_stats,
299            startup_pending,
300            render_thread_init,
301        })
302    }
303
304    /// This represents the number of seconds of processing latency incurred by
305    /// the `AudioContext` passing the audio from the `AudioDestinationNode`
306    /// to the audio subsystem.
307    // We don't do any buffering between rendering the audio and sending
308    // it to the audio subsystem, so this value is zero. (see Gecko)
309    #[allow(clippy::unused_self)]
310    #[must_use]
311    pub fn base_latency(&self) -> f64 {
312        0.
313    }
314
315    /// The estimation in seconds of audio output latency, i.e., the interval
316    /// between the time the UA requests the host system to play a buffer and
317    /// the time at which the first sample in the buffer is actually processed
318    /// by the audio output device.
319    #[must_use]
320    #[allow(clippy::missing_panics_doc)]
321    pub fn output_latency(&self) -> f64 {
322        self.try_output_latency()
323            .unwrap_or_else(|e| panic!("InvalidStateError - {e}"))
324    }
325
326    /// The estimation in seconds of audio output latency.
327    ///
328    /// # Errors
329    ///
330    /// Returns an error when the selected audio backend cannot query the output latency.
331    fn try_output_latency(&self) -> Result<f64, Box<dyn Error>> {
332        Ok(self.backend_manager.lock().unwrap().output_latency()?)
333    }
334
335    /// Identifier or the information of the current audio output device.
336    ///
337    /// The initial value is `""`, which means the default audio output device.
338    #[allow(clippy::missing_panics_doc)]
339    pub fn sink_id(&self) -> String {
340        self.backend_manager.lock().unwrap().sink_id().to_owned()
341    }
342
343    /// Returns an [`AudioRenderCapacity`] instance associated with an AudioContext.
344    #[must_use]
345    pub fn render_capacity(&self) -> AudioRenderCapacity {
346        self.render_capacity.clone()
347    }
348
349    /// Returns an [`AudioPlaybackStats`] instance associated with this `AudioContext`.
350    #[must_use]
351    pub fn playback_stats(&self) -> AudioPlaybackStats {
352        self.playback_stats.clone()
353    }
354
355    /// Update the current audio output device.
356    ///
357    /// The provided `sink_id` string must match a device name `enumerate_devices_sync`.
358    ///
359    /// Supplying `"none"` for the `sink_id` will process the audio graph without playing through an
360    /// audio output device.
361    ///
362    /// This function operates synchronously and might block the current thread. An async version
363    /// is currently not implemented.
364    #[allow(clippy::needless_collect, clippy::missing_panics_doc)]
365    pub fn set_sink_id_sync(&self, sink_id: String) -> Result<(), Box<dyn Error>> {
366        log::debug!("SinkChange requested");
367        if self.sink_id() == sink_id {
368            log::debug!("SinkChange: no-op");
369            return Ok(()); // sink is already active
370        }
371
372        if !is_valid_sink_id(&sink_id) {
373            Err(format!("NotFoundError: invalid sinkId {sink_id}"))?;
374        };
375
376        log::debug!("SinkChange: locking backend manager");
377        let mut backend_manager_guard = self.backend_manager.lock().unwrap();
378        let original_state = self.state();
379        if original_state == AudioContextState::Closed {
380            log::debug!("SinkChange: context is closed");
381            return Ok(());
382        }
383
384        // Acquire exclusive lock on ctrl msg sender
385        log::debug!("SinkChange: locking message channel");
386        let ctrl_msg_send = self.base.lock_control_msg_sender();
387
388        // Flush out the ctrl msg receiver, cache
389        let mut pending_msgs: Vec<_> = self.render_thread_init.ctrl_msg_recv.try_iter().collect();
390
391        // Acquire the active audio graph from the current render thread, shutting it down
392        let graph = if matches!(pending_msgs.first(), Some(ControlMessage::Startup { .. })) {
393            // Handle the edge case where the previous backend was suspended for its entire lifetime.
394            // In this case, the `Startup` control message was never processed.
395            log::debug!("SinkChange: recover unstarted graph");
396
397            let msg = pending_msgs.remove(0);
398            match msg {
399                ControlMessage::Startup { graph } => graph,
400                _ => unreachable!(),
401            }
402        } else {
403            // Acquire the audio graph from the current render thread, shutting it down
404            log::debug!("SinkChange: recover graph from render thread");
405
406            let (graph_send, graph_recv) = crossbeam_channel::bounded(1);
407            let message = ControlMessage::CloseAndRecycle { sender: graph_send };
408            ctrl_msg_send.send(message).unwrap();
409            if original_state == AudioContextState::Suspended {
410                // We must wake up the render thread to be able to handle the shutdown.
411                // No new audio will be produced because it will receive the shutdown command first.
412                backend_manager_guard.resume()?;
413            }
414            graph_recv.recv().unwrap()
415        };
416
417        log::debug!("SinkChange: closing audio stream");
418        backend_manager_guard.close()?;
419
420        // hotswap the backend
421        let options = AudioContextOptions {
422            sample_rate: Some(self.sample_rate()),
423            latency_hint: AudioContextLatencyCategory::default(), // todo reuse existing setting
424            sink_id,
425            render_size_hint: AudioContextRenderSizeCategory::default(), // todo reuse existing setting
426        };
427        log::debug!("SinkChange: starting audio stream");
428        *backend_manager_guard = io::build_output(options, self.render_thread_init.clone())?;
429
430        // if the previous backend state was suspend, suspend the new one before shipping the graph
431        if original_state == AudioContextState::Suspended {
432            log::debug!("SinkChange: suspending audio stream");
433            backend_manager_guard.suspend()?;
434        }
435
436        // send the audio graph to the new render thread
437        let message = ControlMessage::Startup { graph };
438        ctrl_msg_send.send(message).unwrap();
439
440        // flush the cached msgs
441        pending_msgs
442            .into_iter()
443            .for_each(|m| self.base().send_control_msg(m));
444
445        // explicitly release the lock to prevent concurrent render threads
446        drop(backend_manager_guard);
447
448        // trigger event when all the work is done
449        let _ = self.base.send_event(EventDispatch::sink_change());
450
451        log::debug!("SinkChange: done");
452        Ok(())
453    }
454
455    /// Register callback to run when the audio sink has changed
456    ///
457    /// Only a single event handler is active at any time. Calling this method multiple times will
458    /// override the previous event handler.
459    pub fn set_onsinkchange<F: FnMut(Event) + Send + 'static>(&self, mut callback: F) {
460        let callback = move |_| {
461            callback(Event {
462                type_: "sinkchange",
463            })
464        };
465
466        self.base().set_event_handler(
467            EventType::SinkChange,
468            EventHandler::Multiple(Box::new(callback)),
469        );
470    }
471
472    /// Unset the callback to run when the audio sink has changed
473    pub fn clear_onsinkchange(&self) {
474        self.base().clear_event_handler(EventType::SinkChange);
475    }
476
477    /// Request a structured diagnostic report of the audio context.
478    ///
479    /// The report is collected asynchronously: backend details are captured on the control thread,
480    /// while render thread and graph details are captured in the realtime render thread. The
481    /// callback is invoked once on the event loop thread.
482    ///
483    /// This API is available with the `diagnostics` crate feature.
484    #[cfg(feature = "diagnostics")]
485    #[allow(clippy::missing_panics_doc)]
486    pub fn run_diagnostics<F: Fn(AudioContextDiagnostics) + Send + 'static>(&self, callback: F) {
487        let backend = {
488            let backend = self.backend_manager.lock().unwrap();
489            AudioBackendDiagnostics {
490                name: backend.name().to_string(),
491                sink_id: backend.sink_id().to_string(),
492                output_latency: backend.output_latency().ok(),
493            }
494        };
495
496        let callback = move |v| match v {
497            EventPayload::Diagnostics(v) => {
498                callback(v);
499            }
500            _ => unreachable!(),
501        };
502
503        self.base().set_event_handler(
504            EventType::Diagnostics,
505            EventHandler::Once(Box::new(callback)),
506        );
507
508        self.base()
509            .send_control_msg(ControlMessage::RunDiagnostics { backend });
510    }
511
512    /// Suspends the progression of time in the audio context.
513    ///
514    /// This will temporarily halt audio hardware access and reducing CPU/battery usage in the
515    /// process.
516    ///
517    /// # Panics
518    ///
519    /// Will panic if:
520    ///
521    /// * The audio device is not available
522    /// * For a `BackendSpecificError`
523    pub async fn suspend(&self) {
524        // Don't lock the backend manager because we can't hold is across the await point
525        log::debug!("Suspend called");
526
527        let state = self.state();
528        if state == AudioContextState::Closed {
529            log::debug!("Suspend no-op - context is closed");
530            return;
531        }
532
533        if state != AudioContextState::Running && !self.startup_pending.load(Ordering::Acquire) {
534            log::debug!("Suspend no-op - context is not running");
535            return;
536        }
537
538        // Pause rendering via a control message
539        let (sender, receiver) = oneshot::channel();
540        let notify = OneshotNotify::Async(sender);
541        self.base
542            .suspend_control_msgs(ControlMessage::Suspend { notify });
543
544        // Wait for the render thread to have processed the suspend message.
545        // The AudioContextState will be updated by the render thread.
546        log::debug!("Suspending audio graph, waiting for signal..");
547        receiver.await.unwrap();
548
549        // Then ask the audio host to suspend the stream
550        log::debug!("Suspended audio graph. Suspending audio stream..");
551        self.backend_manager
552            .lock()
553            .unwrap()
554            .suspend()
555            .unwrap_or_else(|e| panic!("InvalidStateError - {e}"));
556
557        log::debug!("Suspended audio stream");
558    }
559
560    /// Resumes the progression of time in an audio context that has previously been
561    /// suspended/paused.
562    ///
563    /// # Panics
564    ///
565    /// Will panic if:
566    ///
567    /// * The audio device is not available
568    /// * For a `BackendSpecificError`
569    pub async fn resume(&self) {
570        let (sender, receiver) = oneshot::channel();
571
572        {
573            // Lock the backend manager mutex to avoid concurrent calls
574            log::debug!("Resume called, locking backend manager");
575            let backend_manager_guard = self.backend_manager.lock().unwrap();
576
577            if self.state() != AudioContextState::Suspended {
578                log::debug!("Resume no-op - context is not suspended");
579                return;
580            }
581
582            // Ask the audio host to resume the stream
583            backend_manager_guard
584                .resume()
585                .unwrap_or_else(|e| panic!("InvalidStateError - {e}"));
586
587            // Then, ask to resume rendering via a control message
588            log::debug!("Resumed audio stream, waking audio graph");
589            let notify = OneshotNotify::Async(sender);
590            self.base
591                .resume_control_msgs(ControlMessage::Resume { notify });
592
593            // Drop the Mutex guard so we won't hold it across an await point
594        }
595
596        // Wait for the render thread to have processed the resume message
597        // The AudioContextState will be updated by the render thread.
598        receiver.await.unwrap();
599        log::debug!("Resumed audio graph");
600    }
601
602    /// Closes the `AudioContext`, releasing the system resources being used.
603    ///
604    /// This will not automatically release all `AudioContext`-created objects, but will suspend
605    /// the progression of the currentTime, and stop processing audio data.
606    ///
607    /// # Panics
608    ///
609    /// Will panic when this function is called multiple times
610    pub async fn close(&self) {
611        // Don't lock the backend manager because we can't hold is across the await point
612        log::debug!("Close called");
613
614        if self.state() == AudioContextState::Closed {
615            log::debug!("Close no-op - context is already closed");
616            return;
617        }
618
619        // Stop AudioRenderCapacity before closing so no capacity events are queued during shutdown.
620        self.render_capacity.stop();
621
622        if self.state() == AudioContextState::Running {
623            // First, stop rendering via a control message
624            let (sender, receiver) = oneshot::channel();
625            let notify = OneshotNotify::Async(sender);
626            self.base.send_control_msg(ControlMessage::Close { notify });
627
628            // Wait for the render thread to have processed the suspend message.
629            // The AudioContextState will be updated by the render thread.
630            log::debug!("Suspending audio graph, waiting for signal..");
631            receiver.await.unwrap();
632        } else {
633            // if the context is not running, change the state manually
634            self.base.set_state(AudioContextState::Closed);
635        }
636
637        // Then ask the audio host to close the stream
638        log::debug!("Suspended audio graph. Closing audio stream..");
639        self.backend_manager
640            .lock()
641            .unwrap()
642            .close()
643            .unwrap_or_else(|e| panic!("InvalidStateError - {e}"));
644
645        log::debug!("Closed audio stream");
646    }
647
648    /// Suspends the progression of time in the audio context.
649    ///
650    /// This will temporarily halt audio hardware access and reducing CPU/battery usage in the
651    /// process.
652    ///
653    /// This function operates synchronously and blocks the current thread until the audio thread
654    /// has stopped processing.
655    ///
656    /// # Panics
657    ///
658    /// Will panic if:
659    ///
660    /// * The audio device is not available
661    /// * For a `BackendSpecificError`
662    pub fn suspend_sync(&self) {
663        // Lock the backend manager mutex to avoid concurrent calls
664        log::debug!("Suspend_sync called, locking backend manager");
665        let backend_manager_guard = self.backend_manager.lock().unwrap();
666
667        let state = self.state();
668        if state == AudioContextState::Closed {
669            log::debug!("Suspend_sync no-op - context is closed");
670            return;
671        }
672
673        if state != AudioContextState::Running && !self.startup_pending.load(Ordering::Acquire) {
674            log::debug!("Suspend_sync no-op - context is not running");
675            return;
676        }
677
678        // Pause rendering via a control message
679        let (sender, receiver) = crossbeam_channel::bounded(0);
680        let notify = OneshotNotify::Sync(sender);
681        self.base
682            .suspend_control_msgs(ControlMessage::Suspend { notify });
683
684        // Wait for the render thread to have processed the suspend message.
685        // The AudioContextState will be updated by the render thread.
686        log::debug!("Suspending audio graph, waiting for signal..");
687        receiver.recv().ok();
688
689        // Then ask the audio host to suspend the stream
690        log::debug!("Suspended audio graph. Suspending audio stream..");
691        backend_manager_guard
692            .suspend()
693            .unwrap_or_else(|e| panic!("InvalidStateError - {e}"));
694
695        log::debug!("Suspended audio stream");
696    }
697
698    /// Resumes the progression of time in an audio context that has previously been
699    /// suspended/paused.
700    ///
701    /// This function operates synchronously and blocks the current thread until the audio thread
702    /// has started processing again.
703    ///
704    /// # Panics
705    ///
706    /// Will panic if:
707    ///
708    /// * The audio device is not available
709    /// * For a `BackendSpecificError`
710    pub fn resume_sync(&self) {
711        // Lock the backend manager mutex to avoid concurrent calls
712        log::debug!("Resume_sync called, locking backend manager");
713        let backend_manager_guard = self.backend_manager.lock().unwrap();
714
715        if self.state() != AudioContextState::Suspended {
716            log::debug!("Resume no-op - context is not suspended");
717            return;
718        }
719
720        // Ask the audio host to resume the stream
721        backend_manager_guard
722            .resume()
723            .unwrap_or_else(|e| panic!("InvalidStateError - {e}"));
724
725        // Then, ask to resume rendering via a control message
726        log::debug!("Resumed audio stream, waking audio graph");
727        let (sender, receiver) = crossbeam_channel::bounded(0);
728        let notify = OneshotNotify::Sync(sender);
729        self.base
730            .resume_control_msgs(ControlMessage::Resume { notify });
731
732        // Wait for the render thread to have processed the resume message
733        // The AudioContextState will be updated by the render thread.
734        receiver.recv().ok();
735        log::debug!("Resumed audio graph");
736    }
737
738    /// Closes the `AudioContext`, releasing the system resources being used.
739    ///
740    /// This will not automatically release all `AudioContext`-created objects, but will suspend
741    /// the progression of the currentTime, and stop processing audio data.
742    ///
743    /// This function operates synchronously and blocks the current thread until the audio thread
744    /// has stopped processing.
745    ///
746    /// # Panics
747    ///
748    /// Will panic when this function is called multiple times
749    pub fn close_sync(&self) {
750        // Lock the backend manager mutex to avoid concurrent calls
751        log::debug!("Close_sync called, locking backend manager");
752        let backend_manager_guard = self.backend_manager.lock().unwrap();
753
754        if self.state() == AudioContextState::Closed {
755            log::debug!("Close no-op - context is already closed");
756            return;
757        }
758
759        // Stop AudioRenderCapacity before closing so no capacity events are queued during shutdown.
760        self.render_capacity.stop();
761
762        // First, stop rendering via a control message
763        if self.state() == AudioContextState::Running {
764            let (sender, receiver) = crossbeam_channel::bounded(0);
765            let notify = OneshotNotify::Sync(sender);
766            self.base.send_control_msg(ControlMessage::Close { notify });
767
768            // Wait for the render thread to have processed the suspend message.
769            // The AudioContextState will be updated by the render thread.
770            log::debug!("Suspending audio graph, waiting for signal..");
771            receiver.recv().ok();
772        } else {
773            // if the context is not running, change the state manually
774            self.base.set_state(AudioContextState::Closed);
775        }
776
777        // Then ask the audio host to close the stream
778        log::debug!("Suspended audio graph. Closing audio stream..");
779        backend_manager_guard
780            .close()
781            .unwrap_or_else(|e| panic!("InvalidStateError - {e}"));
782
783        log::debug!("Closed audio stream");
784    }
785
786    /// Creates a [`MediaStreamAudioSourceNode`](node::MediaStreamAudioSourceNode) from a
787    /// [`MediaStream`]
788    #[must_use]
789    pub fn create_media_stream_source(
790        &self,
791        media: &MediaStream,
792    ) -> node::MediaStreamAudioSourceNode {
793        let opts = node::MediaStreamAudioSourceOptions {
794            media_stream: media,
795        };
796        node::MediaStreamAudioSourceNode::new(self, opts)
797    }
798
799    /// Creates a [`MediaStreamAudioDestinationNode`](node::MediaStreamAudioDestinationNode)
800    #[must_use]
801    pub fn create_media_stream_destination(&self) -> node::MediaStreamAudioDestinationNode {
802        let opts = AudioNodeOptions::default();
803        node::MediaStreamAudioDestinationNode::new(self, opts)
804    }
805
806    /// Creates a [`MediaStreamTrackAudioSourceNode`](node::MediaStreamTrackAudioSourceNode) from a
807    /// [`MediaStreamTrack`]
808    #[must_use]
809    pub fn create_media_stream_track_source(
810        &self,
811        media: &MediaStreamTrack,
812    ) -> node::MediaStreamTrackAudioSourceNode {
813        let opts = node::MediaStreamTrackAudioSourceOptions {
814            media_stream_track: media,
815        };
816        node::MediaStreamTrackAudioSourceNode::new(self, opts)
817    }
818
819    /// Creates a [`MediaElementAudioSourceNode`](node::MediaElementAudioSourceNode) from a
820    /// [`MediaElement`]
821    #[must_use]
822    pub fn create_media_element_source(
823        &self,
824        media_element: &mut MediaElement,
825    ) -> node::MediaElementAudioSourceNode {
826        let opts = node::MediaElementAudioSourceOptions { media_element };
827        node::MediaElementAudioSourceNode::new(self, opts)
828    }
829}
830
831#[cfg(test)]
832mod tests {
833    use super::*;
834    #[cfg(feature = "diagnostics")]
835    use crate::context::DESTINATION_NODE_ID;
836    use futures::executor;
837
838    #[test]
839    fn test_suspend_resume_close() {
840        let options = AudioContextOptions {
841            sink_id: "none".into(),
842            ..AudioContextOptions::default()
843        };
844
845        // construct with 'none' sink_id
846        let context = AudioContext::new(options);
847
848        // Ensure startup has been processed before testing suspend/resume transitions.
849        executor::block_on(context.resume());
850        assert_eq!(context.state(), AudioContextState::Running);
851
852        executor::block_on(context.suspend());
853        assert_eq!(context.state(), AudioContextState::Suspended);
854        let time1 = context.current_time();
855        assert!(time1 >= 0.);
856
857        // allow some time to progress
858        std::thread::sleep(std::time::Duration::from_millis(1));
859        let time2 = context.current_time();
860        assert_eq!(time1, time2); // no progression of time
861
862        executor::block_on(context.resume());
863        assert_eq!(context.state(), AudioContextState::Running);
864
865        // allow some time to progress
866        std::thread::sleep(std::time::Duration::from_millis(1));
867
868        let time3 = context.current_time();
869        assert!(time3 > time2); // time is progressing
870
871        executor::block_on(context.close());
872        assert_eq!(context.state(), AudioContextState::Closed);
873
874        let time4 = context.current_time();
875
876        // allow some time to progress
877        std::thread::sleep(std::time::Duration::from_millis(1));
878
879        let time5 = context.current_time();
880        assert_eq!(time5, time4); // no progression of time
881    }
882
883    #[test]
884    fn test_suspend_during_startup() {
885        let options = AudioContextOptions {
886            sink_id: "none".into(),
887            ..AudioContextOptions::default()
888        };
889
890        let context = AudioContext::new(options);
891
892        executor::block_on(context.suspend());
893        assert_eq!(context.state(), AudioContextState::Suspended);
894
895        let time1 = context.current_time();
896        std::thread::sleep(std::time::Duration::from_millis(5));
897        let time2 = context.current_time();
898        assert_eq!(time1, time2);
899    }
900
901    #[test]
902    fn test_suspend_sync_during_startup() {
903        let options = AudioContextOptions {
904            sink_id: "none".into(),
905            ..AudioContextOptions::default()
906        };
907
908        let context = AudioContext::new(options);
909
910        context.suspend_sync();
911        assert_eq!(context.state(), AudioContextState::Suspended);
912
913        let time1 = context.current_time();
914        std::thread::sleep(std::time::Duration::from_millis(5));
915        let time2 = context.current_time();
916        assert_eq!(time1, time2);
917    }
918
919    fn require_send_sync<T: Send + Sync>(_: T) {}
920
921    #[test]
922    fn test_all_futures_thread_safe() {
923        let options = AudioContextOptions {
924            sink_id: "none".into(),
925            ..AudioContextOptions::default()
926        };
927        let context = AudioContext::new(options);
928
929        require_send_sync(context.suspend());
930        require_send_sync(context.resume());
931        require_send_sync(context.close());
932    }
933
934    #[test]
935    fn test_try_new_invalid_sample_rate() {
936        let options = AudioContextOptions {
937            sample_rate: Some(0.),
938            sink_id: "none".into(),
939            ..AudioContextOptions::default()
940        };
941
942        let result = AudioContext::try_new(options);
943        assert!(result.is_err());
944        let error_msg = result.unwrap_err().to_string();
945        assert!(error_msg.contains("Invalid sample rate"));
946    }
947
948    #[test]
949    #[should_panic]
950    fn test_invalid_sink_id() {
951        let options = AudioContextOptions {
952            sink_id: "invalid".into(),
953            ..AudioContextOptions::default()
954        };
955        let _ = AudioContext::new(options);
956    }
957
958    #[test]
959    fn test_try_new_invalid_sink_id() {
960        let options = AudioContextOptions {
961            sink_id: "invalid".into(),
962            ..AudioContextOptions::default()
963        };
964
965        let error = AudioContext::try_new(options).unwrap_err();
966        assert_eq!(
967            error.to_string(),
968            "NotFoundError - Invalid sinkId: \"invalid\""
969        );
970    }
971
972    #[cfg(feature = "diagnostics")]
973    #[test]
974    fn test_run_diagnostics_returns_structured_output() {
975        let options = AudioContextOptions {
976            sink_id: "none".into(),
977            ..AudioContextOptions::default()
978        };
979        let context = AudioContext::new(options);
980        let (sender, receiver) = std::sync::mpsc::channel();
981
982        context.run_diagnostics(move |diagnostics| {
983            sender.send(diagnostics).unwrap();
984        });
985
986        let diagnostics = receiver
987            .recv_timeout(std::time::Duration::from_secs(1))
988            .unwrap();
989
990        assert!(diagnostics.backend.name.contains("NoneBackend"));
991        assert_eq!(diagnostics.backend.sink_id, "none");
992        assert_eq!(diagnostics.backend.output_latency, Some(0.));
993        assert_eq!(diagnostics.render_thread.sample_rate, context.sample_rate());
994        assert_eq!(
995            diagnostics.render_thread.number_of_channels,
996            crate::MAX_CHANNELS
997        );
998        assert!(diagnostics.graph.active);
999        assert_eq!(diagnostics.graph.node_count, diagnostics.graph.nodes.len());
1000        assert_eq!(diagnostics.graph.edge_count, 0);
1001        assert!(diagnostics.graph.in_cycle.is_empty());
1002        assert!(diagnostics.graph.cycle_breakers.is_empty());
1003        assert!(diagnostics
1004            .graph
1005            .nodes
1006            .iter()
1007            .any(|node| node.id == DESTINATION_NODE_ID.0
1008                && node.inputs == node.input_channels.len()
1009                && node.outputs == node.output_channels.len()));
1010    }
1011}