Files
qbz/crates/qbz-audio/src/diagnostic.rs
T

205 lines
5.6 KiB
Rust

//! Audio bit-depth diagnostic
//!
//! Lock-free capture that accumulates an OR-mask of all sample values
//! converted to i32. The trailing zeros in the mask reveal the effective
//! bit depth of the source data — no sample storage needed.
//!
//! Works for both rodio (PipeWire/ALSA via CPAL) and ALSA Direct paths
//! via a transparent Source wrapper.
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use rodio::Source;
use serde::Serialize;
// ---------------------------------------------------------------------------
// Shared diagnostic state (atomics — safe to clone across threads)
// ---------------------------------------------------------------------------
#[derive(Clone)]
pub struct AudioDiagnostic {
capturing: Arc<AtomicBool>,
or_mask: Arc<AtomicU32>,
sample_count: Arc<AtomicU64>,
sample_rate: Arc<AtomicU32>,
channels: Arc<AtomicU32>,
}
impl AudioDiagnostic {
pub fn new() -> Self {
Self {
capturing: Arc::new(AtomicBool::new(false)),
or_mask: Arc::new(AtomicU32::new(0)),
sample_count: Arc::new(AtomicU64::new(0)),
sample_rate: Arc::new(AtomicU32::new(0)),
channels: Arc::new(AtomicU32::new(0)),
}
}
/// Begin capturing. Resets previous state.
pub fn start_capture(&self, sample_rate: u32, channels: u32) {
self.or_mask.store(0, Ordering::SeqCst);
self.sample_count.store(0, Ordering::SeqCst);
self.sample_rate.store(sample_rate, Ordering::SeqCst);
self.channels.store(channels, Ordering::SeqCst);
self.capturing.store(true, Ordering::SeqCst);
log::info!(
"[Diagnostic] Bit-depth capture started ({}Hz, {}ch)",
sample_rate,
channels
);
}
#[inline]
pub fn is_capturing(&self) -> bool {
self.capturing.load(Ordering::Relaxed)
}
/// Push a single sample (called per-sample in the Source wrapper).
#[inline]
pub fn push_sample(&self, sample: f32) {
if !self.is_capturing() {
return;
}
let clamped = sample.clamp(-1.0, 1.0);
let s32 = (clamped * 2_147_483_647.0) as i32;
self.or_mask.fetch_or(s32.unsigned_abs(), Ordering::Relaxed);
self.sample_count.fetch_add(1, Ordering::Relaxed);
}
/// Stop capturing and return the analysis.
pub fn stop_and_analyze(&self) -> BitDepthResult {
self.capturing.store(false, Ordering::SeqCst);
let or_mask = self.or_mask.load(Ordering::SeqCst);
let sample_count = self.sample_count.load(Ordering::SeqCst);
let sample_rate = self.sample_rate.load(Ordering::SeqCst);
let channels = self.channels.load(Ordering::SeqCst);
let trailing_zeros = if or_mask == 0 {
32
} else {
or_mask.trailing_zeros()
};
let effective_bits = 32 - trailing_zeros;
let frames = if channels > 0 {
sample_count / channels as u64
} else {
sample_count
};
let duration_secs = if sample_rate > 0 {
frames as f64 / sample_rate as f64
} else {
0.0
};
log::info!(
"[Diagnostic] Capture stopped: {} samples, {:.1}s, or_mask=0x{:08X}, trailing_zeros={}, effective_bits={}",
sample_count, duration_secs, or_mask, trailing_zeros, effective_bits
);
BitDepthResult {
sample_count,
sample_rate,
channels,
duration_secs,
or_mask: format!("0x{:08X}", or_mask),
trailing_zeros,
effective_bits,
}
}
}
impl Default for AudioDiagnostic {
fn default() -> Self {
Self::new()
}
}
// ---------------------------------------------------------------------------
// Result returned to the frontend
// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BitDepthResult {
pub sample_count: u64,
pub sample_rate: u32,
pub channels: u32,
pub duration_secs: f64,
pub or_mask: String,
pub trailing_zeros: u32,
pub effective_bits: u32,
}
// ---------------------------------------------------------------------------
// Source wrapper — transparent tap for bit-depth capture
// ---------------------------------------------------------------------------
pub struct DiagnosticSource<S>
where
S: Source<Item = f32>,
{
inner: S,
diagnostic: AudioDiagnostic,
}
impl<S> DiagnosticSource<S>
where
S: Source<Item = f32>,
{
pub fn new(source: S, diagnostic: AudioDiagnostic) -> Self {
Self {
inner: source,
diagnostic,
}
}
}
impl<S> Iterator for DiagnosticSource<S>
where
S: Source<Item = f32>,
{
type Item = f32;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let sample = self.inner.next()?;
self.diagnostic.push_sample(sample);
Some(sample)
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<S> Source for DiagnosticSource<S>
where
S: Source<Item = f32>,
{
#[inline]
fn current_span_len(&self) -> Option<usize> {
self.inner.current_span_len()
}
#[inline]
fn channels(&self) -> std::num::NonZero<u16> {
self.inner.channels()
}
#[inline]
fn sample_rate(&self) -> std::num::NonZero<u32> {
self.inner.sample_rate()
}
#[inline]
fn total_duration(&self) -> Option<Duration> {
self.inner.total_duration()
}
}