Correct RTA detector timing and peak integration

This commit is contained in:
Mikei386
2026-07-21 21:29:41 +02:00
parent 3478bfb495
commit 9b5250d42b
10 changed files with 312 additions and 112 deletions
+6 -6
View File
@@ -5,7 +5,7 @@
- Referenz ist der RTW PortaMonitor 1064X/1064X-PLUS, insbesondere dessen RTA-, PPM-, Peakmeter-, Goniometer- und Korrelationsverhalten. - Referenz ist der RTW PortaMonitor 1064X/1064X-PLUS, insbesondere dessen RTA-, PPM-, Peakmeter-, Goniometer- und Korrelationsverhalten.
- Der primäre RTW-nahe RTA arbeitet als IIR-Fractional-Octave-Filterbank. FFT bleibt eine optionale, getrennt gekennzeichnete Spektrumsansicht. - Der primäre RTW-nahe RTA arbeitet als IIR-Fractional-Octave-Filterbank. FFT bleibt eine optionale, getrennt gekennzeichnete Spektrumsansicht.
- Der RTW-RTA-Modus soll 31 Bänder in 1/3-Oktaven von 20 Hz bis 20 kHz sowie ein Verhalten entsprechend IEC 225/ANSI Class 2 beziehungsweise der passenden aktuellen Nachfolgenorm bieten. - Der RTW-RTA-Modus soll 31 Bänder in 1/3-Oktaven von 20 Hz bis 20 kHz sowie ein Verhalten entsprechend IEC 225/ANSI Class 2 beziehungsweise der passenden aktuellen Nachfolgenorm bieten.
- Vorgesehene RTA-Modi: Fast, Medium, Slow, Average und Peak; Peak Hold 2,5 s, 4 s oder manuell. - Vorgesehene RTA-Detektoren: Average (RMS) und Peak (10 ms); dazu die RMS-Reaktionszeiten Fast, Medium, Slow und Impulse sowie Peak Hold 2,5 s, 4 s oder manuell.
- Zwischen hörbarem Signal und Anzeige soll nur die unvermeidbare Mess- und Bildschirmlatenz liegen. Alte Messframes dürfen niemals eine anwachsende Verzögerung erzeugen. - Zwischen hörbarem Signal und Anzeige soll nur die unvermeidbare Mess- und Bildschirmlatenz liegen. Alte Messframes dürfen niemals eine anwachsende Verzögerung erzeugen.
- Normbedingte Ballistiken werden nicht künstlich verkürzt. Technische Transportlatenz und gewollte Instrumententrägheit werden getrennt behandelt. - Normbedingte Ballistiken werden nicht künstlich verkürzt. Technische Transportlatenz und gewollte Instrumententrägheit werden getrennt behandelt.
- LUFS und LRA bleiben vorerst außerhalb dieser Aufgabenliste. - LUFS und LRA bleiben vorerst außerhalb dieser Aufgabenliste.
@@ -62,11 +62,11 @@ Diese vorhandenen Funktionen sind nicht automatisch messtechnisch korrekt. Die f
- **Noch offen:** Der vollständige Class-2-Toleranzmasken-Nachweis und eine formelle Geräte-/Laborvalidierung fehlen; deshalb bleibt der Gesamtpunkt offen. - **Noch offen:** Der vollständige Class-2-Toleranzmasken-Nachweis und eine formelle Geräte-/Laborvalidierung fehlen; deshalb bleibt der Gesamtpunkt offen.
- **Abnahme:** Jedes der 31 Bänder besteht automatisierte Sweep- und Pegeltests innerhalb der festgelegten Toleranzen. - **Abnahme:** Jedes der 31 Bänder besteht automatisierte Sweep- und Pegeltests innerhalb der festgelegten Toleranzen.
- [ ] **6. RTW-Integrationsmodi vollständig und energetisch korrekt implementieren** - [ ] **6. RTW-Detektor und Reaktionszeiten vollständig und energetisch korrekt implementieren**
- **Soll:** Fast, Medium, Slow, Average und Peak mit dokumentiertem RTW-nahem Verhalten. - **Soll:** Getrennte Auswahl von Average-/Peak-Detektor und der RMS-Reaktionszeit entsprechend der RTW-Bedienlogik.
- **Ist:** Fast, Medium, Slow und Impulse integrieren jetzt blockgrößenunabhängig im Leistungsbereich; Average bildet das kumulative Energiemittel, Peak den höchsten gefilterten Samplewert. Erst danach erfolgt die dB-Umrechnung. - **Ist:** Average (RMS) und Peak (10 ms) sind getrennte Detektoren; Fast arbeitet mit 125 ms, Slow mit 1 s und Impulse asymmetrisch mit 35 ms Anstieg sowie 1,5 s Rücklauf. Der IIR-Average-Detektor integriert samplegenau im Leistungsbereich; Peak ermittelt gleitend den höchsten gefilterten Samplewert der letzten 10 ms. Beide sind damit unabhängig von der ALSA-Periodengröße. Alte gespeicherte `Average`-/`Peak`-Modi werden automatisch migriert.
- **Geprüft:** Ein Regressionstest bestätigt identische Integration bei unterschiedlichen Blockgrößen. - **Geprüft:** Regressionstests prüfen Blockgrößenunabhängigkeit, 10-ms-Peakintegration, Impulse-Anstieg/-Rücklauf sowie die Konfigurationsmigration.
- **Noch offen:** Die gewählte Medium-Zeitkonstante von 0,5 s und die übrigen Profile müssen noch mit vollständigem RTW-Handbuch oder Referenzgerät abgeglichen werden; deshalb bleibt der Gesamtpunkt offen. - **Noch offen:** Medium bleibt mit 0,5 s eine Phoenix-/Legacy-Zeitkonstante, solange kein verbindlicher RTW-Wert vorliegt. Die endgültige dynamische Gerätevalidierung bleibt deshalb offen.
- **Abnahme:** Sprung-, Burst- und Rauschtests zeigen für jeden Modus reproduzierbares RTW-nahes Verhalten. - **Abnahme:** Sprung-, Burst- und Rauschtests zeigen für jeden Modus reproduzierbares RTW-nahes Verhalten.
- [ ] **7. Peak Hold und Bandspeicher wie beim PortaMonitor ergänzen** - [ ] **7. Peak Hold und Bandspeicher wie beim PortaMonitor ergänzen**
+103 -62
View File
@@ -22,8 +22,9 @@ use crate::ppm::{PpmDetector, PpmStandard};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use crate::rta::{ use crate::rta::{
design_fractional_octave_band, design_legacy_repeated_band, exact_fractional_octave_center, design_fractional_octave_band, design_legacy_repeated_band, exact_fractional_octave_center,
fractional_octave_edges, integrate_power, normalize_weighting, weighting_power_correction, fractional_octave_edges, integrate_power, integrate_power_asymmetric, normalize_weighting,
weighting_reference_db, FrequencyWeighting, StereoCascade, RTW_THIRD_OCTAVE_CENTERS, weighting_power_correction, weighting_reference_db, FrequencyWeighting, SlidingPeak,
StereoCascade, RTW_THIRD_OCTAVE_CENTERS,
}; };
use crate::state::NativeWavRecorder; use crate::state::NativeWavRecorder;
use crate::{ use crate::{
@@ -127,11 +128,9 @@ struct RtaBank {
filters: Vec<StereoCascade>, filters: Vec<StereoCascade>,
weighting: FrequencyWeighting, weighting: FrequencyWeighting,
weighting_corrections: Vec<f64>, weighting_corrections: Vec<f64>,
acc_l: Vec<f64>, attack_alpha: f64,
acc_r: Vec<f64>, release_alpha: f64,
block_peaks: Vec<f64>, peak_windows: Option<Vec<SlidingPeak>>,
average_acc: Vec<f64>,
average_samples: u64,
energies: Vec<f64>, energies: Vec<f64>,
levels: Vec<f32>, levels: Vec<f32>,
peaks: Vec<f32>, peaks: Vec<f32>,
@@ -172,6 +171,7 @@ struct FftRtaState {
fft_re_r: Vec<f32>, fft_re_r: Vec<f32>,
fft_im_r: Vec<f32>, fft_im_r: Vec<f32>,
power_bins: Vec<f32>, power_bins: Vec<f32>,
energies: Vec<f64>,
levels: Vec<f32>, levels: Vec<f32>,
peaks: Vec<f32>, peaks: Vec<f32>,
} }
@@ -1235,7 +1235,7 @@ fn build_meter_frame(
if let Some(state) = ppm_state.rta_state.as_mut() { if let Some(state) = ppm_state.rta_state.as_mut() {
match state { match state {
RtaEngineState::Iir(bank) => { RtaEngineState::Iir(bank) => {
finalize_rta_bank(bank, sample_rate, frames, 2, rta_config); finalize_rta_bank(bank, sample_rate, frames);
ppm_state.last_rta = Some(build_iir_rta_frame(bank, sample_rate, rta_config)); ppm_state.last_rta = Some(build_iir_rta_frame(bank, sample_rate, rta_config));
} }
RtaEngineState::Fft(fft) => { RtaEngineState::Fft(fft) => {
@@ -1329,7 +1329,7 @@ fn build_meter_frame(
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn ensure_rta_state(state: &mut PpmState, sample_rate: u32, config: &PhoenixRtaConfig) { fn ensure_rta_state(state: &mut PpmState, sample_rate: u32, config: &PhoenixRtaConfig) {
let signature = format!( let signature = format!(
"{}|{}|{}|{}|{}|{}|{}|{}|{}", "{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{:.6}|{:.6}",
normalize_engine(&config.engine), normalize_engine(&config.engine),
normalize_filterbank(&config.filterbank), normalize_filterbank(&config.filterbank),
normalize_layout(&config.layout), normalize_layout(&config.layout),
@@ -1338,7 +1338,11 @@ fn ensure_rta_state(state: &mut PpmState, sample_rate: u32, config: &PhoenixRtaC
config.order.clamp(2, 8), config.order.clamp(2, 8),
normalize_weighting(&config.weighting), normalize_weighting(&config.weighting),
normalize_fft_size(config.fft_size), normalize_fft_size(config.fft_size),
sample_rate sample_rate,
normalize_rta_detector(&config.detector),
normalize_rta_integration(&config.integration),
config.tau_fast,
config.tau_slow,
); );
if state.rta_state.is_some() && state.rta_signature == signature { if state.rta_state.is_some() && state.rta_signature == signature {
return; return;
@@ -1481,6 +1485,16 @@ fn create_rta_bank(sample_rate: u32, config: &PhoenixRtaConfig) -> RtaBank {
filters.push(StereoCascade::new(coefficients)); filters.push(StereoCascade::new(coefficients));
} }
let len = active_bands.len(); let len = active_bands.len();
let (attack_tau, release_tau) = detector_time_constants(config);
let sample_interval = 1.0 / f64::from(sample_rate.max(1));
let attack_alpha = 1.0 - (-sample_interval / f64::from(attack_tau)).exp();
let release_alpha = 1.0 - (-sample_interval / f64::from(release_tau)).exp();
let peak_window_samples = (sample_rate / 100).max(1) as usize;
let peak_windows = (normalize_rta_detector(&config.detector) == "peak").then(|| {
(0..len)
.map(|_| SlidingPeak::new(peak_window_samples))
.collect()
});
RtaBank { RtaBank {
layout: active_layout, layout: active_layout,
bpo_key, bpo_key,
@@ -1490,11 +1504,9 @@ fn create_rta_bank(sample_rate: u32, config: &PhoenixRtaConfig) -> RtaBank {
filters, filters,
weighting: FrequencyWeighting::new(&config.weighting, sample_rate), weighting: FrequencyWeighting::new(&config.weighting, sample_rate),
weighting_corrections, weighting_corrections,
acc_l: vec![0.0; len], attack_alpha,
acc_r: vec![0.0; len], release_alpha,
block_peaks: vec![0.0; len], peak_windows,
average_acc: vec![0.0; len],
average_samples: 0,
energies: vec![0.0; len], energies: vec![0.0; len],
levels: vec![-120.0; len], levels: vec![-120.0; len],
peaks: vec![-120.0; len], peaks: vec![-120.0; len],
@@ -1533,6 +1545,7 @@ fn create_fft_state(sample_rate: u32, config: &PhoenixRtaConfig) -> FftRtaState
fft_re_r: vec![0.0; fft_size], fft_re_r: vec![0.0; fft_size],
fft_im_r: vec![0.0; fft_size], fft_im_r: vec![0.0; fft_size],
power_bins: vec![0.0; fft_size / 2], power_bins: vec![0.0; fft_size / 2],
energies: vec![0.0; len],
levels: vec![-120.0; len], levels: vec![-120.0; len],
peaks: vec![-120.0; len], peaks: vec![-120.0; len],
} }
@@ -1589,11 +1602,19 @@ fn process_rta_sample(bank: &mut RtaBank, in_l: f32, in_r: f32) {
let (out_l, out_r) = filter.process(weighted_l, weighted_r); let (out_l, out_r) = filter.process(weighted_l, weighted_r);
let out_l = f64::from(out_l); let out_l = f64::from(out_l);
let out_r = f64::from(out_r); let out_r = f64::from(out_r);
bank.acc_l[band_index] += out_l * out_l; if let Some(windows) = bank.peak_windows.as_mut() {
bank.acc_r[band_index] += out_r * out_r; let power = (out_l * out_l).max(out_r * out_r);
bank.block_peaks[band_index] = bank.block_peaks[band_index] bank.energies[band_index] = windows[band_index].push(power);
.max(out_l * out_l) continue;
.max(out_r * out_r); }
let power = 0.5 * (out_l * out_l + out_r * out_r);
let previous = bank.energies[band_index];
let alpha = if power >= previous {
bank.attack_alpha
} else {
bank.release_alpha
};
bank.energies[band_index] = previous + alpha * (power - previous);
} }
} }
@@ -1616,35 +1637,10 @@ fn push_spectro_sample(state: &mut SpectroState, l: f32, r: f32) {
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn finalize_rta_bank( fn finalize_rta_bank(bank: &mut RtaBank, sample_rate: u32, block_size: usize) {
bank: &mut RtaBank,
sample_rate: u32,
block_size: usize,
channel_count: usize,
config: &PhoenixRtaConfig,
) {
let dt = block_size as f32 / sample_rate as f32; let dt = block_size as f32 / sample_rate as f32;
let integration = normalize_rta_integration(&config.integration);
let tau = integration_tau(config);
let release_step = RTA_PEAK_DECAY_DB_PER_S * dt; let release_step = RTA_PEAK_DECAY_DB_PER_S * dt;
let denom = (channel_count.max(1) * block_size.max(1)) as f64;
bank.average_samples = bank.average_samples.saturating_add(block_size as u64);
for i in 0..bank.energies.len() { for i in 0..bank.energies.len() {
let block_sum = bank.acc_l[i]
+ if channel_count > 1 {
bank.acc_r[i]
} else {
0.0
};
let power = block_sum / denom;
bank.average_acc[i] += block_sum / channel_count.max(1) as f64;
bank.energies[i] = match integration {
"peak" => bank.block_peaks[i].max(1.0e-12),
"average" => bank.average_acc[i] / bank.average_samples.max(1) as f64,
_ => integrate_power(bank.energies[i], power, block_size, sample_rate, tau),
}
.max(1.0e-12);
let corrected_energy = bank.energies[i] * bank.weighting_corrections[i]; let corrected_energy = bank.energies[i] * bank.weighting_corrections[i];
let level_db = (10.0 * corrected_energy.max(1.0e-12).log10()) as f32; let level_db = (10.0 * corrected_energy.max(1.0e-12).log10()) as f32;
bank.levels[i] = level_db; bank.levels[i] = level_db;
@@ -1657,10 +1653,6 @@ fn finalize_rta_bank(
.max(level_db) .max(level_db)
.max(RTA_PEAK_FLOOR_DB); .max(RTA_PEAK_FLOOR_DB);
} }
bank.acc_l[i] = 0.0;
bank.acc_r[i] = 0.0;
bank.block_peaks[i] = 0.0;
} }
} }
@@ -1687,8 +1679,6 @@ fn finalize_fft_state(
); );
let dt = state.fft_step_samples as f32 / sample_rate as f32; let dt = state.fft_step_samples as f32 / sample_rate as f32;
let tau = integration_tau(config);
let k = 1.0 - (-dt / tau.max(0.001)).exp();
let release_step = RTA_PEAK_DECAY_DB_PER_S * dt; let release_step = RTA_PEAK_DECAY_DB_PER_S * dt;
let weighting = normalize_weighting(&config.weighting); let weighting = normalize_weighting(&config.weighting);
for (i, band) in state.mapping.iter().enumerate() { for (i, band) in state.mapping.iter().enumerate() {
@@ -1698,11 +1688,39 @@ fn finalize_fft_state(
band_power += *power * seg.weight; band_power += *power * seg.weight;
} }
} }
let weighted = band_power.max(1.0e-12) let weighted = f64::from(
* band_weighting_gain(band.f_lo, band.center, band.f_hi, weighting); band_power.max(1.0e-12)
let db = 10.0 * weighted.max(1.0e-12).log10(); * band_weighting_gain(band.f_lo, band.center, band.f_hi, weighting),
let prev = state.levels[i]; );
state.levels[i] = prev + k * (db - prev); state.energies[i] = match normalize_rta_detector(&config.detector) {
"peak" => integrate_power(
state.energies[i],
weighted,
state.fft_step_samples,
sample_rate,
0.010,
),
_ if normalize_rta_integration(&config.integration) == "impulse" => {
integrate_power_asymmetric(
state.energies[i],
weighted,
state.fft_step_samples,
sample_rate,
0.035,
1.5,
)
}
_ => integrate_power(
state.energies[i],
weighted,
state.fft_step_samples,
sample_rate,
integration_tau(config),
),
}
.max(1.0e-12);
let db = (10.0 * state.energies[i].log10()) as f32;
state.levels[i] = db;
let prev_peak = state.peaks[i]; let prev_peak = state.peaks[i];
if !prev_peak.is_finite() || state.levels[i] >= prev_peak { if !prev_peak.is_finite() || state.levels[i] >= prev_peak {
state.peaks[i] = state.levels[i]; state.peaks[i] = state.levels[i];
@@ -1750,6 +1768,8 @@ fn build_iir_rta_frame(bank: &RtaBank, sample_rate: u32, config: &PhoenixRtaConf
RtaFrame { RtaFrame {
engine: "iir".to_string(), engine: "iir".to_string(),
filterbank: normalize_filterbank(&config.filterbank).to_string(), filterbank: normalize_filterbank(&config.filterbank).to_string(),
detector: normalize_rta_detector(&config.detector).to_string(),
response: normalize_rta_integration(&config.integration).to_string(),
bands_avg: bank.levels.clone(), bands_avg: bank.levels.clone(),
bands_peak: bank.peaks.clone(), bands_peak: bank.peaks.clone(),
centers: bank.centers.clone(), centers: bank.centers.clone(),
@@ -1771,6 +1791,8 @@ fn build_fft_rta_frame(
RtaFrame { RtaFrame {
engine: "fft".to_string(), engine: "fft".to_string(),
filterbank: "none".to_string(), filterbank: "none".to_string(),
detector: normalize_rta_detector(&config.detector).to_string(),
response: normalize_rta_integration(&config.integration).to_string(),
bands_avg: state.levels.clone(), bands_avg: state.levels.clone(),
bands_peak: state.peaks.clone(), bands_peak: state.peaks.clone(),
centers: state.centers.clone(), centers: state.centers.clone(),
@@ -1880,10 +1902,31 @@ fn freq_range_bounds(sample_rate: u32, freq_range: &str) -> (f32, f32) {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn integration_tau(config: &PhoenixRtaConfig) -> f32 { fn integration_tau(config: &PhoenixRtaConfig) -> f32 {
match normalize_rta_integration(&config.integration) { match normalize_rta_integration(&config.integration) {
"impulse" => 0.035,
"medium" => 0.5, "medium" => 0.5,
"slow" => config.tau_slow.max(0.05), "slow" => 1.0,
_ => config.tau_fast.max(0.01), _ => 0.125,
}
}
#[cfg(target_os = "linux")]
fn detector_time_constants(config: &PhoenixRtaConfig) -> (f32, f32) {
if normalize_rta_detector(&config.detector) == "peak" {
return (0.010, 0.010);
}
match normalize_rta_integration(&config.integration) {
"impulse" => (0.035, 1.5),
_ => {
let tau = integration_tau(config);
(tau, tau)
}
}
}
#[cfg(target_os = "linux")]
fn normalize_rta_detector(value: &str) -> &'static str {
match value.trim().to_ascii_lowercase().as_str() {
"peak" => "peak",
_ => "average",
} }
} }
@@ -1893,8 +1936,6 @@ fn normalize_rta_integration(value: &str) -> &'static str {
"impulse" => "impulse", "impulse" => "impulse",
"medium" => "medium", "medium" => "medium",
"slow" => "slow", "slow" => "slow",
"average" => "average",
"peak" => "peak",
_ => "fast", _ => "fast",
} }
} }
+5 -1
View File
@@ -20,6 +20,8 @@ pub struct ServiceStatus {
pub struct RtaFrame { pub struct RtaFrame {
pub engine: String, pub engine: String,
pub filterbank: String, pub filterbank: String,
pub detector: String,
pub response: String,
pub bands_avg: Vec<f32>, pub bands_avg: Vec<f32>,
pub bands_peak: Vec<f32>, pub bands_peak: Vec<f32>,
pub centers: Vec<f32>, pub centers: Vec<f32>,
@@ -53,6 +55,7 @@ pub struct WaveEnvFrame {
pub struct PhoenixRtaConfig { pub struct PhoenixRtaConfig {
pub engine: String, pub engine: String,
pub filterbank: String, pub filterbank: String,
pub detector: String,
pub fft_size: u32, pub fft_size: u32,
pub mono_input: bool, pub mono_input: bool,
pub lr_fractional_delay_enabled: bool, pub lr_fractional_delay_enabled: bool,
@@ -84,6 +87,7 @@ impl Default for PhoenixRtaConfig {
Self { Self {
engine: "iir".to_string(), engine: "iir".to_string(),
filterbank: "legacy".to_string(), filterbank: "legacy".to_string(),
detector: "average".to_string(),
fft_size: 8192, fft_size: 8192,
mono_input: false, mono_input: false,
lr_fractional_delay_enabled: true, lr_fractional_delay_enabled: true,
@@ -92,7 +96,7 @@ impl Default for PhoenixRtaConfig {
freq_range: "norm".to_string(), freq_range: "norm".to_string(),
weighting: "z".to_string(), weighting: "z".to_string(),
order: 6, order: 6,
tau_fast: 0.12, tau_fast: 0.125,
tau_slow: 1.0, tau_slow: 1.0,
integration: "fast".to_string(), integration: "fast".to_string(),
layout: "rtw".to_string(), layout: "rtw".to_string(),
+90
View File
@@ -3,6 +3,8 @@
//! Provides both the original repeated-RBJ Phoenix characteristic and complete //! Provides both the original repeated-RBJ Phoenix characteristic and complete
//! Butterworth fractional-octave bandpasses for controlled comparison. //! Butterworth fractional-octave bandpasses for controlled comparison.
use std::collections::VecDeque;
pub const RTW_THIRD_OCTAVE_CENTERS: &[f32] = &[ pub const RTW_THIRD_OCTAVE_CENTERS: &[f32] = &[
20.0, 25.0, 31.5, 40.0, 50.0, 63.0, 80.0, 100.0, 125.0, 160.0, 200.0, 250.0, 315.0, 400.0, 20.0, 25.0, 31.5, 40.0, 50.0, 63.0, 80.0, 100.0, 125.0, 160.0, 200.0, 250.0, 315.0, 400.0,
500.0, 630.0, 800.0, 1_000.0, 1_250.0, 1_600.0, 2_000.0, 2_500.0, 3_150.0, 4_000.0, 5_000.0, 500.0, 630.0, 800.0, 1_000.0, 1_250.0, 1_600.0, 2_000.0, 2_500.0, 3_150.0, 4_000.0, 5_000.0,
@@ -218,6 +220,58 @@ pub fn integrate_power(
previous + alpha * (block_power - previous) previous + alpha * (block_power - previous)
} }
pub fn integrate_power_asymmetric(
previous: f64,
block_power: f64,
block_samples: usize,
sample_rate: u32,
rise_tau_seconds: f32,
fall_tau_seconds: f32,
) -> f64 {
let tau = if block_power >= previous {
rise_tau_seconds
} else {
fall_tau_seconds
};
integrate_power(previous, block_power, block_samples, sample_rate, tau)
}
pub struct SlidingPeak {
window_samples: u64,
sample_index: u64,
candidates: VecDeque<(u64, f64)>,
}
impl SlidingPeak {
pub fn new(window_samples: usize) -> Self {
let window_samples = window_samples.max(1);
Self {
window_samples: window_samples as u64,
sample_index: 0,
candidates: VecDeque::with_capacity(window_samples + 1),
}
}
pub fn push(&mut self, power: f64) -> f64 {
while self.candidates.back().is_some_and(|entry| entry.1 <= power) {
self.candidates.pop_back();
}
self.candidates.push_back((self.sample_index, power));
let oldest = self
.sample_index
.saturating_sub(self.window_samples.saturating_sub(1));
while self
.candidates
.front()
.is_some_and(|entry| entry.0 < oldest)
{
self.candidates.pop_front();
}
self.sample_index = self.sample_index.wrapping_add(1);
self.candidates.front().map_or(0.0, |entry| entry.1)
}
}
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct StereoBiquad { struct StereoBiquad {
coeffs: BiquadCoeffs, coeffs: BiquadCoeffs,
@@ -492,6 +546,42 @@ mod tests {
} }
} }
#[test]
fn impulse_integration_uses_fast_rise_and_slow_fall() {
let sample_rate = 48_000;
let risen = integrate_power_asymmetric(0.0, 1.0, 1_680, sample_rate, 0.035, 1.5);
let fallen = integrate_power_asymmetric(risen, 0.0, 1_680, sample_rate, 0.035, 1.5);
assert!((risen - (1.0 - (-1.0f64).exp())).abs() < 1.0e-6);
assert!(fallen > risen * 0.97);
}
#[test]
fn ten_millisecond_power_integration_is_block_size_independent() {
fn run(block_size: usize) -> f64 {
let sample_rate = 48_000;
let mut value = 0.0;
let mut processed = 0usize;
while processed < 480 {
let count = block_size.min(480 - processed);
value = integrate_power(value, 1.0, count, sample_rate, 0.010);
processed += count;
}
value
}
assert!((run(48) - run(128)).abs() < 1.0e-12);
assert!((run(128) - (1.0 - (-1.0f64).exp())).abs() < 1.0e-6);
}
#[test]
fn sliding_peak_retains_a_transient_for_exact_window() {
let mut detector = SlidingPeak::new(4);
assert_eq!(detector.push(1.0), 1.0);
assert_eq!(detector.push(0.1), 1.0);
assert_eq!(detector.push(0.2), 1.0);
assert_eq!(detector.push(0.3), 1.0);
assert_eq!(detector.push(0.4), 0.4);
}
#[test] #[test]
fn weighting_is_normalized_and_directionally_correct() { fn weighting_is_normalized_and_directionally_correct() {
for mode in ["a", "c"] { for mode in ["a", "c"] {
+44 -17
View File
@@ -325,6 +325,10 @@ fn normalize_rta_config(mut config: PhoenixRtaConfig) -> PhoenixRtaConfig {
"butterworth" | "butter" => "butterworth".to_string(), "butterworth" | "butter" => "butterworth".to_string(),
_ => "legacy".to_string(), _ => "legacy".to_string(),
}; };
config.detector = match config.detector.trim().to_ascii_lowercase().as_str() {
"peak" => "peak".to_string(),
_ => "average".to_string(),
};
config.fft_size = match config.fft_size { config.fft_size = match config.fft_size {
2048 | 4096 | 8192 | 16384 => config.fft_size, 2048 | 4096 | 8192 | 16384 => config.fft_size,
n if n < 3072 => 2048, n if n < 3072 => 2048,
@@ -362,8 +366,16 @@ fn normalize_rta_config(mut config: PhoenixRtaConfig) -> PhoenixRtaConfig {
"impulse" => "impulse".to_string(), "impulse" => "impulse".to_string(),
"medium" => "medium".to_string(), "medium" => "medium".to_string(),
"slow" => "slow".to_string(), "slow" => "slow".to_string(),
"average" => "average".to_string(), // Compatibility with configurations saved before detector and response
"peak" => "peak".to_string(), // became separate settings.
"peak" => {
config.detector = "peak".to_string();
"fast".to_string()
}
"average" => {
config.detector = "average".to_string();
"fast".to_string()
}
_ => "fast".to_string(), _ => "fast".to_string(),
}; };
config.order = config.order.clamp(2, 8); config.order = config.order.clamp(2, 8);
@@ -376,21 +388,10 @@ fn normalize_rta_config(mut config: PhoenixRtaConfig) -> PhoenixRtaConfig {
config.freq_range = "norm".to_string(); config.freq_range = "norm".to_string();
} }
let tau_fast = if config.tau_fast.is_finite() { // Fast and Slow are named standard responses, not free tuning controls.
config.tau_fast // Keep the serialized fields for old clients, but normalize their values.
} else { config.tau_fast = 0.125;
0.12 config.tau_slow = 1.0;
};
let tau_slow = if config.tau_slow.is_finite() {
config.tau_slow
} else {
1.0
};
config.tau_fast = tau_fast.clamp(0.01, 3.0);
config.tau_slow = tau_slow.clamp(0.05, 10.0);
if config.tau_slow < config.tau_fast {
config.tau_slow = config.tau_fast;
}
let offset_l = if config.input_offset_db_l.is_finite() { let offset_l = if config.input_offset_db_l.is_finite() {
config.input_offset_db_l config.input_offset_db_l
} else { } else {
@@ -751,6 +752,32 @@ mod tests {
assert_eq!(config.integration, "medium"); assert_eq!(config.integration, "medium");
} }
#[test]
fn legacy_peak_mode_migrates_to_peak_detector_with_fast_response() {
let config = normalize_rta_config(PhoenixRtaConfig {
detector: String::new(),
integration: "peak".to_string(),
..PhoenixRtaConfig::default()
});
assert_eq!(config.detector, "peak");
assert_eq!(config.integration, "fast");
}
#[test]
fn legacy_average_mode_migrates_to_average_detector_with_fast_response() {
let config = normalize_rta_config(PhoenixRtaConfig {
detector: String::new(),
integration: "average".to_string(),
tau_fast: 0.12,
tau_slow: 0.8,
..PhoenixRtaConfig::default()
});
assert_eq!(config.detector, "average");
assert_eq!(config.integration, "fast");
assert_eq!(config.tau_fast, 0.125);
assert_eq!(config.tau_slow, 1.0);
}
#[test] #[test]
fn non_third_octave_global_selection_retains_rtw_profile() { fn non_third_octave_global_selection_retains_rtw_profile() {
let mut global = PhoenixGlobalConfig::default(); let mut global = PhoenixGlobalConfig::default();
+4 -1
View File
@@ -517,6 +517,8 @@ function buildPhoenixMeterPacket(frame) {
rta: rta ? { rta: rta ? {
engine: String(rta?.engine || 'iir'), engine: String(rta?.engine || 'iir'),
filterbank: String(rta?.filterbank || 'legacy'), filterbank: String(rta?.filterbank || 'legacy'),
detector: String(rta?.detector || 'average'),
response: String(rta?.response || 'fast'),
bands_avg: Array.isArray(rta?.bands_avg) ? rta.bands_avg : (Array.isArray(rta?.bands) ? rta.bands : []), bands_avg: Array.isArray(rta?.bands_avg) ? rta.bands_avg : (Array.isArray(rta?.bands) ? rta.bands : []),
bands_peak: Array.isArray(rta?.bands_peak) ? rta.bands_peak : [], bands_peak: Array.isArray(rta?.bands_peak) ? rta.bands_peak : [],
bands: Array.isArray(rta?.bands) ? rta.bands : (Array.isArray(rta?.bands_avg) ? rta.bands_avg : []), bands: Array.isArray(rta?.bands) ? rta.bands : (Array.isArray(rta?.bands_avg) ? rta.bands_avg : []),
@@ -873,6 +875,7 @@ export function buildRtaRuntimeConfig(CONFIG = {}) {
return { return {
engine: rtwProfile ? 'iir' : (CONFIG.RTA_ENGINE || 'iir'), engine: rtwProfile ? 'iir' : (CONFIG.RTA_ENGINE || 'iir'),
filterbank: CONFIG.RTA_IIR_FILTERBANK === 'butterworth' ? 'butterworth' : 'legacy', filterbank: CONFIG.RTA_IIR_FILTERBANK === 'butterworth' ? 'butterworth' : 'legacy',
detector: CONFIG.RTA_DETECTOR === 'peak' ? 'peak' : 'average',
fftSize: CONFIG.FFT_SIZE || 4096, fftSize: CONFIG.FFT_SIZE || 4096,
monoInput: !!CONFIG.MONO_INPUT, monoInput: !!CONFIG.MONO_INPUT,
lrFractionalDelayEnabled: !!CONFIG.LR_FRACTIONAL_DELAY_ENABLED, lrFractionalDelayEnabled: !!CONFIG.LR_FRACTIONAL_DELAY_ENABLED,
@@ -881,7 +884,7 @@ export function buildRtaRuntimeConfig(CONFIG = {}) {
freqRange: rtwProfile ? 'norm' : (CONFIG.RTA_FREQ_RANGE || 'norm'), freqRange: rtwProfile ? 'norm' : (CONFIG.RTA_FREQ_RANGE || 'norm'),
weighting: CONFIG.RTA_WEIGHTING || 'z', weighting: CONFIG.RTA_WEIGHTING || 'z',
order: rtwProfile ? 6 : (CONFIG.RTA_IIR_ORDER || 6), order: rtwProfile ? 6 : (CONFIG.RTA_IIR_ORDER || 6),
tauFast: CONFIG.RTA_IIR_TAU_FAST || 0.12, tauFast: CONFIG.RTA_IIR_TAU_FAST || 0.125,
tauSlow: CONFIG.RTA_IIR_TAU_SLOW || 1.0, tauSlow: CONFIG.RTA_IIR_TAU_SLOW || 1.0,
integration: CONFIG.RTA_INTEGRATION || 'fast', integration: CONFIG.RTA_INTEGRATION || 'fast',
layout, layout,
+16 -2
View File
@@ -59,14 +59,15 @@ const CONFIG = {
RTA_BAR_LAYOUT: 'rtw', // 'iec' | 'rtw' RTA_BAR_LAYOUT: 'rtw', // 'iec' | 'rtw'
RTA_BAR_BASE_COLOR: '#ffe066', RTA_BAR_BASE_COLOR: '#ffe066',
HEADER_TEXT_COLOR: '#ffe066', HEADER_TEXT_COLOR: '#ffe066',
RTA_INTEGRATION: 'fast', // 'impulse' | 'fast' | 'medium' | 'slow' | 'average' | 'peak' RTA_DETECTOR: 'average', // 'average' | 'peak'
RTA_INTEGRATION: 'fast', // 'impulse' | 'fast' | 'medium' | 'slow'
RTA_BALLISTICS_MODE: 'average', // 'average' | 'peak' | 'both' RTA_BALLISTICS_MODE: 'average', // 'average' | 'peak' | 'both'
RTA_PEAK_HOLD_MODE: 'auto', // 'off' | 'auto' | 'manual' RTA_PEAK_HOLD_MODE: 'auto', // 'off' | 'auto' | 'manual'
RTA_PEAK_HOLD_SEC: 2.5, RTA_PEAK_HOLD_SEC: 2.5,
RTA_PEAK_DECAY_DB_PER_S: 20, RTA_PEAK_DECAY_DB_PER_S: 20,
RTA_DISPLAY_HOLD_SEC: 0, RTA_DISPLAY_HOLD_SEC: 0,
RTA_IIR_ORDER: 6, RTA_IIR_ORDER: 6,
RTA_IIR_TAU_FAST: 0.12, RTA_IIR_TAU_FAST: 0.125,
RTA_IIR_TAU_SLOW: 1.0, RTA_IIR_TAU_SLOW: 1.0,
SPECTRO_GAMMA: 0.4, SPECTRO_GAMMA: 0.4,
SPECTRO_SCROLL_MODE: 1, // 0.5=Langsam, 1=Normal, 2=Licht, 4=Lächerlich, 6=Wahnsinnig SPECTRO_SCROLL_MODE: 1, // 0.5=Langsam, 1=Normal, 2=Licht, 4=Lächerlich, 6=Wahnsinnig
@@ -788,6 +789,19 @@ function loadConfig(opts = {}) {
CONFIG.RTA_IIR_FILTERBANK = CONFIG.RTA_IIR_FILTERBANK === 'butterworth' CONFIG.RTA_IIR_FILTERBANK = CONFIG.RTA_IIR_FILTERBANK === 'butterworth'
? 'butterworth' ? 'butterworth'
: 'legacy'; : 'legacy';
if (CONFIG.RTA_INTEGRATION === 'peak') {
CONFIG.RTA_DETECTOR = 'peak';
CONFIG.RTA_INTEGRATION = 'fast';
} else if (CONFIG.RTA_INTEGRATION === 'average') {
CONFIG.RTA_DETECTOR = 'average';
CONFIG.RTA_INTEGRATION = 'fast';
}
CONFIG.RTA_DETECTOR = CONFIG.RTA_DETECTOR === 'peak' ? 'peak' : 'average';
if (!['impulse', 'fast', 'medium', 'slow'].includes(CONFIG.RTA_INTEGRATION)) {
CONFIG.RTA_INTEGRATION = 'fast';
}
CONFIG.RTA_IIR_TAU_FAST = 0.125;
CONFIG.RTA_IIR_TAU_SLOW = 1.0;
if (CONFIG.RTA_BAR_LAYOUT === 'rtw') { if (CONFIG.RTA_BAR_LAYOUT === 'rtw') {
CONFIG.RTA_ENGINE = 'iir'; CONFIG.RTA_ENGINE = 'iir';
CONFIG.RTA_IIR_ORDER = 6; CONFIG.RTA_IIR_ORDER = 6;
+19 -14
View File
@@ -325,12 +325,12 @@
<small>Filtergüte Ordnung des IIR-Filters</small> <small>Filtergüte Ordnung des IIR-Filters</small>
</div> </div>
<div class="opt"><label>IIR Fast τ (s)</label> <div class="opt"><label>IIR Fast τ (s)</label>
<input id="opt_rtaTauFast" type="number" min="0.01" max="1" step="0.01" style="width:110px"> <input id="opt_rtaTauFast" type="number" value="0.125" style="width:110px" disabled>
<small>Schnelles Integrationsfenster</small> <small>Feste Standardreaktion: 125 ms</small>
</div> </div>
<div class="opt"><label>IIR Slow τ (s)</label> <div class="opt"><label>IIR Slow τ (s)</label>
<input id="opt_rtaTauSlow" type="number" min="0.1" max="5" step="0.1" style="width:110px"> <input id="opt_rtaTauSlow" type="number" value="1.0" style="width:110px" disabled>
<small>Langsames Integrationsfenster</small> <small>Feste Standardreaktion: 1 Sekunde</small>
</div> </div>
<div class="opt"><label>Frequenzbereich</label> <div class="opt"><label>Frequenzbereich</label>
<select id="opt_rtaFreq" style="width:150px"> <select id="opt_rtaFreq" style="width:150px">
@@ -359,24 +359,29 @@
</select> </select>
<small>Pegelgewichtung</small> <small>Pegelgewichtung</small>
</div> </div>
<div class="opt"><label>Integrationszeit</label> <div class="opt"><label>RTA-Detektor</label>
<select id="opt_rtaDetector" style="width:180px">
<option value="average" selected>Average (RMS)</option>
<option value="peak">Peak (10 ms)</option>
</select>
<small>RTW-Detektortyp: RMS mit gewählter Response oder feste 10-ms-Peakintegration</small>
</div>
<div class="opt"><label>Response</label>
<select id="opt_rtaIntegration" style="width:150px"> <select id="opt_rtaIntegration" style="width:150px">
<option value="impulse">Impulse</option> <option value="impulse">Impulse</option>
<option value="fast" selected>Fast</option> <option value="fast" selected>Fast</option>
<option value="medium">Medium</option> <option value="medium">Medium</option>
<option value="slow">Slow</option> <option value="slow">Slow</option>
<option value="average">Average</option>
<option value="peak">Peak (10 ms)</option>
</select> </select>
<small>Reaktionsmodus</small> <small>Average-Ballistik: Impulse 35 ms/1,5 s, Fast 125 ms, Medium 500 ms oder Slow 1 s</small>
</div> </div>
<div class="opt"><label>RTA Ballistics</label> <div class="opt"><label>RTA-Anzeige</label>
<select id="opt_rtaBallistics" style="width:200px"> <select id="opt_rtaBallistics" style="width:200px">
<option value="average" selected>Average (RMS)</option> <option value="average" selected>Detektorwert</option>
<option value="peak">Peak</option> <option value="peak">Peak Hold</option>
<option value="both">Average + Peak Overlay</option> <option value="both">Detektorwert + Peak Hold</option>
</select> </select>
<small>Angezeigte Kurven</small> <small>Auswahl der angezeigten Mess- und Hold-Kurven</small>
</div> </div>
<div class="opt"><label>Hold Mode</label> <div class="opt"><label>Hold Mode</label>
<select id="opt_rtaHoldMode" style="width:150px"> <select id="opt_rtaHoldMode" style="width:150px">
@@ -1267,7 +1272,7 @@
<li><strong>Spektrogramm:</strong> Langzeitstillstand und stotterndes Nachholen beseitigt. Inkrementelles Spaltenzeichnen, Worker-ACK, Watchdog/Neustart und begrenztes Überspringen veralteter Spalten ergänzt. 0,5×, 1×, 2×, 4× und 6× besitzen nun eine feste, FFT- und DPI-unabhängige Zeitbasis.</li> <li><strong>Spektrogramm:</strong> Langzeitstillstand und stotterndes Nachholen beseitigt. Inkrementelles Spaltenzeichnen, Worker-ACK, Watchdog/Neustart und begrenztes Überspringen veralteter Spalten ergänzt. 0,5×, 1×, 2×, 4× und 6× besitzen nun eine feste, FFT- und DPI-unabhängige Zeitbasis.</li>
<li><strong>DIN-/EBU-PPM:</strong> Blockunabhängige Quasi-Peak-Detektoren mit bandbegrenzter Interpolation, korrekter Attack- und Rücklaufballistik sowie automatischen Tonburst-, Frequenzgang- und Polaritätstests implementiert. Eine zweite Browser-Anstiegsballistik wurde entfernt.</li> <li><strong>DIN-/EBU-PPM:</strong> Blockunabhängige Quasi-Peak-Detektoren mit bandbegrenzter Interpolation, korrekter Attack- und Rücklaufballistik sowie automatischen Tonburst-, Frequenzgang- und Polaritätstests implementiert. Eine zweite Browser-Anstiegsballistik wurde entfernt.</li>
<li><strong>RTW-RTA:</strong> RTW-Profil verbindlich auf die IIR-Oktavteilband-Filterbank festgelegt. Bis zur Vergleichsmessung am realen RTW-Gerät sind die bisherige Phoenix/RTW-Charakteristik und vollständige Butterworth-Bandpässe sechster Ordnung getrennt auswählbar.</li> <li><strong>RTW-RTA:</strong> RTW-Profil verbindlich auf die IIR-Oktavteilband-Filterbank festgelegt. Bis zur Vergleichsmessung am realen RTW-Gerät sind die bisherige Phoenix/RTW-Charakteristik und vollständige Butterworth-Bandpässe sechster Ordnung getrennt auswählbar.</li>
<li><strong>RTA-Bewertung und Integration:</strong> A-, C- und Z-Bewertung sampleweise vor der Filterbank ergänzt. Fast, Medium, Slow, Impulse, Average und Peak rechnen im Leistungsbereich und unabhängig von der ALSA-Periodengröße.</li> <li><strong>RTA-Detektor und Reaktionszeit:</strong> Average (RMS) und Peak (gleitendes 10-ms-Maximum) sind nun getrennt von Fast, Medium, Slow und Impulse wählbar. Fast wurde auf 125 ms korrigiert; Impulse verwendet 35 ms Anstieg und 1,5 s Rücklauf. Die IIR-Detektoren arbeiten samplegenau und unabhängig von der ALSA-Periodengröße. Alte Einstellungen werden automatisch migriert.</li>
<li><strong>RTA-Auflösung:</strong> Umschaltung zwischen 1/3, 1/6 und 1/12 repariert. Alle drei Auflösungen bleiben im RTW-Profil aktiv und verwenden dessen IIR-Filterbank, Darstellung und Ballistik.</li> <li><strong>RTA-Auflösung:</strong> Umschaltung zwischen 1/3, 1/6 und 1/12 repariert. Alle drei Auflösungen bleiben im RTW-Profil aktiv und verwenden dessen IIR-Filterbank, Darstellung und Ballistik.</li>
<li><strong>Hardwareparameter:</strong> Tatsächlich von ALSA ausgehandelte Samplerate, Perioden- und Puffergröße werden nun durch DSP, Aufnahme, Status und Browser verwendet; die feste 48-kHz-Annahme wurde entfernt.</li> <li><strong>Hardwareparameter:</strong> Tatsächlich von ALSA ausgehandelte Samplerate, Perioden- und Puffergröße werden nun durch DSP, Aufnahme, Status und Browser verwendet; die feste 48-kHz-Annahme wurde entfernt.</li>
<li><strong>Goniometer:</strong> Nur neue XY-Samplepaare werden mit einer periodengrößenunabhängigen mittleren Rate von 60 Hz übertragen. Punktbegrenzung wirkt bereits auf den Transport; begrenzte und wiederverwendbare Spurpuffer reduzieren Speicher- und Zeichenlast.</li> <li><strong>Goniometer:</strong> Nur neue XY-Samplepaare werden mit einer periodengrößenunabhängigen mittleren Rate von 60 Hz übertragen. Punktbegrenzung wirkt bereits auf den Transport; begrenzte und wiederverwendbare Spurpuffer reduzieren Speicher- und Zeichenlast.</li>
+17 -4
View File
@@ -287,6 +287,7 @@ function syncUI() {
['opt_rtaLayout', CONFIG.RTA_BAR_LAYOUT || 'iec'], ['opt_rtaLayout', CONFIG.RTA_BAR_LAYOUT || 'iec'],
['opt_rtaBarBaseColor', CONFIG.RTA_BAR_BASE_COLOR || '#ffe066'], ['opt_rtaBarBaseColor', CONFIG.RTA_BAR_BASE_COLOR || '#ffe066'],
['opt_rtaWeighting', CONFIG.RTA_WEIGHTING || 'z'], ['opt_rtaWeighting', CONFIG.RTA_WEIGHTING || 'z'],
['opt_rtaDetector', CONFIG.RTA_DETECTOR || 'average'],
['opt_rtaIntegration', CONFIG.RTA_INTEGRATION || 'fast'], ['opt_rtaIntegration', CONFIG.RTA_INTEGRATION || 'fast'],
['opt_rtaBallistics', CONFIG.RTA_BALLISTICS_MODE || 'average'], ['opt_rtaBallistics', CONFIG.RTA_BALLISTICS_MODE || 'average'],
['opt_rtaHoldMode', CONFIG.RTA_PEAK_HOLD_MODE || 'auto'], ['opt_rtaHoldMode', CONFIG.RTA_PEAK_HOLD_MODE || 'auto'],
@@ -303,7 +304,7 @@ function syncUI() {
['opt_waveColorL', CONFIG.WAVEFORM_COLOR_LEFT || '#00e7ff'], ['opt_waveColorL', CONFIG.WAVEFORM_COLOR_LEFT || '#00e7ff'],
['opt_waveColorR', CONFIG.WAVEFORM_COLOR_RIGHT || '#ff6b81'], ['opt_waveColorR', CONFIG.WAVEFORM_COLOR_RIGHT || '#ff6b81'],
['opt_waveColorDiff', CONFIG.WAVEFORM_COLOR_DIFF || '#00e7ff'], ['opt_waveColorDiff', CONFIG.WAVEFORM_COLOR_DIFF || '#00e7ff'],
['opt_rtaTauFast', CONFIG.RTA_IIR_TAU_FAST ?? 0.12], ['opt_rtaTauFast', CONFIG.RTA_IIR_TAU_FAST ?? 0.125],
['opt_rtaTauSlow', CONFIG.RTA_IIR_TAU_SLOW ?? 1.0], ['opt_rtaTauSlow', CONFIG.RTA_IIR_TAU_SLOW ?? 1.0],
['opt_rtRenderMode', CONFIG.REALTIME_RENDER_STYLE || 'bars'], ['opt_rtRenderMode', CONFIG.REALTIME_RENDER_STYLE || 'bars'],
['opt_rtHold', CONFIG.REALTIME_BAR_HOLD_MS], ['opt_rtHold', CONFIG.REALTIME_BAR_HOLD_MS],
@@ -418,6 +419,12 @@ function syncUI() {
const iWin = E('opt_lufsIWindowMin'); const iWin = E('opt_lufsIWindowMin');
if (iWin) iWin.disabled = !!CONFIG.LUFS_I_NORM_ENABLED; if (iWin) iWin.disabled = !!CONFIG.LUFS_I_NORM_ENABLED;
const rtaResponse = E('opt_rtaIntegration');
if (rtaResponse) rtaResponse.disabled = CONFIG.RTA_DETECTOR === 'peak';
const rtaTauFast = E('opt_rtaTauFast');
if (rtaTauFast) rtaTauFast.disabled = true;
const rtaTauSlow = E('opt_rtaTauSlow');
if (rtaTauSlow) rtaTauSlow.disabled = true;
const mp3Wrap = E('opt_recMp3BitrateWrap'); const mp3Wrap = E('opt_recMp3BitrateWrap');
if (mp3Wrap) mp3Wrap.style.display = (String(CONFIG.RECORD_OUTPUT_FORMAT || 'wav') === 'mp3') ? '' : 'none'; if (mp3Wrap) mp3Wrap.style.display = (String(CONFIG.RECORD_OUTPUT_FORMAT || 'wav') === 'mp3') ? '' : 'none';
const mp3Sel = E('opt_recMp3Bitrate'); const mp3Sel = E('opt_recMp3Bitrate');
@@ -1133,8 +1140,14 @@ function wireHandlers(env) {
const allowed = new Set(['z','a','c']); const allowed = new Set(['z','a','c']);
CONFIG.RTA_WEIGHTING = allowed.has(v) ? v : 'z'; CONFIG.RTA_WEIGHTING = allowed.has(v) ? v : 'z';
}); });
h('opt_rtaDetector', v => {
CONFIG.RTA_DETECTOR = v === 'peak' ? 'peak' : 'average';
const response = E('opt_rtaIntegration');
if (response) response.disabled = CONFIG.RTA_DETECTOR === 'peak';
return CONFIG.RTA_DETECTOR;
});
h('opt_rtaIntegration', v => { h('opt_rtaIntegration', v => {
const allowed = new Set(['impulse','fast','medium','slow','average','peak']); const allowed = new Set(['impulse','fast','medium','slow']);
CONFIG.RTA_INTEGRATION = allowed.has(v) ? v : 'fast'; CONFIG.RTA_INTEGRATION = allowed.has(v) ? v : 'fast';
return CONFIG.RTA_INTEGRATION; return CONFIG.RTA_INTEGRATION;
}); });
@@ -1212,8 +1225,8 @@ function wireHandlers(env) {
CONFIG.WAVEFORM_COLOR_DIFF = v; CONFIG.WAVEFORM_COLOR_DIFF = v;
notifyPhoenixGlobalConfig(); notifyPhoenixGlobalConfig();
}); });
h('opt_rtaTauFast', v => { CONFIG.RTA_IIR_TAU_FAST = clamp(+v, 0.01, 1.0); }); h('opt_rtaTauFast', () => { CONFIG.RTA_IIR_TAU_FAST = 0.125; return 0.125; });
h('opt_rtaTauSlow', v => { CONFIG.RTA_IIR_TAU_SLOW = clamp(+v, 0.1, 5.0); }); h('opt_rtaTauSlow', () => { CONFIG.RTA_IIR_TAU_SLOW = 1.0; return 1.0; });
h('opt_rtRenderMode', v => { CONFIG.REALTIME_RENDER_STYLE = (v === 'line') ? 'line' : 'bars'; }); h('opt_rtRenderMode', v => { CONFIG.REALTIME_RENDER_STYLE = (v === 'line') ? 'line' : 'bars'; });
h('opt_rtHold', v => CONFIG.REALTIME_BAR_HOLD_MS = clamp(+v, 100, 5000)); h('opt_rtHold', v => CONFIG.REALTIME_BAR_HOLD_MS = clamp(+v, 100, 5000));
h('opt_rtDecay', v => CONFIG.REALTIME_BAR_DECAY_DB_PER_S = clamp(+v, 1, 60)); h('opt_rtDecay', v => CONFIG.REALTIME_BAR_DECAY_DB_PER_S = clamp(+v, 1, 60));
+8 -5
View File
@@ -31,10 +31,9 @@ const PEAK_HOLD_MAP = new Map(Object.entries({
})); }));
const INTEGRATION_TAU = { const INTEGRATION_TAU = {
impulse: 0.035,
fast: 0.125, fast: 0.125,
medium: 0.5,
slow: 1.0, slow: 1.0,
peak: 0.01,
}; };
@@ -283,6 +282,7 @@ function buildConfigSignature(CONFIG, nyq, binCount) {
CONFIG.RTA_BPO_MODE, CONFIG.RTA_BPO_MODE,
CONFIG.RTA_WEIGHTING, CONFIG.RTA_WEIGHTING,
CONFIG.RTA_BAR_LAYOUT, CONFIG.RTA_BAR_LAYOUT,
CONFIG.RTA_DETECTOR,
CONFIG.RTA_INTEGRATION, CONFIG.RTA_INTEGRATION,
CONFIG.RTA_PEAK_HOLD_MODE, CONFIG.RTA_PEAK_HOLD_MODE,
CONFIG.RTA_PEAK_HOLD_SEC, CONFIG.RTA_PEAK_HOLD_SEC,
@@ -383,15 +383,14 @@ function computeBandLevels(state, utils, buf, CONFIG, range) {
} }
function applyIntegration(state, levels, CONFIG, range) { function applyIntegration(state, levels, CONFIG, range) {
const mode = (CONFIG.RTA_INTEGRATION && INTEGRATION_TAU[CONFIG.RTA_INTEGRATION]) const mode = (CONFIG.RTA_INTEGRATION === 'impulse' || INTEGRATION_TAU[CONFIG.RTA_INTEGRATION])
? CONFIG.RTA_INTEGRATION ? CONFIG.RTA_INTEGRATION
: 'fast'; : 'fast';
const tau = INTEGRATION_TAU[mode] || 0.125; const peakDetector = CONFIG.RTA_DETECTOR === 'peak';
const now = performance.now(); const now = performance.now();
const last = state.lastIntegrationTs || now; const last = state.lastIntegrationTs || now;
const dt = Math.max(1 / 240, (now - last) / 1000); const dt = Math.max(1 / 240, (now - last) / 1000);
state.lastIntegrationTs = now; state.lastIntegrationTs = now;
const alpha = (tau > 0 && dt > 0) ? 1 - Math.exp(-dt / tau) : 1;
if (!state.ewma || state.ewma.length !== levels.length) { if (!state.ewma || state.ewma.length !== levels.length) {
state.ewma = new Float32Array(levels.length); state.ewma = new Float32Array(levels.length);
@@ -400,6 +399,10 @@ function applyIntegration(state, levels, CONFIG, range) {
const buffer = state.ewma; const buffer = state.ewma;
for (let i = 0; i < levels.length; i++) { for (let i = 0; i < levels.length; i++) {
const prev = buffer[i]; const prev = buffer[i];
const tau = peakDetector
? 0.010
: (mode === 'impulse' ? (levels[i] >= prev ? 0.035 : 1.5) : (INTEGRATION_TAU[mode] || 0.125));
const alpha = (tau > 0 && dt > 0) ? 1 - Math.exp(-dt / tau) : 1;
buffer[i] = prev + (levels[i] - prev) * alpha; buffer[i] = prev + (levels[i] - prev) * alpha;
} }
return buffer; return buffer;