web_audio_api/
capacity.rs

1use crossbeam_channel::{Receiver, Sender};
2use std::sync::{Arc, Mutex};
3
4use crate::context::{BaseAudioContext, ConcreteBaseAudioContext};
5use crate::events::{EventDispatch, EventHandler, EventPayload, EventType};
6use crate::Event;
7
8#[derive(Copy, Clone, Debug)]
9pub(crate) struct AudioRenderCapacityLoad {
10    pub render_timestamp: f64,
11    pub load_value: f64,
12}
13
14/// Options for constructing an `AudioRenderCapacity`
15#[derive(Clone, Debug)]
16pub struct AudioRenderCapacityOptions {
17    /// An update interval (in seconds) for dispatching [`AudioRenderCapacityEvent`]s
18    pub update_interval: f64,
19}
20
21impl Default for AudioRenderCapacityOptions {
22    fn default() -> Self {
23        Self {
24            update_interval: 1.,
25        }
26    }
27}
28
29/// Performance metrics of the rendering thread
30#[derive(Clone, Debug)]
31pub struct AudioRenderCapacityEvent {
32    /// The start time of the data collection period in terms of the associated AudioContext's currentTime
33    pub timestamp: f64,
34    /// An average of collected load values over the given update interval
35    pub average_load: f64,
36    /// A maximum value from collected load values over the given update interval.
37    pub peak_load: f64,
38    /// A ratio between the number of buffer underruns and the total number of system-level audio callbacks over the given update interval.
39    pub underrun_ratio: f64,
40    /// Inherits from this base Event
41    pub event: Event,
42}
43
44impl AudioRenderCapacityEvent {
45    fn new(timestamp: f64, average_load: f64, peak_load: f64, underrun_ratio: f64) -> Self {
46        // We are limiting the precision here conform
47        // https://webaudio.github.io/web-audio-api/#dom-audiorendercapacityevent-averageload
48        Self {
49            timestamp,
50            average_load: (average_load * 100.).round() / 100.,
51            peak_load: (peak_load * 100.).round() / 100.,
52            underrun_ratio: (underrun_ratio * 100.).ceil() / 100.,
53            event: Event {
54                type_: "AudioRenderCapacityEvent",
55            },
56        }
57    }
58}
59
60/// Provider for rendering performance metrics
61///
62/// A load value is computed for each system-level audio callback, by dividing its execution
63/// duration by the system-level audio callback buffer size divided by the sample rate.
64///
65/// Ideally the load value is below 1.0, meaning that it took less time to render the audio than it
66/// took to play it out. An audio buffer underrun happens when this load value is greater than 1.0: the
67/// system could not render audio fast enough for real-time.
68#[derive(Clone)]
69pub struct AudioRenderCapacity {
70    context: ConcreteBaseAudioContext,
71    receiver: Receiver<AudioRenderCapacityLoad>,
72    stop_send: Arc<Mutex<Option<Sender<()>>>>,
73}
74
75impl std::fmt::Debug for AudioRenderCapacity {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.debug_struct("AudioRenderCapacity")
78            .field(
79                "context",
80                &format!("BaseAudioContext@{}", self.context.address()),
81            )
82            .finish_non_exhaustive()
83    }
84}
85
86impl AudioRenderCapacity {
87    pub(crate) fn new(
88        context: ConcreteBaseAudioContext,
89        receiver: Receiver<AudioRenderCapacityLoad>,
90    ) -> Self {
91        let stop_send = Arc::new(Mutex::new(None));
92
93        Self {
94            context,
95            receiver,
96            stop_send,
97        }
98    }
99
100    /// Start metric collection and analysis
101    #[allow(clippy::missing_panics_doc)]
102    pub fn start(&self, options: AudioRenderCapacityOptions) {
103        // stop current metric collection, if any
104        self.stop();
105
106        let receiver = self.receiver.clone();
107        let (stop_send, stop_recv) = crossbeam_channel::bounded(0);
108        *self.stop_send.lock().unwrap() = Some(stop_send);
109
110        let mut timestamp: f64 = self.context.current_time();
111        let mut load_sum: f64 = 0.;
112        let mut counter = 0;
113        let mut peak_load: f64 = 0.;
114        let mut underrun_sum = 0;
115
116        let mut next_checkpoint = timestamp + options.update_interval;
117        let base_context = self.context.clone();
118        std::thread::spawn(move || loop {
119            let try_item = crossbeam_channel::select! {
120                recv(receiver) -> item => item,
121                recv(stop_recv) -> _ => return,
122            };
123
124            // stop thread when render thread has shut down
125            let item = match try_item {
126                Err(_) => return,
127                Ok(item) => item,
128            };
129
130            let AudioRenderCapacityLoad {
131                render_timestamp,
132                load_value,
133            } = item;
134
135            counter += 1;
136            load_sum += load_value;
137            peak_load = peak_load.max(load_value);
138            if load_value > 1. {
139                underrun_sum += 1;
140            }
141
142            if render_timestamp >= next_checkpoint {
143                let event = AudioRenderCapacityEvent::new(
144                    timestamp,
145                    load_sum / counter as f64,
146                    peak_load,
147                    underrun_sum as f64 / counter as f64,
148                );
149
150                let send_result = base_context.send_event(EventDispatch::render_capacity(event));
151                if send_result.is_err() {
152                    break;
153                }
154
155                next_checkpoint += options.update_interval;
156                timestamp = render_timestamp;
157                load_sum = 0.;
158                counter = 0;
159                peak_load = 0.;
160                underrun_sum = 0;
161            }
162        });
163    }
164
165    /// Stop metric collection and analysis
166    #[allow(clippy::missing_panics_doc)]
167    pub fn stop(&self) {
168        // halt callback thread
169        if let Some(stop_send) = self.stop_send.lock().unwrap().take() {
170            let _ = stop_send.send(());
171        }
172    }
173
174    /// The EventHandler for [`AudioRenderCapacityEvent`].
175    ///
176    /// Only a single event handler is active at any time. Calling this method multiple times will
177    /// override the previous event handler.
178    pub fn set_onupdate<F: FnMut(AudioRenderCapacityEvent) + Send + 'static>(
179        &self,
180        mut callback: F,
181    ) {
182        let callback = move |v| match v {
183            EventPayload::RenderCapacity(v) => callback(v),
184            _ => unreachable!(),
185        };
186
187        self.context.set_event_handler(
188            EventType::RenderCapacity,
189            EventHandler::Multiple(Box::new(callback)),
190        );
191    }
192
193    /// Unset the EventHandler for [`AudioRenderCapacityEvent`].
194    pub fn clear_onupdate(&self) {
195        self.context.clear_event_handler(EventType::RenderCapacity);
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::context::{AudioContext, AudioContextOptions};
203
204    #[test]
205    fn test_same_instance() {
206        let options = AudioContextOptions {
207            sink_id: "none".into(),
208            ..AudioContextOptions::default()
209        };
210        let context = AudioContext::new(options);
211
212        let rc1 = context.render_capacity();
213        let rc2 = context.render_capacity();
214        let rc3 = rc2.clone();
215
216        // assert all items are actually the same instance
217        assert!(Arc::ptr_eq(&rc1.stop_send, &rc2.stop_send));
218        assert!(Arc::ptr_eq(&rc1.stop_send, &rc3.stop_send));
219    }
220
221    #[test]
222    fn test_stop_when_not_running() {
223        let options = AudioContextOptions {
224            sink_id: "none".into(),
225            ..AudioContextOptions::default()
226        };
227        let context = AudioContext::new(options);
228
229        let rc = context.render_capacity();
230        rc.stop();
231    }
232
233    #[test]
234    fn test_render_capacity() {
235        let options = AudioContextOptions {
236            sink_id: "none".into(),
237            ..AudioContextOptions::default()
238        };
239        let context = AudioContext::new(options);
240
241        let rc = context.render_capacity();
242        let (send, recv) = crossbeam_channel::bounded(1);
243        rc.set_onupdate(move |e| send.send(e).unwrap());
244        rc.start(AudioRenderCapacityOptions {
245            update_interval: 0.05,
246        });
247        let event = recv.recv().unwrap();
248
249        assert!(event.timestamp >= 0.);
250        assert!(event.average_load >= 0.);
251        assert!(event.peak_load >= 0.);
252        assert!(event.underrun_ratio >= 0.);
253
254        assert!(event.timestamp.is_finite());
255        assert!(event.average_load.is_finite());
256        assert!(event.peak_load.is_finite());
257        assert!(event.underrun_ratio.is_finite());
258
259        assert_eq!(event.event.type_, "AudioRenderCapacityEvent");
260    }
261}