Correct goniometer transport and correlation meter

This commit is contained in:
Mikei386
2026-07-21 20:06:47 +02:00
parent 91aeccb938
commit 54f0f7a450
12 changed files with 297 additions and 80 deletions
+16 -15
View File
@@ -11,11 +11,13 @@ use std::{
use tracing::warn;
#[cfg(target_os = "linux")]
use crate::model::{RtaFrame, SpectroFrame, WaveEnvFrame};
#[cfg(target_os = "linux")]
use crate::correlation::CorrelationMeter;
#[cfg(target_os = "linux")]
use crate::goniometer::{selected_sample_indices, GoniometerClock};
#[cfg(target_os = "linux")]
use crate::model::{RtaFrame, SpectroFrame, WaveEnvFrame};
#[cfg(target_os = "linux")]
use crate::ppm::{PpmDetector, PpmStandard};
#[cfg(target_os = "linux")]
use crate::rta::{
@@ -481,7 +483,7 @@ struct PpmState {
correlation: CorrelationMeter,
xy_pending_l: Vec<f32>,
xy_pending_r: Vec<f32>,
xy_emit_phase: u64,
xy_clock: GoniometerClock,
}
#[cfg(target_os = "linux")]
@@ -513,7 +515,7 @@ impl Default for PpmState {
correlation: CorrelationMeter::new(48_000, 1.0, 0),
xy_pending_l: Vec::with_capacity(1024),
xy_pending_r: Vec::with_capacity(1024),
xy_emit_phase: 0,
xy_clock: GoniometerClock::default(),
}
}
}
@@ -1153,8 +1155,7 @@ fn take_goniometer_samples(state: &mut PpmState, target_points: usize) -> (Vec<f
} else {
// Preserve chronological sample pairs while reducing transport to the
// selected display density. Endpoints are always retained.
for index in 0..count {
let source = index * (available - 1) / (count - 1);
for source in selected_sample_indices(available, count) {
left.push(state.xy_pending_l[source]);
right.push(state.xy_pending_r[source]);
}
@@ -1284,15 +1285,15 @@ fn build_meter_frame(
let ppm_ebu_l = dbfs(ppm_ebu_amp_l);
let ppm_ebu_r = dbfs(ppm_ebu_amp_r);
let wave_env = wave_env_flush(&mut ppm_state.wave_env);
ppm_state.xy_emit_phase = ppm_state.xy_emit_phase.saturating_add(
(frames as u64).saturating_mul(XY_TARGET_UPDATES_PER_SECOND),
);
let (xy_l, xy_r) = if ppm_state.xy_emit_phase >= u64::from(sample_rate.max(1)) {
ppm_state.xy_emit_phase %= u64::from(sample_rate.max(1));
take_goniometer_samples(ppm_state, rta_config.xy_points as usize)
} else {
(Vec::new(), Vec::new())
};
let (xy_l, xy_r) =
if ppm_state
.xy_clock
.advance(frames, sample_rate, XY_TARGET_UPDATES_PER_SECOND as u32)
{
take_goniometer_samples(ppm_state, rta_config.xy_points as usize)
} else {
(Vec::new(), Vec::new())
};
MeterFrame {
seq: seq.fetch_add(1, Ordering::Relaxed) + 1,
+16 -7
View File
@@ -27,7 +27,7 @@ impl CorrelationMeter {
power_r: 0.0,
cross_power: 0.0,
value: 0.0,
negative_peak: 1.0,
negative_peak: 0.0,
reset_token,
};
meter.configure(sample_rate, response_seconds, reset_token);
@@ -40,12 +40,12 @@ impl CorrelationMeter {
if self.sample_rate != sample_rate || self.response_seconds != response_seconds {
self.sample_rate = sample_rate;
self.response_seconds = response_seconds;
self.alpha = 1.0
- (-1.0 / (f64::from(sample_rate) * f64::from(response_seconds))).exp();
self.alpha =
1.0 - (-1.0 / (f64::from(sample_rate) * f64::from(response_seconds))).exp();
}
if self.reset_token != reset_token {
self.reset_token = reset_token;
self.negative_peak = 1.0;
self.negative_peak = 0.0;
}
}
@@ -106,13 +106,21 @@ mod tests {
fn detects_positive_negative_and_quadrature_signals() {
let sample_rate = 48_000;
let phase_step = 2.0 * std::f32::consts::PI * 1_000.0 / sample_rate as f32;
for (phase, expected) in [(0.0, 1.0), (std::f32::consts::PI, -1.0), (std::f32::consts::FRAC_PI_2, 0.0)] {
for (phase, expected) in [
(0.0, 1.0),
(std::f32::consts::PI, -1.0),
(std::f32::consts::FRAC_PI_2, 0.0),
] {
let mut meter = CorrelationMeter::new(sample_rate, 1.0, 0);
run_signal(&mut meter, 5, |index| {
let angle = phase_step * index as f32;
(angle.sin(), (angle + phase).sin())
});
assert!((meter.value() - expected).abs() < 0.002, "phase {phase}: {}", meter.value());
assert!(
(meter.value() - expected).abs() < 0.002,
"phase {phase}: {}",
meter.value()
);
}
}
@@ -134,7 +142,7 @@ mod tests {
});
assert!(fast.negative_peak() < -0.7);
fast.configure(sample_rate, 1.0, 1);
assert_eq!(fast.negative_peak(), 1.0);
assert_eq!(fast.negative_peak(), 0.0);
}
#[test]
@@ -142,6 +150,7 @@ mod tests {
let mut meter = CorrelationMeter::new(48_000, 1.0, 0);
run_signal(&mut meter, 2, |_| (0.0, 0.0));
assert_eq!(meter.value(), 0.0);
assert_eq!(meter.negative_peak(), 0.0);
run_signal(&mut meter, 2, |index| {
let sample = if index & 1 == 0 { 0.5 } else { -0.5 };
(sample, 0.0)
+70
View File
@@ -0,0 +1,70 @@
//! Timing and sample-selection helpers for the realtime goniometer stream.
#[derive(Clone, Debug, Default)]
pub struct GoniometerClock {
phase: u64,
}
impl GoniometerClock {
/// Returns true at the first capture boundary after the next display tick.
/// The fractional phase is retained, so the average rate is independent
/// of ALSA period size.
pub fn advance(&mut self, frames: usize, sample_rate: u32, updates_per_second: u32) -> bool {
let sample_rate = u64::from(sample_rate.max(1));
self.phase = self
.phase
.saturating_add((frames as u64).saturating_mul(u64::from(updates_per_second.max(1))));
if self.phase < sample_rate {
return false;
}
self.phase %= sample_rate;
true
}
}
pub fn selected_sample_indices(available: usize, requested: usize) -> Vec<usize> {
if available == 0 || requested == 0 {
return Vec::new();
}
let count = available.min(requested.max(2));
if count >= available {
return (0..available).collect();
}
(0..count)
.map(|index| index * (available - 1) / (count - 1))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn update_rate_is_independent_of_capture_period() {
for (sample_rate, period) in [(44_100, 128), (48_000, 128), (48_000, 192), (96_000, 512)] {
let mut clock = GoniometerClock::default();
let mut emissions = 0usize;
let mut processed = 0usize;
let target = sample_rate as usize * 10;
while processed < target {
let frames = period.min(target - processed);
emissions += usize::from(clock.advance(frames, sample_rate, 60));
processed += frames;
}
assert!(
(599..=600).contains(&emissions),
"{sample_rate}/{period}: {emissions}"
);
}
}
#[test]
fn selection_never_upsamples_and_keeps_endpoints() {
assert_eq!(selected_sample_indices(3, 1024), vec![0, 1, 2]);
let indices = selected_sample_indices(800, 128);
assert_eq!(indices.len(), 128);
assert_eq!(indices[0], 0);
assert_eq!(indices[127], 799);
assert!(indices.windows(2).all(|pair| pair[0] < pair[1]));
}
}
+1
View File
@@ -1,6 +1,7 @@
mod audio;
mod config;
mod correlation;
mod goniometer;
mod model;
mod ppm;
mod routes;