Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run diagnostics on live AudioContext #401

Merged
merged 5 commits into from
Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
35 changes: 34 additions & 1 deletion src/context/online.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -344,6 +344,39 @@ impl AudioContext {
self.base().clear_event_handler(EventType::SinkChange);
}

#[allow(clippy::missing_panics_doc)]
#[doc(hidden)] // Method signature might change in the future
pub fn run_diagnostics<F: Fn(String) + Send + 'static>(&self, callback: F) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if we should mark this method #[doc(hidden)] because I could imagine we are not keeping the API stable over different versions. Would be interesting to return more structured data in the future

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup looks reasonable

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();
callback(s);
}
_ => unreachable!(),
};

self.base().set_event_handler(
EventType::Diagnostics,
EventHandler::Once(Box::new(callback)),
);

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
Expand Down
9 changes: 9 additions & 0 deletions src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub(crate) enum EventType {
SinkChange,
RenderCapacity,
ProcessorError(AudioNodeId),
Diagnostics,
}

/// The Error Event interface
Expand All @@ -39,6 +40,7 @@ pub(crate) enum EventPayload {
None,
RenderCapacity(AudioRenderCapacityEvent),
ProcessorError(ErrorEvent),
Diagnostics(Vec<u8>),
}

pub(crate) struct EventDispatch {
Expand Down Expand Up @@ -74,6 +76,13 @@ impl EventDispatch {
payload: EventPayload::ProcessorError(value),
}
}

pub fn diagnostics(value: Vec<u8>) -> Self {
EventDispatch {
type_: EventType::Diagnostics,
payload: EventPayload::Diagnostics(value),
}
}
}

pub(crate) enum EventHandler {
Expand Down
5 changes: 5 additions & 0 deletions src/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Self>()
}

/// Setup a new output stream (speakers)
fn build_output(options: AudioContextOptions, render_thread_init: RenderThreadInit) -> Self
where
Expand Down
3 changes: 3 additions & 0 deletions src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,7 @@ pub(crate) enum ControlMessage {
id: AudioNodeId,
msg: llq::Node<Box<dyn Any + Send>>,
},

/// Request a diagnostic report of the audio graph
RunDiagnostics { buffer: Vec<u8> },
}
37 changes: 37 additions & 0 deletions src/render/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,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
Expand All @@ -42,6 +57,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_deref())
.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 {
Expand Down Expand Up @@ -94,6 +122,15 @@ pub(crate) struct Graph {
cycle_breakers: Vec<AudioNodeId>,
}

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<AudioNodeId>) -> Self {
Graph {
Expand Down
1 change: 1 addition & 0 deletions src/render/node_collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<RefCell<Node>>>,
}
Expand Down
40 changes: 40 additions & 0 deletions src/render/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method enables us to ask a Box<dyn AudioProcessor> what its true type is (e.g. ConstantSourceRenderer). This opens the way to mute audio graph legs with no side effects like proposed in #397

Copy link
Collaborator

@b-ma b-ma Nov 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I'll try to make to list all the cases I can think of about this graph thing next week

std::any::type_name::<Self>()
}
}

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>);
Expand Down Expand Up @@ -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 { .. }"
);
}
}
25 changes: 25 additions & 0 deletions src/render/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use super::graph::Graph;
pub(crate) struct RenderThread {
graph: Option<Graph>,
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,
Expand All @@ -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,
Expand All @@ -56,6 +68,7 @@ impl RenderThread {
Self {
graph: None,
sample_rate,
buffer_size: 0,
number_of_channels,
frames_played,
receiver: Some(receiver),
Expand Down Expand Up @@ -148,6 +161,16 @@ impl RenderThread {
gc.push(msg)
}
}
RunDiagnostics { mut buffer } => {
if let Some(sender) = self.event_sender.as_ref() {
use std::io::Write;
writeln!(&mut buffer, "{:#?}", &self).ok();
writeln!(&mut buffer, "{:?}", &self.graph).ok();
sender
.try_send(EventDispatch::diagnostics(buffer))
.expect("Unable to send diagnostics - channel is full");
}
}
}
}
}
Expand Down Expand Up @@ -236,6 +259,8 @@ impl RenderThread {
}

fn render_inner<S: FromSample<f32> + 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() {
Expand Down