Improve phase trail and correlation controls

This commit is contained in:
Mikei386 committed 2026-07-27 18:13:07 +02:00
1 parent 00fb59f643
commit a4fa0ceb47
13 files changed
+268 -41

No files matched your search

+2 -1
View File
@@ -628,7 +628,7 @@ impl Default for PpmState {
true_peak_r: TruePeakDetector::default(),
transport_clock: GoniometerClock::default(),
transport_peaks: TransportPeaks::default(),
correlation: CorrelationMeter::new(48_000, 1.0, 0),
correlation: CorrelationMeter::new(48_000, 1.0, -75.0, 0),
phase_wheel: PhaseWheelAnalyzer::new(48_000),
xy_pending_l: Vec::with_capacity(1024),
xy_pending_r: Vec::with_capacity(1024),
@@ -1178,6 +1178,7 @@ fn process_audio_block(
ppm_state.correlation.configure(
sample_rate,
rta_config.correlation_response_s,
rta_config.correlation_silence_threshold_rms_dbfs,
rta_config.correlation_reset_token,
);
+117 -16
View File
@@ -9,39 +9,73 @@ pub struct CorrelationMeter {
sample_rate: u32,
response_seconds: f32,
alpha: f64,
gate_alpha: f64,
silence_threshold_db: f32,
silence_threshold_power: f64,
power_l: f64,
power_r: f64,
cross_power: f64,
gate_power: f64,
value: f32,
negative_peak: f32,
reset_token: u64,
}
impl CorrelationMeter {
pub fn new(sample_rate: u32, response_seconds: f32, reset_token: u64) -> Self {
pub fn new(
sample_rate: u32,
response_seconds: f32,
silence_threshold_db: f32,
reset_token: u64,
) -> Self {
let mut meter = Self {
sample_rate: 0,
response_seconds: 0.0,
alpha: 1.0,
gate_alpha: 1.0,
silence_threshold_db: -75.0,
silence_threshold_power: 10.0_f64.powf(-7.5),
power_l: 0.0,
power_r: 0.0,
cross_power: 0.0,
gate_power: 0.0,
value: 0.0,
negative_peak: 0.0,
reset_token,
};
meter.configure(sample_rate, response_seconds, reset_token);
meter.configure(
sample_rate,
response_seconds,
silence_threshold_db,
reset_token,
);
meter
}
pub fn configure(&mut self, sample_rate: u32, response_seconds: f32, reset_token: u64) {
pub fn configure(
&mut self,
sample_rate: u32,
response_seconds: f32,
silence_threshold_db: f32,
reset_token: u64,
) {
let sample_rate = sample_rate.max(8_000);
let response_seconds = normalize_response_seconds(response_seconds);
let silence_threshold_db = normalize_silence_threshold_db(silence_threshold_db);
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();
// The silence decision must react independently of the selected
// correlation response time. A 50 ms RMS envelope avoids both
// sample-zero chatter and multi-second threshold lag.
self.gate_alpha = 1.0 - (-1.0 / (f64::from(sample_rate) * 0.05)).exp();
}
if self.silence_threshold_db != silence_threshold_db {
self.silence_threshold_db = silence_threshold_db;
self.silence_threshold_power =
10.0_f64.powf(f64::from(silence_threshold_db) / 10.0);
}
if self.reset_token != reset_token {
self.reset_token = reset_token;
@@ -55,12 +89,18 @@ impl CorrelationMeter {
self.power_l += self.alpha * (l * l - self.power_l);
self.power_r += self.alpha * (r * r - self.power_r);
self.cross_power += self.alpha * (l * r - self.cross_power);
self.gate_power += self.gate_alpha
* (0.5 * (l * l + r * r) - self.gate_power);
// Below roughly -100 dBFS RMS per channel the quotient is no longer a
// useful phase measurement and should settle at the neutral position.
// Below the configured mono RMS threshold the correlation indication
// settles at its neutral position.
const MIN_POWER: f64 = 1.0e-10;
let denominator = (self.power_l * self.power_r).sqrt();
self.value = if self.power_l > MIN_POWER && self.power_r > MIN_POWER && denominator > 0.0 {
self.value = if self.gate_power >= self.silence_threshold_power
&& self.power_l > MIN_POWER
&& self.power_r > MIN_POWER
&& denominator > 0.0
{
(self.cross_power / denominator).clamp(-1.0, 1.0) as f32
} else {
0.0
@@ -80,10 +120,22 @@ impl CorrelationMeter {
}
pub fn normalize_response_seconds(value: f32) -> f32 {
if value.is_finite() && value >= 1.75 {
2.5
} else {
if !value.is_finite() || value <= 0.0 {
1.0
} else if value < 0.75 {
0.5
} else if value < 1.75 {
1.0
} else {
2.5
}
}
pub fn normalize_silence_threshold_db(value: f32) -> f32 {
if value.is_finite() {
value.clamp(-90.0, -40.0)
} else {
-75.0
}
}
@@ -111,7 +163,7 @@ mod tests {
(std::f32::consts::PI, -1.0),
(std::f32::consts::FRAC_PI_2, 0.0),
] {
let mut meter = CorrelationMeter::new(sample_rate, 1.0, 0);
let mut meter = CorrelationMeter::new(sample_rate, 1.0, -75.0, 0);
run_signal(&mut meter, 5, |index| {
let angle = phase_step * index as f32;
(angle.sin(), (angle + phase).sin())
@@ -127,8 +179,8 @@ mod tests {
#[test]
fn response_time_and_peak_reset_are_deterministic() {
let sample_rate = 48_000;
let mut fast = CorrelationMeter::new(sample_rate, 1.0, 0);
let mut slow = CorrelationMeter::new(sample_rate, 2.5, 0);
let mut fast = CorrelationMeter::new(sample_rate, 1.0, -75.0, 0);
let mut slow = CorrelationMeter::new(sample_rate, 2.5, -75.0, 0);
for index in 0..sample_rate as usize {
let sample = if index & 1 == 0 { 0.5 } else { -0.5 };
fast.process(sample, sample);
@@ -141,33 +193,82 @@ mod tests {
(sample, -sample)
});
assert!(fast.negative_peak() < -0.7);
fast.configure(sample_rate, 1.0, 1);
fast.configure(sample_rate, 1.0, -75.0, 1);
assert_eq!(fast.negative_peak(), 0.0);
}
#[test]
fn fast_response_tracks_a_phase_reversal_before_slow_response() {
let sample_rate = 48_000;
let mut fast = CorrelationMeter::new(sample_rate, 1.0, 0);
let mut slow = CorrelationMeter::new(sample_rate, 2.5, 0);
let mut very_fast = CorrelationMeter::new(sample_rate, 0.5, -75.0, 0);
let mut fast = CorrelationMeter::new(sample_rate, 1.0, -75.0, 0);
let mut slow = CorrelationMeter::new(sample_rate, 2.5, -75.0, 0);
for index in 0..sample_rate as usize * 5 {
let sample = if index & 1 == 0 { 0.5 } else { -0.5 };
very_fast.process(sample, sample);
fast.process(sample, sample);
slow.process(sample, sample);
}
for index in 0..sample_rate as usize {
let sample = if index & 1 == 0 { 0.5 } else { -0.5 };
very_fast.process(sample, -sample);
fast.process(sample, -sample);
slow.process(sample, -sample);
}
assert!(very_fast.value() < fast.value() - 0.35);
assert!(fast.value() < slow.value() - 0.35);
assert!(very_fast.negative_peak() < fast.negative_peak());
assert!(fast.negative_peak() < 0.0);
assert_eq!(slow.negative_peak(), 0.0);
}
#[test]
fn response_time_normalization_accepts_all_three_profiles() {
assert_eq!(normalize_response_seconds(0.5), 0.5);
assert_eq!(normalize_response_seconds(1.0), 1.0);
assert_eq!(normalize_response_seconds(2.5), 2.5);
assert_eq!(normalize_response_seconds(f32::NAN), 1.0);
assert_eq!(normalize_response_seconds(0.0), 1.0);
}
#[test]
fn configurable_silence_threshold_neutralizes_the_meter() {
let sample_rate = 48_000;
let amplitude = 10.0_f32.powf(-64.0 / 20.0);
let mut meter = CorrelationMeter::new(sample_rate, 0.5, -75.0, 0);
run_signal(&mut meter, 1, |index| {
let sample = if index & 1 == 0 {
amplitude
} else {
-amplitude
};
(sample, sample)
});
assert!(meter.value() > 0.99);
meter.configure(sample_rate, 0.5, -50.0, 0);
run_signal(&mut meter, 1, |index| {
let sample = if index & 1 == 0 {
amplitude
} else {
-amplitude
};
(sample, sample)
});
assert_eq!(meter.value(), 0.0);
}
#[test]
fn silence_threshold_is_normalized_to_the_ui_range() {
assert_eq!(normalize_silence_threshold_db(-50.0), -50.0);
assert_eq!(normalize_silence_threshold_db(-100.0), -90.0);
assert_eq!(normalize_silence_threshold_db(-20.0), -40.0);
assert_eq!(normalize_silence_threshold_db(f32::NAN), -75.0);
}
#[test]
fn silence_and_single_channel_are_neutral() {
let mut meter = CorrelationMeter::new(48_000, 1.0, 0);
let mut meter = CorrelationMeter::new(48_000, 1.0, -75.0, 0);
run_signal(&mut meter, 2, |_| (0.0, 0.0));
assert_eq!(meter.value(), 0.0);
assert_eq!(meter.negative_peak(), 0.0);
+2
View File
@@ -82,6 +82,7 @@ pub struct PhoenixRtaConfig {
pub lufs_i_window_min: u32,
pub lufs_i_norm_enabled: bool,
pub correlation_response_s: f32,
pub correlation_silence_threshold_rms_dbfs: f32,
pub correlation_reset_token: u64,
pub xy_points: u32,
}
@@ -118,6 +119,7 @@ impl Default for PhoenixRtaConfig {
lufs_i_window_min: 4,
lufs_i_norm_enabled: false,
correlation_response_s: 1.0,
correlation_silence_threshold_rms_dbfs: -75.0,
correlation_reset_token: 0,
xy_points: 1024,
}
+4
View File
@@ -487,6 +487,10 @@ fn normalize_rta_config(mut config: PhoenixRtaConfig) -> PhoenixRtaConfig {
config.lufs_i_norm_enabled = !!config.lufs_i_norm_enabled;
config.correlation_response_s =
crate::correlation::normalize_response_seconds(config.correlation_response_s);
config.correlation_silence_threshold_rms_dbfs =
crate::correlation::normalize_silence_threshold_db(
config.correlation_silence_threshold_rms_dbfs,
);
config.xy_points = match config.xy_points {
128 | 256 | 512 | 1024 | 2048 => config.xy_points,
n if n < 192 => 128,