Correct goniometer transport and correlation meter
This commit is contained in:
+16
-15
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
mod audio;
|
||||
mod config;
|
||||
mod correlation;
|
||||
mod goniometer;
|
||||
mod model;
|
||||
mod ppm;
|
||||
mod routes;
|
||||
|
||||
Reference in New Issue
Block a user