From 9b5250d42b744b149bc21bd9a55f6b35eb4a6177 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:29:41 +0200 Subject: [PATCH] Correct RTA detector timing and peak integration --- TODO.md | 12 +-- src/audio.rs | 165 ++++++++++++++++++++++++++---------------- src/model.rs | 6 +- src/rta.rs | 90 +++++++++++++++++++++++ src/state.rs | 61 +++++++++++----- www/core/audio.js | 5 +- www/core/config.js | 18 ++++- www/index.html | 33 +++++---- www/ui/options.js | 21 +++++- www/views/realtime.js | 13 ++-- 10 files changed, 312 insertions(+), 112 deletions(-) diff --git a/TODO.md b/TODO.md index 4aeb059..d0b6a37 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,7 @@ - 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 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. - 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. @@ -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. - **Abnahme:** Jedes der 31 Bänder besteht automatisierte Sweep- und Pegeltests innerhalb der festgelegten Toleranzen. -- [ ] **6. RTW-Integrationsmodi vollständig und energetisch korrekt implementieren** - - **Soll:** Fast, Medium, Slow, Average und Peak mit dokumentiertem RTW-nahem Verhalten. - - **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. - - **Geprüft:** Ein Regressionstest bestätigt identische Integration bei unterschiedlichen Blockgrößen. - - **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. +- [ ] **6. RTW-Detektor und Reaktionszeiten vollständig und energetisch korrekt implementieren** + - **Soll:** Getrennte Auswahl von Average-/Peak-Detektor und der RMS-Reaktionszeit entsprechend der RTW-Bedienlogik. + - **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:** Regressionstests prüfen Blockgrößenunabhängigkeit, 10-ms-Peakintegration, Impulse-Anstieg/-Rücklauf sowie die Konfigurationsmigration. + - **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. - [ ] **7. Peak Hold und Bandspeicher wie beim PortaMonitor ergänzen** diff --git a/src/audio.rs b/src/audio.rs index 3d4a5e3..faceb64 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -22,8 +22,9 @@ use crate::ppm::{PpmDetector, PpmStandard}; #[cfg(target_os = "linux")] use crate::rta::{ design_fractional_octave_band, design_legacy_repeated_band, exact_fractional_octave_center, - fractional_octave_edges, integrate_power, normalize_weighting, weighting_power_correction, - weighting_reference_db, FrequencyWeighting, StereoCascade, RTW_THIRD_OCTAVE_CENTERS, + fractional_octave_edges, integrate_power, integrate_power_asymmetric, normalize_weighting, + weighting_power_correction, weighting_reference_db, FrequencyWeighting, SlidingPeak, + StereoCascade, RTW_THIRD_OCTAVE_CENTERS, }; use crate::state::NativeWavRecorder; use crate::{ @@ -127,11 +128,9 @@ struct RtaBank { filters: Vec, weighting: FrequencyWeighting, weighting_corrections: Vec, - acc_l: Vec, - acc_r: Vec, - block_peaks: Vec, - average_acc: Vec, - average_samples: u64, + attack_alpha: f64, + release_alpha: f64, + peak_windows: Option>, energies: Vec, levels: Vec, peaks: Vec, @@ -172,6 +171,7 @@ struct FftRtaState { fft_re_r: Vec, fft_im_r: Vec, power_bins: Vec, + energies: Vec, levels: Vec, peaks: Vec, } @@ -1235,7 +1235,7 @@ fn build_meter_frame( if let Some(state) = ppm_state.rta_state.as_mut() { match state { 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)); } RtaEngineState::Fft(fft) => { @@ -1329,7 +1329,7 @@ fn build_meter_frame( #[cfg(target_os = "linux")] fn ensure_rta_state(state: &mut PpmState, sample_rate: u32, config: &PhoenixRtaConfig) { let signature = format!( - "{}|{}|{}|{}|{}|{}|{}|{}|{}", + "{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{:.6}|{:.6}", normalize_engine(&config.engine), normalize_filterbank(&config.filterbank), 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), normalize_weighting(&config.weighting), 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 { return; @@ -1481,6 +1485,16 @@ fn create_rta_bank(sample_rate: u32, config: &PhoenixRtaConfig) -> RtaBank { filters.push(StereoCascade::new(coefficients)); } 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 { layout: active_layout, bpo_key, @@ -1490,11 +1504,9 @@ fn create_rta_bank(sample_rate: u32, config: &PhoenixRtaConfig) -> RtaBank { filters, weighting: FrequencyWeighting::new(&config.weighting, sample_rate), weighting_corrections, - acc_l: vec![0.0; len], - acc_r: vec![0.0; len], - block_peaks: vec![0.0; len], - average_acc: vec![0.0; len], - average_samples: 0, + attack_alpha, + release_alpha, + peak_windows, energies: vec![0.0; len], levels: 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_im_r: vec![0.0; fft_size], power_bins: vec![0.0; fft_size / 2], + energies: vec![0.0; len], levels: 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 = f64::from(out_l); let out_r = f64::from(out_r); - bank.acc_l[band_index] += out_l * out_l; - bank.acc_r[band_index] += out_r * out_r; - bank.block_peaks[band_index] = bank.block_peaks[band_index] - .max(out_l * out_l) - .max(out_r * out_r); + if let Some(windows) = bank.peak_windows.as_mut() { + let power = (out_l * out_l).max(out_r * out_r); + bank.energies[band_index] = windows[band_index].push(power); + continue; + } + 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")] -fn finalize_rta_bank( - bank: &mut RtaBank, - sample_rate: u32, - block_size: usize, - channel_count: usize, - config: &PhoenixRtaConfig, -) { +fn finalize_rta_bank(bank: &mut RtaBank, sample_rate: u32, block_size: usize) { 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 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() { - 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 level_db = (10.0 * corrected_energy.max(1.0e-12).log10()) as f32; bank.levels[i] = level_db; @@ -1657,10 +1653,6 @@ fn finalize_rta_bank( .max(level_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 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 weighting = normalize_weighting(&config.weighting); for (i, band) in state.mapping.iter().enumerate() { @@ -1698,11 +1688,39 @@ fn finalize_fft_state( band_power += *power * seg.weight; } } - let weighted = band_power.max(1.0e-12) - * band_weighting_gain(band.f_lo, band.center, band.f_hi, weighting); - let db = 10.0 * weighted.max(1.0e-12).log10(); - let prev = state.levels[i]; - state.levels[i] = prev + k * (db - prev); + let weighted = f64::from( + band_power.max(1.0e-12) + * band_weighting_gain(band.f_lo, band.center, band.f_hi, weighting), + ); + 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]; if !prev_peak.is_finite() || state.levels[i] >= prev_peak { state.peaks[i] = state.levels[i]; @@ -1750,6 +1768,8 @@ fn build_iir_rta_frame(bank: &RtaBank, sample_rate: u32, config: &PhoenixRtaConf RtaFrame { engine: "iir".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_peak: bank.peaks.clone(), centers: bank.centers.clone(), @@ -1771,6 +1791,8 @@ fn build_fft_rta_frame( RtaFrame { engine: "fft".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_peak: state.peaks.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")] fn integration_tau(config: &PhoenixRtaConfig) -> f32 { match normalize_rta_integration(&config.integration) { - "impulse" => 0.035, "medium" => 0.5, - "slow" => config.tau_slow.max(0.05), - _ => config.tau_fast.max(0.01), + "slow" => 1.0, + _ => 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", "medium" => "medium", "slow" => "slow", - "average" => "average", - "peak" => "peak", _ => "fast", } } diff --git a/src/model.rs b/src/model.rs index 2bf1437..06e50a2 100644 --- a/src/model.rs +++ b/src/model.rs @@ -20,6 +20,8 @@ pub struct ServiceStatus { pub struct RtaFrame { pub engine: String, pub filterbank: String, + pub detector: String, + pub response: String, pub bands_avg: Vec, pub bands_peak: Vec, pub centers: Vec, @@ -53,6 +55,7 @@ pub struct WaveEnvFrame { pub struct PhoenixRtaConfig { pub engine: String, pub filterbank: String, + pub detector: String, pub fft_size: u32, pub mono_input: bool, pub lr_fractional_delay_enabled: bool, @@ -84,6 +87,7 @@ impl Default for PhoenixRtaConfig { Self { engine: "iir".to_string(), filterbank: "legacy".to_string(), + detector: "average".to_string(), fft_size: 8192, mono_input: false, lr_fractional_delay_enabled: true, @@ -92,7 +96,7 @@ impl Default for PhoenixRtaConfig { freq_range: "norm".to_string(), weighting: "z".to_string(), order: 6, - tau_fast: 0.12, + tau_fast: 0.125, tau_slow: 1.0, integration: "fast".to_string(), layout: "rtw".to_string(), diff --git a/src/rta.rs b/src/rta.rs index 18690a8..6fe012a 100644 --- a/src/rta.rs +++ b/src/rta.rs @@ -3,6 +3,8 @@ //! Provides both the original repeated-RBJ Phoenix characteristic and complete //! Butterworth fractional-octave bandpasses for controlled comparison. +use std::collections::VecDeque; + 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, 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) } +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)] struct StereoBiquad { 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] fn weighting_is_normalized_and_directionally_correct() { for mode in ["a", "c"] { diff --git a/src/state.rs b/src/state.rs index d1bf365..965d5dd 100644 --- a/src/state.rs +++ b/src/state.rs @@ -325,6 +325,10 @@ fn normalize_rta_config(mut config: PhoenixRtaConfig) -> PhoenixRtaConfig { "butterworth" | "butter" => "butterworth".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 { 2048 | 4096 | 8192 | 16384 => config.fft_size, n if n < 3072 => 2048, @@ -362,8 +366,16 @@ fn normalize_rta_config(mut config: PhoenixRtaConfig) -> PhoenixRtaConfig { "impulse" => "impulse".to_string(), "medium" => "medium".to_string(), "slow" => "slow".to_string(), - "average" => "average".to_string(), - "peak" => "peak".to_string(), + // Compatibility with configurations saved before detector and response + // became separate settings. + "peak" => { + config.detector = "peak".to_string(); + "fast".to_string() + } + "average" => { + config.detector = "average".to_string(); + "fast".to_string() + } _ => "fast".to_string(), }; 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(); } - let tau_fast = if config.tau_fast.is_finite() { - config.tau_fast - } else { - 0.12 - }; - 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; - } + // Fast and Slow are named standard responses, not free tuning controls. + // Keep the serialized fields for old clients, but normalize their values. + config.tau_fast = 0.125; + config.tau_slow = 1.0; let offset_l = if config.input_offset_db_l.is_finite() { config.input_offset_db_l } else { @@ -751,6 +752,32 @@ mod tests { 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] fn non_third_octave_global_selection_retains_rtw_profile() { let mut global = PhoenixGlobalConfig::default(); diff --git a/www/core/audio.js b/www/core/audio.js index bd556fd..a974c06 100644 --- a/www/core/audio.js +++ b/www/core/audio.js @@ -517,6 +517,8 @@ function buildPhoenixMeterPacket(frame) { rta: rta ? { engine: String(rta?.engine || 'iir'), 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_peak: Array.isArray(rta?.bands_peak) ? rta.bands_peak : [], bands: Array.isArray(rta?.bands) ? rta.bands : (Array.isArray(rta?.bands_avg) ? rta.bands_avg : []), @@ -873,6 +875,7 @@ export function buildRtaRuntimeConfig(CONFIG = {}) { return { engine: rtwProfile ? 'iir' : (CONFIG.RTA_ENGINE || 'iir'), filterbank: CONFIG.RTA_IIR_FILTERBANK === 'butterworth' ? 'butterworth' : 'legacy', + detector: CONFIG.RTA_DETECTOR === 'peak' ? 'peak' : 'average', fftSize: CONFIG.FFT_SIZE || 4096, monoInput: !!CONFIG.MONO_INPUT, lrFractionalDelayEnabled: !!CONFIG.LR_FRACTIONAL_DELAY_ENABLED, @@ -881,7 +884,7 @@ export function buildRtaRuntimeConfig(CONFIG = {}) { freqRange: rtwProfile ? 'norm' : (CONFIG.RTA_FREQ_RANGE || 'norm'), weighting: CONFIG.RTA_WEIGHTING || 'z', 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, integration: CONFIG.RTA_INTEGRATION || 'fast', layout, diff --git a/www/core/config.js b/www/core/config.js index eb714ad..4a5ef30 100644 --- a/www/core/config.js +++ b/www/core/config.js @@ -59,14 +59,15 @@ const CONFIG = { RTA_BAR_LAYOUT: 'rtw', // 'iec' | 'rtw' RTA_BAR_BASE_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_PEAK_HOLD_MODE: 'auto', // 'off' | 'auto' | 'manual' RTA_PEAK_HOLD_SEC: 2.5, RTA_PEAK_DECAY_DB_PER_S: 20, RTA_DISPLAY_HOLD_SEC: 0, RTA_IIR_ORDER: 6, - RTA_IIR_TAU_FAST: 0.12, + RTA_IIR_TAU_FAST: 0.125, RTA_IIR_TAU_SLOW: 1.0, SPECTRO_GAMMA: 0.4, 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' ? 'butterworth' : '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') { CONFIG.RTA_ENGINE = 'iir'; CONFIG.RTA_IIR_ORDER = 6; diff --git a/www/index.html b/www/index.html index 57c6b92..08930fd 100644 --- a/www/index.html +++ b/www/index.html @@ -325,12 +325,12 @@ Filtergüte – Ordnung des IIR-Filters
- - Schnelles Integrationsfenster + + Feste Standardreaktion: 125 ms
- - Langsames Integrationsfenster + + Feste Standardreaktion: 1 Sekunde
Pegelgewichtung
-
+
+ + RTW-Detektortyp: RMS mit gewählter Response oder feste 10-ms-Peakintegration +
+
- Reaktionsmodus + Average-Ballistik: Impulse 35 ms/1,5 s, Fast 125 ms, Medium 500 ms oder Slow 1 s
-
+
- Angezeigte Kurven + Auswahl der angezeigten Mess- und Hold-Kurven