From e2585ec6a32e142a41adaf643063893f13186259 Mon Sep 17 00:00:00 2001 From: Otto Date: Thu, 23 Nov 2023 08:49:15 +0100 Subject: [PATCH 1/5] Implement Debug formatting for the audio Graph --- src/context/mod.rs | 8 ++++++- src/render/graph.rs | 23 ++++++++++++++++++++ src/render/node_collection.rs | 1 + src/render/processor.rs | 40 +++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/context/mod.rs b/src/context/mod.rs index 48f960b1..e9f31b98 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -36,9 +36,15 @@ pub(crate) const LISTENER_AUDIO_PARAM_IDS: [AudioParamId; 9] = [ /// Unique identifier for audio nodes. /// /// Used for internal bookkeeping. -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +#[derive(Hash, PartialEq, Eq, Clone, Copy)] pub(crate) struct AudioNodeId(pub u64); +impl std::fmt::Debug for AudioNodeId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "AudioNodeId({})", self.0) + } +} + /// Unique identifier for audio params. /// /// Store these in your `AudioProcessor` to get access to `AudioParam` values. diff --git a/src/render/graph.rs b/src/render/graph.rs index b13f0bdf..4957d081 100644 --- a/src/render/graph.rs +++ b/src/render/graph.rs @@ -11,6 +11,7 @@ use crate::node::ChannelConfig; use crate::render::RenderScope; /// Connection between two audio nodes +#[derive(Debug)] struct OutgoingEdge { /// index of the current Nodes output port self_index: usize, @@ -42,6 +43,19 @@ pub struct Node { cycle_breaker: bool, } +impl std::fmt::Debug for Node { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Node") + .field("id", &self.reclaim_id.as_ref().map(|n| &**n)) + .field("processor", &self.processor) + .field("channel_config", &self.channel_config) + .field("outgoing_edges", &self.outgoing_edges) + .field("free_when_finished", &self.free_when_finished) + .field("cycle_breaker", &self.cycle_breaker) + .finish_non_exhaustive() + } +} + impl Node { /// Render an audio quantum fn process(&mut self, params: AudioParamValues<'_>, scope: &RenderScope) -> bool { @@ -94,6 +108,15 @@ pub(crate) struct Graph { cycle_breakers: Vec, } +impl std::fmt::Debug for Graph { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Graph") + .field("nodes", &self.nodes) + .field("ordered", &self.ordered) + .finish_non_exhaustive() + } +} + impl Graph { pub fn new(reclaim_id_channel: llq::Producer) -> Self { Graph { diff --git a/src/render/node_collection.rs b/src/render/node_collection.rs index c86aadb8..5882a340 100644 --- a/src/render/node_collection.rs +++ b/src/render/node_collection.rs @@ -4,6 +4,7 @@ use crate::render::graph::Node; use std::cell::RefCell; use std::ops::{Index, IndexMut}; +#[derive(Debug)] pub(crate) struct NodeCollection { nodes: Vec>>, } diff --git a/src/render/processor.rs b/src/render/processor.rs index 8fee65bb..3d471a92 100644 --- a/src/render/processor.rs +++ b/src/render/processor.rs @@ -116,6 +116,18 @@ pub trait AudioProcessor: Send { fn onmessage(&mut self, msg: &mut dyn Any) { log::warn!("Ignoring incoming message"); } + + /// Return the name of the actual AudioProcessor type + #[doc(hidden)] // not meant to be user facing + fn name(&self) -> &'static str { + return std::any::type_name::(); + } +} + +impl std::fmt::Debug for dyn AudioProcessor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct(self.name()).finish_non_exhaustive() + } } struct DerefAudioRenderQuantumChannel<'a>(std::cell::Ref<'a, Node>); @@ -161,3 +173,31 @@ impl<'a> AudioParamValues<'a> { crate::context::LISTENER_AUDIO_PARAM_IDS.map(|p| self.get(&p)) } } + +#[cfg(test)] +mod tests { + use super::*; + + struct TestNode; + + impl AudioProcessor for TestNode { + fn process( + &mut self, + _inputs: &[AudioRenderQuantum], + _outputs: &mut [AudioRenderQuantum], + _params: AudioParamValues<'_>, + _scope: &RenderScope, + ) -> bool { + todo!() + } + } + + #[test] + fn test_debug_fmt() { + let proc = &TestNode as &dyn AudioProcessor; + assert_eq!( + &format!("{:?}", proc), + "web_audio_api::render::processor::tests::TestNode { .. }" + ); + } +} From 8464157500203ff1486220f2776e8c1d05076fa1 Mon Sep 17 00:00:00 2001 From: Otto Date: Fri, 24 Nov 2023 16:55:43 +0100 Subject: [PATCH 2/5] Implement AudioContext::run_diagnostics, improve Debug display of graph --- src/context/online.rs | 22 +++++++++++++++++++++- src/events.rs | 9 +++++++++ src/message.rs | 3 +++ src/render/graph.rs | 18 ++++++++++++++++-- src/render/processor.rs | 2 +- src/render/thread.rs | 23 +++++++++++++++++++++++ 6 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/context/online.rs b/src/context/online.rs index aefb5d9d..1d3cf85e 100644 --- a/src/context/online.rs +++ b/src/context/online.rs @@ -3,7 +3,7 @@ use std::error::Error; use std::sync::Mutex; use crate::context::{AudioContextState, BaseAudioContext, ConcreteBaseAudioContext}; -use crate::events::{EventDispatch, EventHandler, EventType}; +use crate::events::{EventDispatch, EventHandler, EventPayload, EventType}; use crate::io::{self, AudioBackendManager, ControlThreadInit, RenderThreadInit}; use crate::media_devices::{enumerate_devices_sync, MediaDeviceInfoKind}; use crate::media_streams::{MediaStream, MediaStreamTrack}; @@ -344,6 +344,26 @@ impl AudioContext { self.base().clear_event_handler(EventType::SinkChange); } + #[allow(clippy::missing_panics_doc)] + pub fn run_diagnostics(&self, callback: F) { + let callback = move |v| match v { + EventPayload::Diagnostics(v) => { + let s = String::from_utf8(v).unwrap(); + callback(s); + } + _ => unreachable!(), + }; + + self.base().set_event_handler( + EventType::Diagnostics, + EventHandler::Once(Box::new(callback)), + ); + + let buffer = Vec::with_capacity(32 * 1024); + self.base() + .send_control_msg(ControlMessage::RunDiagnostics { buffer }); + } + /// Suspends the progression of time in the audio context. /// /// This will temporarily halt audio hardware access and reducing CPU/battery usage in the diff --git a/src/events.rs b/src/events.rs index 179c5978..bc797eb3 100644 --- a/src/events.rs +++ b/src/events.rs @@ -21,6 +21,7 @@ pub(crate) enum EventType { SinkChange, RenderCapacity, ProcessorError(AudioNodeId), + Diagnostics, } /// The Error Event interface @@ -39,6 +40,7 @@ pub(crate) enum EventPayload { None, RenderCapacity(AudioRenderCapacityEvent), ProcessorError(ErrorEvent), + Diagnostics(Vec), } pub(crate) struct EventDispatch { @@ -74,6 +76,13 @@ impl EventDispatch { payload: EventPayload::ProcessorError(value), } } + + pub fn diagnostics(value: Vec) -> Self { + EventDispatch { + type_: EventType::Diagnostics, + payload: EventPayload::Diagnostics(value), + } + } } pub(crate) enum EventHandler { diff --git a/src/message.rs b/src/message.rs index f1bf7e59..6dec766d 100644 --- a/src/message.rs +++ b/src/message.rs @@ -52,4 +52,7 @@ pub(crate) enum ControlMessage { id: AudioNodeId, msg: llq::Node>, }, + + /// Request a diagnostic report of the audio graph + RunDiagnostics { buffer: Vec }, } diff --git a/src/render/graph.rs b/src/render/graph.rs index 4957d081..a1cc04bf 100644 --- a/src/render/graph.rs +++ b/src/render/graph.rs @@ -11,7 +11,6 @@ use crate::node::ChannelConfig; use crate::render::RenderScope; /// Connection between two audio nodes -#[derive(Debug)] struct OutgoingEdge { /// index of the current Nodes output port self_index: usize, @@ -21,6 +20,21 @@ struct OutgoingEdge { other_index: usize, } +impl std::fmt::Debug for OutgoingEdge { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut format = f.debug_struct("OutgoingEdge"); + format + .field("self_index", &self.self_index) + .field("other_id", &self.other_id); + if self.other_index == usize::MAX { + format.field("other_index", &"HIDDEN"); + } else { + format.field("other_index", &self.other_index); + } + format.finish() + } +} + /// Renderer Node in the Audio Graph pub struct Node { /// AudioNodeId, to be sent back to the control thread when this node is dropped @@ -46,7 +60,7 @@ pub struct Node { impl std::fmt::Debug for Node { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Node") - .field("id", &self.reclaim_id.as_ref().map(|n| &**n)) + .field("id", &self.reclaim_id.as_deref()) .field("processor", &self.processor) .field("channel_config", &self.channel_config) .field("outgoing_edges", &self.outgoing_edges) diff --git a/src/render/processor.rs b/src/render/processor.rs index 3d471a92..580eb4c4 100644 --- a/src/render/processor.rs +++ b/src/render/processor.rs @@ -120,7 +120,7 @@ pub trait AudioProcessor: Send { /// Return the name of the actual AudioProcessor type #[doc(hidden)] // not meant to be user facing fn name(&self) -> &'static str { - return std::any::type_name::(); + std::any::type_name::() } } diff --git a/src/render/thread.rs b/src/render/thread.rs index 606fb492..e7123c39 100644 --- a/src/render/thread.rs +++ b/src/render/thread.rs @@ -148,10 +148,33 @@ impl RenderThread { gc.push(msg) } } + RunDiagnostics { mut buffer } => { + if let Some(sender) = self.event_sender.as_ref() { + self.write_diagnostics(&mut buffer); + let dispatch = EventDispatch::diagnostics(buffer); + sender + .try_send(dispatch) + .expect("Unable to send diagnostics - channel is full"); + } + } } } } + pub fn write_diagnostics(&self, buffer: &mut Vec) { + use std::io::Write; + + writeln!( + buffer, + "current frame: {}", + self.frames_played.load(Ordering::Relaxed) + ) + .ok(); + writeln!(buffer, "sample rate: {}", self.sample_rate).ok(); + writeln!(buffer, "number of channels: {}", self.number_of_channels).ok(); + writeln!(buffer, "graph: {:?}", &self.graph).ok(); // use {:#?} for newlines between items + } + // Render method of the `OfflineAudioContext::start_rendering_sync` // This method is not spec compliant and obviously marked as synchronous, so we // don't launch a thread. From 4244c5c069391e56b1b99d51d6f172797948c607 Mon Sep 17 00:00:00 2001 From: Otto Date: Fri, 24 Nov 2023 19:47:50 +0100 Subject: [PATCH 3/5] Add info about the audio backend to diagnostics --- src/context/online.rs | 14 +++++++++++++- src/io/mod.rs | 5 +++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/context/online.rs b/src/context/online.rs index 1d3cf85e..d352ec18 100644 --- a/src/context/online.rs +++ b/src/context/online.rs @@ -346,6 +346,19 @@ impl AudioContext { #[allow(clippy::missing_panics_doc)] pub fn run_diagnostics(&self, callback: F) { + let mut buffer = Vec::with_capacity(32 * 1024); + { + let backend = self.backend_manager.lock().unwrap(); + use std::io::Write; + writeln!(&mut buffer, "backend: {}", backend.name()).ok(); + writeln!(&mut buffer, "sink id: {}", backend.sink_id()).ok(); + writeln!( + &mut buffer, + "output latency: {:.6}", + backend.output_latency() + ) + .ok(); + } let callback = move |v| match v { EventPayload::Diagnostics(v) => { let s = String::from_utf8(v).unwrap(); @@ -359,7 +372,6 @@ impl AudioContext { EventHandler::Once(Box::new(callback)), ); - let buffer = Vec::with_capacity(32 * 1024); self.base() .send_control_msg(ControlMessage::RunDiagnostics { buffer }); } diff --git a/src/io/mod.rs b/src/io/mod.rs index 15d0feba..a6cc32f6 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -133,6 +133,11 @@ pub(crate) fn build_input(options: AudioContextOptions) -> MediaStream { /// Interface for audio backends pub(crate) trait AudioBackendManager: Send + Sync + 'static { + /// Name of the concrete implementation - for debug purposes + fn name(&self) -> &'static str { + std::any::type_name::() + } + /// Setup a new output stream (speakers) fn build_output(options: AudioContextOptions, render_thread_init: RenderThreadInit) -> Self where From b14e4864612e809721103fb42b73e2e644106a95 Mon Sep 17 00:00:00 2001 From: Otto Date: Fri, 24 Nov 2023 20:03:14 +0100 Subject: [PATCH 4/5] Report actual hardware buffer size in diagnostics, refactor a bit --- src/render/thread.rs | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/render/thread.rs b/src/render/thread.rs index e7123c39..d567a129 100644 --- a/src/render/thread.rs +++ b/src/render/thread.rs @@ -24,6 +24,7 @@ use super::graph::Graph; pub(crate) struct RenderThread { graph: Option, sample_rate: f32, + buffer_size: usize, /// number of channels of the backend stream, i.e. sound card number of /// channels clamped to MAX_CHANNELS number_of_channels: usize, @@ -46,6 +47,17 @@ unsafe impl Sync for Graph {} unsafe impl Send for RenderThread {} unsafe impl Sync for RenderThread {} +impl std::fmt::Debug for RenderThread { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RenderThread") + .field("sample_rate", &self.sample_rate) + .field("buffer_size", &self.buffer_size) + .field("frames_played", &self.frames_played.load(Ordering::Relaxed)) + .field("number_of_channels", &self.number_of_channels) + .finish_non_exhaustive() + } +} + impl RenderThread { pub fn new( sample_rate: f32, @@ -56,6 +68,7 @@ impl RenderThread { Self { graph: None, sample_rate, + buffer_size: 0, number_of_channels, frames_played, receiver: Some(receiver), @@ -150,10 +163,11 @@ impl RenderThread { } RunDiagnostics { mut buffer } => { if let Some(sender) = self.event_sender.as_ref() { - self.write_diagnostics(&mut buffer); - let dispatch = EventDispatch::diagnostics(buffer); + use std::io::Write; + writeln!(&mut buffer, "{:#?}", &self).ok(); + writeln!(&mut buffer, "{:?}", &self.graph).ok(); sender - .try_send(dispatch) + .try_send(EventDispatch::diagnostics(buffer)) .expect("Unable to send diagnostics - channel is full"); } } @@ -161,20 +175,6 @@ impl RenderThread { } } - pub fn write_diagnostics(&self, buffer: &mut Vec) { - use std::io::Write; - - writeln!( - buffer, - "current frame: {}", - self.frames_played.load(Ordering::Relaxed) - ) - .ok(); - writeln!(buffer, "sample rate: {}", self.sample_rate).ok(); - writeln!(buffer, "number of channels: {}", self.number_of_channels).ok(); - writeln!(buffer, "graph: {:?}", &self.graph).ok(); // use {:#?} for newlines between items - } - // Render method of the `OfflineAudioContext::start_rendering_sync` // This method is not spec compliant and obviously marked as synchronous, so we // don't launch a thread. @@ -259,6 +259,8 @@ impl RenderThread { } fn render_inner + Clone>(&mut self, mut output_buffer: &mut [S]) { + self.buffer_size = output_buffer.len(); + // There may be audio frames left over from the previous render call, // if the cpal buffer size did not align with our internal RENDER_QUANTUM_SIZE if let Some((offset, prev_rendered)) = self.buffer_offset.take() { From e0f35a98da0b91eea7f780c3fba8c669ef938975 Mon Sep 17 00:00:00 2001 From: Otto Date: Fri, 24 Nov 2023 20:04:42 +0100 Subject: [PATCH 5/5] Mark AudioContext.run_diagnostics #[doc(hidden)] for now --- src/context/online.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/context/online.rs b/src/context/online.rs index d352ec18..fd81a045 100644 --- a/src/context/online.rs +++ b/src/context/online.rs @@ -345,6 +345,7 @@ impl AudioContext { } #[allow(clippy::missing_panics_doc)] + #[doc(hidden)] // Method signature might change in the future pub fn run_diagnostics(&self, callback: F) { let mut buffer = Vec::with_capacity(32 * 1024); {