Fix RTA peak hold and FFT band energy
This commit is contained in:
+230
-42
@@ -40,9 +40,6 @@ use crate::{
|
||||
// 128-sample periods at 48 kHz -> 62.5 visual updates/s. The DSP history is
|
||||
// still updated sample-by-sample; only transport snapshots are rate-limited.
|
||||
const XY_TARGET_UPDATES_PER_SECOND: u64 = 60;
|
||||
#[cfg(target_os = "linux")]
|
||||
const RTA_PEAK_DECAY_DB_PER_S: f32 = 10.0;
|
||||
#[cfg(target_os = "linux")]
|
||||
const RTA_PEAK_FLOOR_DB: f32 = -150.0;
|
||||
#[cfg(target_os = "linux")]
|
||||
const WAVE_ENV_COLUMNS_PER_SEC: f32 = 9600.0;
|
||||
@@ -133,7 +130,7 @@ struct RtaBank {
|
||||
peak_windows: Option<Vec<SlidingPeak>>,
|
||||
energies: Vec<f64>,
|
||||
levels: Vec<f32>,
|
||||
peaks: Vec<f32>,
|
||||
peak_hold: RtaPeakHold,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -173,7 +170,89 @@ struct FftRtaState {
|
||||
power_bins: Vec<f32>,
|
||||
energies: Vec<f64>,
|
||||
levels: Vec<f32>,
|
||||
peaks: Vec<f32>,
|
||||
peak_hold: RtaPeakHold,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum FftPowerScale {
|
||||
BinAmplitude,
|
||||
IntegratedEnergy,
|
||||
}
|
||||
|
||||
struct RtaPeakHold {
|
||||
values: Vec<f32>,
|
||||
expires_at_sample: Vec<u64>,
|
||||
sample_clock: u64,
|
||||
mode: String,
|
||||
hold_samples: u64,
|
||||
reset_token: u64,
|
||||
}
|
||||
|
||||
impl RtaPeakHold {
|
||||
fn new(len: usize) -> Self {
|
||||
Self {
|
||||
values: vec![RTA_PEAK_FLOOR_DB; len],
|
||||
expires_at_sample: vec![0; len],
|
||||
sample_clock: 0,
|
||||
mode: String::new(),
|
||||
hold_samples: 0,
|
||||
reset_token: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
current: &[f32],
|
||||
advanced_samples: usize,
|
||||
sample_rate: u32,
|
||||
config: &PhoenixRtaConfig,
|
||||
) {
|
||||
self.sample_clock = self.sample_clock.saturating_add(advanced_samples as u64);
|
||||
let mode = normalize_rta_peak_hold_mode(&config.rta_peak_hold_mode);
|
||||
let hold_samples = (f64::from(sample_rate.max(1))
|
||||
* f64::from(config.rta_peak_hold_seconds.max(0.0)))
|
||||
.round()
|
||||
.clamp(0.0, u64::MAX as f64) as u64;
|
||||
let reset = self.mode != mode
|
||||
|| self.hold_samples != hold_samples
|
||||
|| self.reset_token != config.rta_peak_reset_token;
|
||||
if self.values.len() != current.len() {
|
||||
self.values.resize(current.len(), RTA_PEAK_FLOOR_DB);
|
||||
self.expires_at_sample.resize(current.len(), 0);
|
||||
}
|
||||
if reset {
|
||||
self.mode = mode.to_string();
|
||||
self.hold_samples = hold_samples;
|
||||
self.reset_token = config.rta_peak_reset_token;
|
||||
self.values.copy_from_slice(current);
|
||||
self.expires_at_sample.fill(0);
|
||||
}
|
||||
|
||||
if mode == "off" {
|
||||
self.values.copy_from_slice(current);
|
||||
self.expires_at_sample.fill(0);
|
||||
return;
|
||||
}
|
||||
|
||||
for (index, &incoming) in current.iter().enumerate() {
|
||||
let incoming = if incoming.is_finite() {
|
||||
incoming.max(RTA_PEAK_FLOOR_DB)
|
||||
} else {
|
||||
RTA_PEAK_FLOOR_DB
|
||||
};
|
||||
if incoming >= self.values[index] {
|
||||
self.values[index] = incoming;
|
||||
if mode == "auto" {
|
||||
self.expires_at_sample[index] = self.sample_clock.saturating_add(hold_samples);
|
||||
}
|
||||
} else if mode == "auto" && self.sample_clock >= self.expires_at_sample[index] {
|
||||
// A timed hold ends with a hard release to the current detector
|
||||
// value. There is deliberately no second, continuous peak decay.
|
||||
self.values[index] = incoming;
|
||||
self.expires_at_sample[index] = self.sample_clock.saturating_add(hold_samples);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -1164,7 +1243,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);
|
||||
finalize_rta_bank(bank, sample_rate, frames, rta_config);
|
||||
ppm_state.last_rta = Some(build_iir_rta_frame(bank, sample_rate, rta_config));
|
||||
}
|
||||
RtaEngineState::Fft(fft) => {
|
||||
@@ -1440,7 +1519,7 @@ fn create_rta_bank(sample_rate: u32, config: &PhoenixRtaConfig) -> RtaBank {
|
||||
peak_windows,
|
||||
energies: vec![0.0; len],
|
||||
levels: vec![-120.0; len],
|
||||
peaks: vec![-120.0; len],
|
||||
peak_hold: RtaPeakHold::new(len),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1478,7 +1557,7 @@ fn create_fft_state(sample_rate: u32, config: &PhoenixRtaConfig) -> FftRtaState
|
||||
power_bins: vec![0.0; fft_size / 2],
|
||||
energies: vec![0.0; len],
|
||||
levels: vec![-120.0; len],
|
||||
peaks: vec![-120.0; len],
|
||||
peak_hold: RtaPeakHold::new(len),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1568,23 +1647,19 @@ 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) {
|
||||
let dt = block_size as f32 / sample_rate as f32;
|
||||
let release_step = RTA_PEAK_DECAY_DB_PER_S * dt;
|
||||
fn finalize_rta_bank(
|
||||
bank: &mut RtaBank,
|
||||
sample_rate: u32,
|
||||
block_size: usize,
|
||||
config: &PhoenixRtaConfig,
|
||||
) {
|
||||
for i in 0..bank.energies.len() {
|
||||
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;
|
||||
|
||||
let prev_peak = bank.peaks[i];
|
||||
if !prev_peak.is_finite() || level_db >= prev_peak {
|
||||
bank.peaks[i] = level_db;
|
||||
} else {
|
||||
bank.peaks[i] = (prev_peak - release_step)
|
||||
.max(level_db)
|
||||
.max(RTA_PEAK_FLOOR_DB);
|
||||
}
|
||||
}
|
||||
bank.peak_hold
|
||||
.update(&bank.levels, block_size, sample_rate, config);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -1607,10 +1682,9 @@ fn finalize_fft_state(
|
||||
&mut state.fft_re_r,
|
||||
&mut state.fft_im_r,
|
||||
&mut state.power_bins,
|
||||
FftPowerScale::IntegratedEnergy,
|
||||
);
|
||||
|
||||
let dt = state.fft_step_samples as f32 / sample_rate as f32;
|
||||
let release_step = RTA_PEAK_DECAY_DB_PER_S * dt;
|
||||
let weighting = normalize_weighting(&config.weighting);
|
||||
for (i, band) in state.mapping.iter().enumerate() {
|
||||
let mut band_power = 0.0f32;
|
||||
@@ -1652,15 +1726,10 @@ fn finalize_fft_state(
|
||||
.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];
|
||||
} else {
|
||||
state.peaks[i] = (prev_peak - release_step)
|
||||
.max(state.levels[i])
|
||||
.max(RTA_PEAK_FLOOR_DB);
|
||||
}
|
||||
}
|
||||
state
|
||||
.peak_hold
|
||||
.update(&state.levels, state.fft_step_samples, sample_rate, config);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1680,6 +1749,7 @@ fn finalize_spectro_state(state: &mut SpectroState, sample_rate: u32) -> bool {
|
||||
&mut state.fft_re_r,
|
||||
&mut state.fft_im_r,
|
||||
&mut state.bins,
|
||||
FftPowerScale::BinAmplitude,
|
||||
);
|
||||
|
||||
let floor = -160.0f32;
|
||||
@@ -1702,7 +1772,7 @@ fn build_iir_rta_frame(bank: &RtaBank, sample_rate: u32, config: &PhoenixRtaConf
|
||||
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(),
|
||||
bands_peak: bank.peak_hold.values.clone(),
|
||||
centers: bank.centers.clone(),
|
||||
freq_min: bank.freq_min,
|
||||
freq_max: bank.freq_max,
|
||||
@@ -1725,7 +1795,7 @@ fn build_fft_rta_frame(
|
||||
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(),
|
||||
bands_peak: state.peak_hold.values.clone(),
|
||||
centers: state.centers.clone(),
|
||||
freq_min: state.freq_min,
|
||||
freq_max: state.freq_max,
|
||||
@@ -1871,6 +1941,14 @@ fn normalize_rta_integration(value: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_rta_peak_hold_mode(value: &str) -> &'static str {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"off" => "off",
|
||||
"manual" => "manual",
|
||||
_ => "auto",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn build_rta_bands(
|
||||
sample_rate: u32,
|
||||
@@ -1962,7 +2040,6 @@ fn build_fft_band_mapping(bands: &[RtaBandDef], nyq: f32, bin_count: usize) -> V
|
||||
.ceil()
|
||||
.min((bin_count.saturating_sub(1)) as f32) as usize;
|
||||
let mut bins = Vec::new();
|
||||
let mut weight_sum = 0.0f32;
|
||||
for index in start..=end {
|
||||
let bin_start = index as f32 * bin_width;
|
||||
let bin_end = bin_start + bin_width;
|
||||
@@ -1970,9 +2047,8 @@ fn build_fft_band_mapping(bands: &[RtaBandDef], nyq: f32, bin_count: usize) -> V
|
||||
if overlap > 0.0 {
|
||||
bins.push(FftBandSeg {
|
||||
index,
|
||||
weight: overlap,
|
||||
weight: (overlap / bin_width).clamp(0.0, 1.0),
|
||||
});
|
||||
weight_sum += overlap;
|
||||
}
|
||||
}
|
||||
if bins.is_empty() {
|
||||
@@ -1983,10 +2059,6 @@ fn build_fft_band_mapping(bands: &[RtaBandDef], nyq: f32, bin_count: usize) -> V
|
||||
index: idx,
|
||||
weight: 1.0,
|
||||
});
|
||||
weight_sum = 1.0;
|
||||
}
|
||||
for seg in &mut bins {
|
||||
seg.weight /= weight_sum.max(1.0e-12);
|
||||
}
|
||||
result.push(FftBandMap {
|
||||
center: band.center,
|
||||
@@ -1998,7 +2070,6 @@ fn build_fft_band_mapping(bands: &[RtaBandDef], nyq: f32, bin_count: usize) -> V
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn compute_fft_power_bins(
|
||||
ring_l: &[f32],
|
||||
ring_r: &[f32],
|
||||
@@ -2008,14 +2079,17 @@ fn compute_fft_power_bins(
|
||||
fft_re_r: &mut [f32],
|
||||
fft_im_r: &mut [f32],
|
||||
out_power_bins: &mut [f32],
|
||||
scale: FftPowerScale,
|
||||
) {
|
||||
let fft_size = fft_re_l.len();
|
||||
let mut window_sum = 0.0f32;
|
||||
let mut window_power_sum = 0.0f32;
|
||||
for i in 0..fft_size {
|
||||
let src_idx = (ring_pos + i) % fft_size;
|
||||
let phase = (2.0 * std::f32::consts::PI * i as f32) / (fft_size.saturating_sub(1) as f32);
|
||||
let win = 0.5 - 0.5 * phase.cos();
|
||||
window_sum += win;
|
||||
window_power_sum += win * win;
|
||||
fft_re_l[i] = ring_l[src_idx] * win;
|
||||
fft_im_l[i] = 0.0;
|
||||
fft_re_r[i] = ring_r[src_idx] * win;
|
||||
@@ -2026,10 +2100,18 @@ fn compute_fft_power_bins(
|
||||
fft_in_place(fft_re_r, fft_im_r);
|
||||
|
||||
let fft_norm = ((window_sum * 0.5).max(1.0)).powi(2);
|
||||
let energy_norm = (fft_size as f32 * window_power_sum).max(1.0);
|
||||
for bin in 0..out_power_bins.len() {
|
||||
let power_l = fft_re_l[bin] * fft_re_l[bin] + fft_im_l[bin] * fft_im_l[bin];
|
||||
let power_r = fft_re_r[bin] * fft_re_r[bin] + fft_im_r[bin] * fft_im_r[bin];
|
||||
out_power_bins[bin] = 0.5 * (power_l + power_r) / fft_norm;
|
||||
let stereo_power = 0.5 * (power_l + power_r);
|
||||
out_power_bins[bin] = match scale {
|
||||
FftPowerScale::BinAmplitude => stereo_power / fft_norm,
|
||||
FftPowerScale::IntegratedEnergy => {
|
||||
let one_sided = if bin == 0 { 1.0 } else { 2.0 };
|
||||
stereo_power * one_sided / energy_norm
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2047,7 +2129,6 @@ fn db_gain(db: f32) -> f32 {
|
||||
10.0f32.powf(db / 20.0)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn fft_in_place(re: &mut [f32], im: &mut [f32]) {
|
||||
let n = re.len();
|
||||
if n == 0 || n != im.len() || !n.is_power_of_two() {
|
||||
@@ -2134,6 +2215,59 @@ fn band_weighting_gain(f_lo: f32, center: f32, f_hi: f32, mode: &str) -> f32 {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn integrated_fft_power(fft_size: usize) -> f32 {
|
||||
let cycles = 64.0f32;
|
||||
let signal: Vec<f32> = (0..fft_size)
|
||||
.map(|index| {
|
||||
(2.0 * std::f32::consts::PI * cycles * index as f32 / fft_size as f32).sin()
|
||||
})
|
||||
.collect();
|
||||
let mut re_l = vec![0.0; fft_size];
|
||||
let mut im_l = vec![0.0; fft_size];
|
||||
let mut re_r = vec![0.0; fft_size];
|
||||
let mut im_r = vec![0.0; fft_size];
|
||||
let mut bins = vec![0.0; fft_size / 2];
|
||||
compute_fft_power_bins(
|
||||
&signal,
|
||||
&signal,
|
||||
0,
|
||||
&mut re_l,
|
||||
&mut im_l,
|
||||
&mut re_r,
|
||||
&mut im_r,
|
||||
&mut bins,
|
||||
FftPowerScale::IntegratedEnergy,
|
||||
);
|
||||
bins.iter().sum()
|
||||
}
|
||||
|
||||
fn fft_tone_bin_amplitude(fft_size: usize) -> f32 {
|
||||
let tone_bin = 64usize;
|
||||
let signal: Vec<f32> = (0..fft_size)
|
||||
.map(|index| {
|
||||
(2.0 * std::f32::consts::PI * tone_bin as f32 * index as f32 / fft_size as f32)
|
||||
.sin()
|
||||
})
|
||||
.collect();
|
||||
let mut re_l = vec![0.0; fft_size];
|
||||
let mut im_l = vec![0.0; fft_size];
|
||||
let mut re_r = vec![0.0; fft_size];
|
||||
let mut im_r = vec![0.0; fft_size];
|
||||
let mut bins = vec![0.0; fft_size / 2];
|
||||
compute_fft_power_bins(
|
||||
&signal,
|
||||
&signal,
|
||||
0,
|
||||
&mut re_l,
|
||||
&mut im_l,
|
||||
&mut re_r,
|
||||
&mut im_r,
|
||||
&mut bins,
|
||||
FftPowerScale::BinAmplitude,
|
||||
);
|
||||
bins[tone_bin]
|
||||
}
|
||||
|
||||
fn run_rms(period: usize) -> f32 {
|
||||
let sample_rate = 48_000;
|
||||
let mut window = create_moving_average_window(sample_rate, 300.0);
|
||||
@@ -2159,4 +2293,58 @@ mod tests {
|
||||
assert!((run_rms(period) - reference).abs() < 1.0e-7);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fft_integrated_energy_compensates_hann_window_and_fft_size() {
|
||||
let expected = 0.5f32;
|
||||
for fft_size in [2048, 4096, 8192, 16384] {
|
||||
let measured = integrated_fft_power(fft_size);
|
||||
assert!(
|
||||
(measured - expected).abs() < 2.0e-4,
|
||||
"FFT {fft_size}: expected {expected}, got {measured}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spectrogram_bin_amplitude_scale_remains_unchanged() {
|
||||
for fft_size in [2048, 4096, 8192] {
|
||||
let measured = fft_tone_bin_amplitude(fft_size);
|
||||
assert!((measured - 1.0).abs() < 2.0e-4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rta_peak_hold_has_timed_manual_off_and_reset_semantics() {
|
||||
let mut config = PhoenixRtaConfig::default();
|
||||
config.rta_peak_hold_seconds = 0.1;
|
||||
let mut hold = RtaPeakHold::new(1);
|
||||
|
||||
hold.update(&[-10.0], 0, 1_000, &config);
|
||||
hold.update(&[-30.0], 99, 1_000, &config);
|
||||
assert_eq!(hold.values[0], -10.0);
|
||||
hold.update(&[-30.0], 1, 1_000, &config);
|
||||
assert_eq!(
|
||||
hold.values[0], -30.0,
|
||||
"timed hold must release directly to current"
|
||||
);
|
||||
|
||||
config.rta_peak_hold_mode = "manual".to_string();
|
||||
hold.update(&[-20.0], 1, 1_000, &config);
|
||||
hold.update(&[-5.0], 1, 1_000, &config);
|
||||
hold.update(&[-40.0], 1_000, 1_000, &config);
|
||||
assert_eq!(hold.values[0], -5.0, "manual hold must not decay");
|
||||
|
||||
config.rta_peak_reset_token += 1;
|
||||
hold.update(&[-40.0], 1, 1_000, &config);
|
||||
assert_eq!(hold.values[0], -40.0, "reset must release manual hold");
|
||||
|
||||
config.rta_peak_hold_mode = "off".to_string();
|
||||
hold.update(&[-12.0], 1, 1_000, &config);
|
||||
hold.update(&[-35.0], 1, 1_000, &config);
|
||||
assert_eq!(
|
||||
hold.values[0], -35.0,
|
||||
"off must always follow current value"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,9 @@ pub struct PhoenixRtaConfig {
|
||||
pub tau_slow: f32,
|
||||
pub integration: String,
|
||||
pub layout: String,
|
||||
pub rta_peak_hold_mode: String,
|
||||
pub rta_peak_hold_seconds: f32,
|
||||
pub rta_peak_reset_token: u64,
|
||||
pub input_offset_db_l: f32,
|
||||
pub input_offset_db_r: f32,
|
||||
pub ppm_din_attack_ms: f32,
|
||||
@@ -100,6 +103,9 @@ impl Default for PhoenixRtaConfig {
|
||||
tau_slow: 1.0,
|
||||
integration: "fast".to_string(),
|
||||
layout: "rtw".to_string(),
|
||||
rta_peak_hold_mode: "auto".to_string(),
|
||||
rta_peak_hold_seconds: 2.5,
|
||||
rta_peak_reset_token: 0,
|
||||
input_offset_db_l: -5.0,
|
||||
input_offset_db_r: -5.0,
|
||||
ppm_din_attack_ms: 10.0,
|
||||
|
||||
@@ -407,6 +407,30 @@ fn normalize_rta_config(mut config: PhoenixRtaConfig) -> PhoenixRtaConfig {
|
||||
// Keep the serialized fields for old clients, but normalize their values.
|
||||
config.tau_fast = 0.125;
|
||||
config.tau_slow = 1.0;
|
||||
config.rta_peak_hold_mode = match config
|
||||
.rta_peak_hold_mode
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
"off" => "off".to_string(),
|
||||
"manual" => "manual".to_string(),
|
||||
_ => "auto".to_string(),
|
||||
};
|
||||
let requested_hold = if config.rta_peak_hold_seconds.is_finite() {
|
||||
config.rta_peak_hold_seconds
|
||||
} else {
|
||||
2.5
|
||||
};
|
||||
config.rta_peak_hold_seconds = if config.layout == "rtw" {
|
||||
if requested_hold >= 3.25 {
|
||||
4.0
|
||||
} else {
|
||||
2.5
|
||||
}
|
||||
} else {
|
||||
requested_hold.clamp(0.0, 30.0)
|
||||
};
|
||||
let offset_l = if config.input_offset_db_l.is_finite() {
|
||||
config.input_offset_db_l
|
||||
} else {
|
||||
@@ -767,6 +791,29 @@ mod tests {
|
||||
assert_eq!(config.integration, "medium");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rta_peak_hold_config_is_normalized_without_decay_parameter() {
|
||||
let rtw = normalize_rta_config(PhoenixRtaConfig {
|
||||
rta_peak_hold_mode: "MANUAL".to_string(),
|
||||
rta_peak_hold_seconds: 3.7,
|
||||
rta_peak_reset_token: 12,
|
||||
layout: "rtw".to_string(),
|
||||
..PhoenixRtaConfig::default()
|
||||
});
|
||||
assert_eq!(rtw.rta_peak_hold_mode, "manual");
|
||||
assert_eq!(rtw.rta_peak_hold_seconds, 4.0);
|
||||
assert_eq!(rtw.rta_peak_reset_token, 12);
|
||||
|
||||
let extension = normalize_rta_config(PhoenixRtaConfig {
|
||||
rta_peak_hold_mode: "invalid".to_string(),
|
||||
rta_peak_hold_seconds: 45.0,
|
||||
layout: "iec".to_string(),
|
||||
..PhoenixRtaConfig::default()
|
||||
});
|
||||
assert_eq!(extension.rta_peak_hold_mode, "auto");
|
||||
assert_eq!(extension.rta_peak_hold_seconds, 30.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_peak_mode_migrates_to_peak_detector_with_fast_response() {
|
||||
let config = normalize_rta_config(PhoenixRtaConfig {
|
||||
|
||||
Reference in New Issue
Block a user