Correct RTA detector timing and peak integration
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user