Recover analyzer DSP and realtime pipeline corrections
This commit is contained in:
+224
-50
@@ -2,8 +2,7 @@ use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc,
|
||||
Mutex,
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,7 +15,10 @@ use tokio::{
|
||||
use crate::{
|
||||
audio::{spawn_audio_capture_worker, AudioWorkerDeps},
|
||||
config::PhoenixConfig,
|
||||
model::{InputSource, MeterFrame, PhoenixGlobalConfig, PhoenixGlobalConfigEnvelope, PhoenixRtaConfig, ServiceStatus},
|
||||
model::{
|
||||
InputSource, MeterFrame, PhoenixGlobalConfig, PhoenixGlobalConfigEnvelope,
|
||||
PhoenixRtaConfig, ServiceStatus,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -24,7 +26,8 @@ pub struct AppState {
|
||||
pub config: PhoenixConfig,
|
||||
current_input: Arc<RwLock<InputSource>>,
|
||||
seq: Arc<AtomicU64>,
|
||||
metrics_tx: broadcast::Sender<MeterFrame>,
|
||||
actual_sample_rate: Arc<AtomicU64>,
|
||||
metrics_tx: broadcast::Sender<Arc<MeterFrame>>,
|
||||
restart_token: Arc<AtomicU64>,
|
||||
rta_config: Arc<RwLock<PhoenixRtaConfig>>,
|
||||
global_config: Arc<RwLock<PhoenixGlobalConfig>>,
|
||||
@@ -49,7 +52,6 @@ pub(crate) struct NativeWavRecorder {
|
||||
pub pending_discontinuity: bool,
|
||||
}
|
||||
|
||||
|
||||
const NATIVE_RECORDING_EDGE_FADE_FRAMES: usize = 128;
|
||||
|
||||
fn apply_native_recording_edge_fades(pcm_bytes: &mut [u8], channels: u16) {
|
||||
@@ -62,7 +64,9 @@ fn apply_native_recording_edge_fades(pcm_bytes: &mut [u8], channels: u16) {
|
||||
if total_frames < 2 {
|
||||
return;
|
||||
}
|
||||
let fade_frames = NATIVE_RECORDING_EDGE_FADE_FRAMES.min(total_frames / 2).max(1);
|
||||
let fade_frames = NATIVE_RECORDING_EDGE_FADE_FRAMES
|
||||
.min(total_frames / 2)
|
||||
.max(1);
|
||||
|
||||
for frame_idx in 0..fade_frames {
|
||||
let in_gain = (frame_idx as f32 + 1.0) / fade_frames as f32;
|
||||
@@ -71,12 +75,16 @@ fn apply_native_recording_edge_fades(pcm_bytes: &mut [u8], channels: u16) {
|
||||
for ch_idx in 0..ch {
|
||||
let in_off = frame_idx * frame_size + ch_idx * 2;
|
||||
let in_sample = i16::from_le_bytes([pcm_bytes[in_off], pcm_bytes[in_off + 1]]);
|
||||
let in_scaled = (in_sample as f32 * in_gain).round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
|
||||
let in_scaled = (in_sample as f32 * in_gain)
|
||||
.round()
|
||||
.clamp(i16::MIN as f32, i16::MAX as f32) as i16;
|
||||
pcm_bytes[in_off..in_off + 2].copy_from_slice(&in_scaled.to_le_bytes());
|
||||
|
||||
let out_off = out_frame_idx * frame_size + ch_idx * 2;
|
||||
let out_sample = i16::from_le_bytes([pcm_bytes[out_off], pcm_bytes[out_off + 1]]);
|
||||
let out_scaled = (out_sample as f32 * out_gain).round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
|
||||
let out_scaled = (out_sample as f32 * out_gain)
|
||||
.round()
|
||||
.clamp(i16::MIN as f32, i16::MAX as f32) as i16;
|
||||
pcm_bytes[out_off..out_off + 2].copy_from_slice(&out_scaled.to_le_bytes());
|
||||
}
|
||||
}
|
||||
@@ -84,13 +92,18 @@ fn apply_native_recording_edge_fades(pcm_bytes: &mut [u8], channels: u16) {
|
||||
|
||||
impl AppState {
|
||||
pub fn new(config: PhoenixConfig) -> Self {
|
||||
let (metrics_tx, _) = broadcast::channel(512);
|
||||
// WebSocket consumers always drain to the newest state. A small
|
||||
// channel bounds memory and prevents seconds of stale measurements.
|
||||
let (metrics_tx, _) = broadcast::channel(32);
|
||||
let initial_global_config = load_global_config(&config);
|
||||
let initial_rta_config = apply_global_to_rta(config.default_rta_config(), &initial_global_config);
|
||||
let initial_rta_config =
|
||||
apply_global_to_rta(config.default_rta_config(), &initial_global_config);
|
||||
let configured_sample_rate = config.sample_rate;
|
||||
Self {
|
||||
config,
|
||||
current_input: Arc::new(RwLock::new(InputSource::Line)),
|
||||
seq: Arc::new(AtomicU64::new(0)),
|
||||
actual_sample_rate: Arc::new(AtomicU64::new(configured_sample_rate as u64)),
|
||||
metrics_tx,
|
||||
restart_token: Arc::new(AtomicU64::new(0)),
|
||||
rta_config: Arc::new(RwLock::new(initial_rta_config)),
|
||||
@@ -100,7 +113,7 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe_metrics(&self) -> broadcast::Receiver<MeterFrame> {
|
||||
pub fn subscribe_metrics(&self) -> broadcast::Receiver<Arc<MeterFrame>> {
|
||||
self.metrics_tx.subscribe()
|
||||
}
|
||||
|
||||
@@ -115,7 +128,10 @@ impl AppState {
|
||||
audio_engine: "alsa-direct",
|
||||
metrics_mode: "live",
|
||||
alsa_device: self.config.alsa_device(),
|
||||
sample_rate: self.config.sample_rate,
|
||||
sample_rate: self
|
||||
.actual_sample_rate
|
||||
.load(Ordering::SeqCst)
|
||||
.min(u32::MAX as u64) as u32,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +164,7 @@ impl AppState {
|
||||
lufs_i_norm_enabled: normalized.lufs_i_norm_enabled,
|
||||
ppm_din_loudness_boxes: current.ppm_din_loudness_boxes,
|
||||
ppm_din_loudness_offset_db: current.ppm_din_loudness_offset_db,
|
||||
xy_points: current.xy_points,
|
||||
xy_points: normalized.xy_points,
|
||||
gonio_display_gain_db: current.gonio_display_gain_db,
|
||||
record_output_format: current.record_output_format.clone(),
|
||||
record_mp3_bitrate_kbps: current.record_mp3_bitrate_kbps,
|
||||
@@ -158,6 +174,8 @@ impl AppState {
|
||||
clock_led_color: current.clock_led_color.clone(),
|
||||
header_text_color: current.header_text_color.clone(),
|
||||
rta_bar_base_color: current.rta_bar_base_color.clone(),
|
||||
spectro_gamma: current.spectro_gamma,
|
||||
spectro_scroll_mode: current.spectro_scroll_mode,
|
||||
peak_history_scroll_mode: current.peak_history_scroll_mode,
|
||||
peak_history_fill_enabled: current.peak_history_fill_enabled,
|
||||
peak_history_fill_invert: current.peak_history_fill_invert,
|
||||
@@ -248,6 +266,7 @@ impl AppState {
|
||||
global_config_rev: self.global_config_rev.clone(),
|
||||
native_wav_recorders: self.native_wav_recorders.clone(),
|
||||
seq: self.seq.clone(),
|
||||
actual_sample_rate: self.actual_sample_rate.clone(),
|
||||
metrics_tx: self.metrics_tx.clone(),
|
||||
restart_token: self.restart_token.clone(),
|
||||
});
|
||||
@@ -264,7 +283,10 @@ impl AppState {
|
||||
guard.insert(
|
||||
session_id,
|
||||
NativeWavRecorder {
|
||||
sample_rate: self.config.sample_rate,
|
||||
sample_rate: self
|
||||
.actual_sample_rate
|
||||
.load(Ordering::SeqCst)
|
||||
.min(u32::MAX as u64) as u32,
|
||||
channels: 2,
|
||||
pcm_bytes: Vec::new(),
|
||||
total_frames: 0,
|
||||
@@ -334,34 +356,69 @@ fn normalize_rta_config(mut config: PhoenixRtaConfig) -> PhoenixRtaConfig {
|
||||
};
|
||||
config.integration = match config.integration.trim().to_ascii_lowercase().as_str() {
|
||||
"impulse" => "impulse".to_string(),
|
||||
"medium" => "medium".to_string(),
|
||||
"slow" => "slow".to_string(),
|
||||
"average" => "average".to_string(),
|
||||
"peak" => "peak".to_string(),
|
||||
_ => "fast".to_string(),
|
||||
};
|
||||
config.order = config.order.clamp(2, 8);
|
||||
if config.order % 2 != 0 {
|
||||
config.order = (config.order + 1).min(8);
|
||||
}
|
||||
if config.layout == "rtw" {
|
||||
config.engine = "iir".to_string();
|
||||
config.bpo = "1_3".to_string();
|
||||
config.order = 6;
|
||||
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 };
|
||||
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;
|
||||
}
|
||||
let offset_l = if config.input_offset_db_l.is_finite() { config.input_offset_db_l } else { -5.0 };
|
||||
let offset_r = if config.input_offset_db_r.is_finite() { config.input_offset_db_r } else { -5.0 };
|
||||
let offset_l = if config.input_offset_db_l.is_finite() {
|
||||
config.input_offset_db_l
|
||||
} else {
|
||||
-5.0
|
||||
};
|
||||
let offset_r = if config.input_offset_db_r.is_finite() {
|
||||
config.input_offset_db_r
|
||||
} else {
|
||||
-5.0
|
||||
};
|
||||
config.input_offset_db_l = offset_l.clamp(-60.0, 20.0);
|
||||
config.input_offset_db_r = offset_r.clamp(-60.0, 20.0);
|
||||
let din_attack_ms = if config.ppm_din_attack_ms.is_finite() { config.ppm_din_attack_ms } else { 5.0 };
|
||||
let din_decay = if config.ppm_din_decay_db_per_s.is_finite() { config.ppm_din_decay_db_per_s } else { 20.0 / 1.7 };
|
||||
let ebu_attack_ms = if config.ppm_ebu_attack_ms.is_finite() { config.ppm_ebu_attack_ms } else { 10.0 };
|
||||
let ebu_decay = if config.ppm_ebu_decay_db_per_s.is_finite() { config.ppm_ebu_decay_db_per_s } else { 24.0 / 2.8 };
|
||||
config.ppm_din_attack_ms = din_attack_ms.clamp(0.1, 100.0);
|
||||
config.ppm_din_decay_db_per_s = din_decay.clamp(0.1, 120.0);
|
||||
// Standard profiles are fixed. Keeping the serialized fields preserves
|
||||
// compatibility with older clients without allowing silent mistuning.
|
||||
config.ppm_din_attack_ms = 10.0;
|
||||
config.ppm_din_decay_db_per_s = 20.0 / 1.5;
|
||||
config.ppm_din_fast_attack = !!config.ppm_din_fast_attack;
|
||||
config.ppm_ebu_attack_ms = ebu_attack_ms.clamp(0.1, 100.0);
|
||||
config.ppm_ebu_decay_db_per_s = ebu_decay.clamp(0.1, 120.0);
|
||||
config.ppm_ebu_attack_ms = 10.0;
|
||||
config.ppm_ebu_decay_db_per_s = 24.0 / 2.8;
|
||||
config.lufs_i_window_min = config.lufs_i_window_min.clamp(1, 10);
|
||||
config.lufs_i_norm_enabled = !!config.lufs_i_norm_enabled;
|
||||
config.correlation_response_s =
|
||||
crate::correlation::normalize_response_seconds(config.correlation_response_s);
|
||||
config.xy_points = match config.xy_points {
|
||||
128 | 256 | 512 | 1024 | 2048 => config.xy_points,
|
||||
n if n < 192 => 128,
|
||||
n if n < 384 => 256,
|
||||
n if n < 768 => 512,
|
||||
n if n < 1536 => 1024,
|
||||
_ => 2048,
|
||||
};
|
||||
config
|
||||
}
|
||||
|
||||
@@ -388,7 +445,10 @@ fn normalize_meter_slot(input: String, fallback: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_global_config(mut config: PhoenixGlobalConfig, runtime: &PhoenixConfig) -> PhoenixGlobalConfig {
|
||||
fn normalize_global_config(
|
||||
mut config: PhoenixGlobalConfig,
|
||||
runtime: &PhoenixConfig,
|
||||
) -> PhoenixGlobalConfig {
|
||||
config.fft_size = match config.fft_size {
|
||||
2048 | 4096 | 8192 | 16384 => config.fft_size,
|
||||
n if n < 3072 => 2048,
|
||||
@@ -402,8 +462,16 @@ fn normalize_global_config(mut config: PhoenixGlobalConfig, runtime: &PhoenixCon
|
||||
"1_12" => "1_12".to_string(),
|
||||
_ => "1_6".to_string(),
|
||||
};
|
||||
let offset_l = if config.input_offset_db_l.is_finite() { config.input_offset_db_l } else { -5.0 };
|
||||
let offset_r = if config.input_offset_db_r.is_finite() { config.input_offset_db_r } else { -5.0 };
|
||||
let offset_l = if config.input_offset_db_l.is_finite() {
|
||||
config.input_offset_db_l
|
||||
} else {
|
||||
-5.0
|
||||
};
|
||||
let offset_r = if config.input_offset_db_r.is_finite() {
|
||||
config.input_offset_db_r
|
||||
} else {
|
||||
-5.0
|
||||
};
|
||||
config.input_offset_db_l = offset_l.clamp(-10.0, 10.0);
|
||||
config.input_offset_db_r = offset_r.clamp(-10.0, 10.0);
|
||||
config.mono_input = !!config.mono_input;
|
||||
@@ -415,22 +483,26 @@ fn normalize_global_config(mut config: PhoenixGlobalConfig, runtime: &PhoenixCon
|
||||
};
|
||||
config.lr_fractional_delay_samples = delay.clamp(-1.5, 1.5);
|
||||
config.al_markers_enabled = !!config.al_markers_enabled;
|
||||
let thin = if config.meter_bar_thin.is_finite() { config.meter_bar_thin } else { 0.55 };
|
||||
let thin = if config.meter_bar_thin.is_finite() {
|
||||
config.meter_bar_thin
|
||||
} else {
|
||||
0.55
|
||||
};
|
||||
config.meter_bar_thin = thin.clamp(0.35, 0.9);
|
||||
config.panel_dividers_enabled = !!config.panel_dividers_enabled;
|
||||
let din_attack_ms = if config.ppm_din_attack_ms.is_finite() { config.ppm_din_attack_ms } else { 5.0 };
|
||||
let din_decay = if config.ppm_din_decay_db_per_s.is_finite() { config.ppm_din_decay_db_per_s } else { 20.0 / 1.7 };
|
||||
let ebu_attack_ms = if config.ppm_ebu_attack_ms.is_finite() { config.ppm_ebu_attack_ms } else { 10.0 };
|
||||
let ebu_decay = if config.ppm_ebu_decay_db_per_s.is_finite() { config.ppm_ebu_decay_db_per_s } else { 24.0 / 2.8 };
|
||||
config.ppm_din_attack_ms = din_attack_ms.clamp(0.1, 100.0);
|
||||
config.ppm_din_decay_db_per_s = din_decay.clamp(0.1, 120.0);
|
||||
config.ppm_din_attack_ms = 10.0;
|
||||
config.ppm_din_decay_db_per_s = 20.0 / 1.5;
|
||||
config.ppm_din_fast_attack = !!config.ppm_din_fast_attack;
|
||||
config.ppm_ebu_attack_ms = ebu_attack_ms.clamp(0.1, 100.0);
|
||||
config.ppm_ebu_decay_db_per_s = ebu_decay.clamp(0.1, 120.0);
|
||||
config.ppm_ebu_attack_ms = 10.0;
|
||||
config.ppm_ebu_decay_db_per_s = 24.0 / 2.8;
|
||||
config.lufs_i_window_min = config.lufs_i_window_min.clamp(1, 10);
|
||||
config.lufs_i_norm_enabled = !!config.lufs_i_norm_enabled;
|
||||
config.ppm_din_loudness_boxes = !!config.ppm_din_loudness_boxes;
|
||||
let loudness_offset = if config.ppm_din_loudness_offset_db.is_finite() { config.ppm_din_loudness_offset_db } else { 0.0 };
|
||||
let loudness_offset = if config.ppm_din_loudness_offset_db.is_finite() {
|
||||
config.ppm_din_loudness_offset_db
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
config.ppm_din_loudness_offset_db = loudness_offset.clamp(-7.0, 7.0);
|
||||
config.xy_points = match config.xy_points {
|
||||
128 | 256 | 512 | 1024 | 2048 => config.xy_points,
|
||||
@@ -440,9 +512,18 @@ fn normalize_global_config(mut config: PhoenixGlobalConfig, runtime: &PhoenixCon
|
||||
n if n < 1536 => 1024,
|
||||
_ => 2048,
|
||||
};
|
||||
let gonio_gain = if config.gonio_display_gain_db.is_finite() { config.gonio_display_gain_db } else { -5.0 };
|
||||
let gonio_gain = if config.gonio_display_gain_db.is_finite() {
|
||||
config.gonio_display_gain_db
|
||||
} else {
|
||||
-5.0
|
||||
};
|
||||
config.gonio_display_gain_db = gonio_gain.clamp(-35.0, 35.0);
|
||||
config.record_output_format = match config.record_output_format.trim().to_ascii_lowercase().as_str() {
|
||||
config.record_output_format = match config
|
||||
.record_output_format
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
"mp3" => "mp3".to_string(),
|
||||
"webm" => "webm".to_string(),
|
||||
_ => "wav".to_string(),
|
||||
@@ -459,13 +540,39 @@ fn normalize_global_config(mut config: PhoenixGlobalConfig, runtime: &PhoenixCon
|
||||
"volumio" => "volumio".to_string(),
|
||||
_ => "analyzer".to_string(),
|
||||
};
|
||||
let auto_gap = if config.record_auto_split_gap_sec.is_finite() { config.record_auto_split_gap_sec } else { 1.5 };
|
||||
let auto_gap = if config.record_auto_split_gap_sec.is_finite() {
|
||||
config.record_auto_split_gap_sec
|
||||
} else {
|
||||
1.5
|
||||
};
|
||||
config.record_auto_split_gap_sec = (auto_gap.clamp(0.5, 3.0) * 2.0).round() / 2.0;
|
||||
let auto_threshold = if config.record_auto_threshold_dbfs.is_finite() { config.record_auto_threshold_dbfs } else { -50.0 };
|
||||
let auto_threshold = if config.record_auto_threshold_dbfs.is_finite() {
|
||||
config.record_auto_threshold_dbfs
|
||||
} else {
|
||||
-50.0
|
||||
};
|
||||
config.record_auto_threshold_dbfs = auto_threshold.clamp(-120.0, 20.0);
|
||||
config.clock_led_color = normalize_ui_color(config.clock_led_color, "#ff0000");
|
||||
config.header_text_color = normalize_ui_color(config.header_text_color, "#ffe066");
|
||||
config.rta_bar_base_color = normalize_ui_color(config.rta_bar_base_color, "#ffe066");
|
||||
config.spectro_gamma = if config.spectro_gamma.is_finite() {
|
||||
config.spectro_gamma.clamp(0.3, 1.2)
|
||||
} else {
|
||||
0.9
|
||||
};
|
||||
config.spectro_scroll_mode = match config.spectro_scroll_mode {
|
||||
x if !x.is_finite() => 1.0,
|
||||
x if (x - 0.5).abs() < f32::EPSILON => 0.5,
|
||||
x if (x - 1.0).abs() < f32::EPSILON => 1.0,
|
||||
x if (x - 2.0).abs() < f32::EPSILON => 2.0,
|
||||
x if (x - 4.0).abs() < f32::EPSILON => 4.0,
|
||||
x if (x - 6.0).abs() < f32::EPSILON => 6.0,
|
||||
x if x < 0.75 => 0.5,
|
||||
x if x < 1.5 => 1.0,
|
||||
x if x < 3.0 => 2.0,
|
||||
x if x < 5.0 => 4.0,
|
||||
_ => 6.0,
|
||||
};
|
||||
config.peak_history_scroll_mode = match config.peak_history_scroll_mode {
|
||||
x if !x.is_finite() => 1.0,
|
||||
x if (x - 0.5).abs() < f32::EPSILON => 0.5,
|
||||
@@ -482,9 +589,18 @@ fn normalize_global_config(mut config: PhoenixGlobalConfig, runtime: &PhoenixCon
|
||||
config.peak_history_fill_enabled = !!config.peak_history_fill_enabled;
|
||||
config.peak_history_fill_invert = !!config.peak_history_fill_invert;
|
||||
config.peak_history_slot = normalize_meter_slot(config.peak_history_slot, "ppm-din");
|
||||
let phase_gain = if config.phase_display_gain_db.is_finite() { config.phase_display_gain_db } else { 0.0 };
|
||||
let phase_gain = if config.phase_display_gain_db.is_finite() {
|
||||
config.phase_display_gain_db
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
config.phase_display_gain_db = (phase_gain.clamp(-35.0, 35.0) / 5.0).round() * 5.0;
|
||||
config.phase_amplitude_mode = match config.phase_amplitude_mode.trim().to_ascii_lowercase().as_str() {
|
||||
config.phase_amplitude_mode = match config
|
||||
.phase_amplitude_mode
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
"ppm-din" => "ppm-din".to_string(),
|
||||
_ => "bandpass".to_string(),
|
||||
};
|
||||
@@ -513,9 +629,17 @@ fn normalize_global_config(mut config: PhoenixGlobalConfig, runtime: &PhoenixCon
|
||||
config.lufs_color_s = normalize_ui_color(config.lufs_color_s, "#ff3b3b");
|
||||
config.lufs_scale_label_color = normalize_ui_color(config.lufs_scale_label_color, "#8fd3d4");
|
||||
config.screensaver_enabled = !!config.screensaver_enabled;
|
||||
let idle = if config.screensaver_idle_min.is_finite() { config.screensaver_idle_min } else { 5.0 };
|
||||
let idle = if config.screensaver_idle_min.is_finite() {
|
||||
config.screensaver_idle_min
|
||||
} else {
|
||||
5.0
|
||||
};
|
||||
config.screensaver_idle_min = idle.clamp(0.0, 120.0);
|
||||
let activity = if config.screensaver_activity_db.is_finite() { config.screensaver_activity_db } else { -50.0 };
|
||||
let activity = if config.screensaver_activity_db.is_finite() {
|
||||
config.screensaver_activity_db
|
||||
} else {
|
||||
-50.0
|
||||
};
|
||||
config.screensaver_activity_db = activity.clamp(-120.0, 0.0);
|
||||
config.screensaver_mode = match config.screensaver_mode.trim().to_ascii_lowercase().as_str() {
|
||||
"starfield" => "starfield".to_string(),
|
||||
@@ -525,11 +649,18 @@ fn normalize_global_config(mut config: PhoenixGlobalConfig, runtime: &PhoenixCon
|
||||
};
|
||||
config.screensaver_led_glow = !!config.screensaver_led_glow;
|
||||
let color = config.screensaver_led_color.trim();
|
||||
config.screensaver_led_color = if color.is_empty() { "#ff0000".to_string() } else { color.to_string() };
|
||||
config.screensaver_led_color = if color.is_empty() {
|
||||
"#ff0000".to_string()
|
||||
} else {
|
||||
color.to_string()
|
||||
};
|
||||
config
|
||||
}
|
||||
|
||||
fn apply_global_to_rta(mut config: PhoenixRtaConfig, global: &PhoenixGlobalConfig) -> PhoenixRtaConfig {
|
||||
fn apply_global_to_rta(
|
||||
mut config: PhoenixRtaConfig,
|
||||
global: &PhoenixGlobalConfig,
|
||||
) -> PhoenixRtaConfig {
|
||||
config.fft_size = global.fft_size;
|
||||
config.bpo = global.rta_bpo_mode.clone();
|
||||
config.input_offset_db_l = global.input_offset_db_l;
|
||||
@@ -544,6 +675,7 @@ fn apply_global_to_rta(mut config: PhoenixRtaConfig, global: &PhoenixGlobalConfi
|
||||
config.ppm_ebu_decay_db_per_s = global.ppm_ebu_decay_db_per_s;
|
||||
config.lufs_i_window_min = global.lufs_i_window_min;
|
||||
config.lufs_i_norm_enabled = global.lufs_i_norm_enabled;
|
||||
config.xy_points = global.xy_points;
|
||||
normalize_rta_config(config)
|
||||
}
|
||||
|
||||
@@ -558,7 +690,10 @@ fn load_global_config(runtime: &PhoenixConfig) -> PhoenixGlobalConfig {
|
||||
normalize_global_config(parsed, runtime)
|
||||
}
|
||||
|
||||
async fn persist_global_config(runtime: &PhoenixConfig, config: &PhoenixGlobalConfig) -> anyhow::Result<()> {
|
||||
async fn persist_global_config(
|
||||
runtime: &PhoenixConfig,
|
||||
config: &PhoenixGlobalConfig,
|
||||
) -> anyhow::Result<()> {
|
||||
let path = runtime.global_config_path.clone();
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
@@ -571,3 +706,42 @@ async fn persist_global_config(runtime: &PhoenixConfig, config: &PhoenixGlobalCo
|
||||
.with_context(|| format!("failed to write Phoenix config {}", path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rtw_profile_is_always_iir_third_octave() {
|
||||
let config = normalize_rta_config(PhoenixRtaConfig {
|
||||
engine: "fft".to_string(),
|
||||
bpo: "1_12".to_string(),
|
||||
order: 2,
|
||||
freq_range: "lf".to_string(),
|
||||
layout: "rtw".to_string(),
|
||||
..PhoenixRtaConfig::default()
|
||||
});
|
||||
assert_eq!(config.engine, "iir");
|
||||
assert_eq!(config.bpo, "1_3");
|
||||
assert_eq!(config.order, 6);
|
||||
assert_eq!(config.freq_range, "norm");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phoenix_profile_retains_explicit_extensions() {
|
||||
let config = normalize_rta_config(PhoenixRtaConfig {
|
||||
engine: "fft".to_string(),
|
||||
bpo: "1_12".to_string(),
|
||||
order: 8,
|
||||
freq_range: "lf".to_string(),
|
||||
layout: "iec".to_string(),
|
||||
integration: "medium".to_string(),
|
||||
..PhoenixRtaConfig::default()
|
||||
});
|
||||
assert_eq!(config.engine, "fft");
|
||||
assert_eq!(config.bpo, "1_12");
|
||||
assert_eq!(config.order, 8);
|
||||
assert_eq!(config.freq_range, "lf");
|
||||
assert_eq!(config.integration, "medium");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user