use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use crate::MeasurementSource;
pub fn default_config_version() -> String {
"1.3.0".to_string()
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct RecordingConfiguration {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub playback_device_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub playback_device_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub playback_sample_rate: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub playback_channels: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speaker_configuration: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel_names: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recording_device_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recording_device_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recording_sample_rate: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recording_channels: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mic_calibration_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mic_calibration_paths: Option<Vec<Option<String>>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recording_directory: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signal_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signal_duration_secs: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signal_level_db: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sweep_start_freq: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sweep_end_freq: Option<f32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ProcessingMode {
#[default]
LowLatency,
PhaseLinear,
Hybrid,
MixedPhase,
WarpedIir,
KautzModal,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum SubwooferStrategy {
#[default]
Single,
Mso,
Dba,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
#[serde(rename_all = "snake_case")]
pub enum SystemModel {
Stereo,
HomeCinema,
#[default]
Custom,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum TiltType {
#[default]
Flat,
Harman,
Custom,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum TargetShape {
#[default]
Flat,
Harman,
Custom,
File,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum HighpassType {
#[default]
LinkwitzRiley,
Butterworth,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum MultiSeatStrategy {
#[default]
MinimizeVariance,
PrimaryWithConstraints,
Average,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum MultiMeasurementStrategy {
#[default]
Average,
WeightedSum,
Minimax,
VariancePenalized,
SpatialRobustness,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Cea2034CorrectionMode {
Flat,
Score,
#[default]
Auto,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SubwooferSystemConfig {
#[serde(default)]
pub config: SubwooferStrategy,
#[serde(skip_serializing_if = "Option::is_none")]
pub crossover: Option<String>,
#[serde(flatten)]
pub mapping: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SystemConfig {
#[serde(default)]
pub model: SystemModel,
pub speakers: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subwoofers: Option<SubwooferSystemConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum SpeakerConfig {
Group(SpeakerGroup),
MultiSub(MultiSubGroup),
Dba(DBAConfig),
Cardioid(Box<CardioidConfig>),
Single(MeasurementSource),
}
impl SpeakerConfig {
pub fn speaker_name(&self) -> Option<&str> {
match self {
SpeakerConfig::Single(source) => source.speaker_name(),
SpeakerConfig::Group(group) => group.speaker_name.as_deref(),
SpeakerConfig::MultiSub(ms) => ms.speaker_name.as_deref(),
SpeakerConfig::Dba(dba) => dba.speaker_name.as_deref(),
SpeakerConfig::Cardioid(c) => c.speaker_name.as_deref(),
}
}
pub fn resolve_paths(&mut self, base_dir: &std::path::Path) {
match self {
SpeakerConfig::Single(source) => source.resolve_paths(base_dir),
SpeakerConfig::Group(group) => group.resolve_paths(base_dir),
SpeakerConfig::MultiSub(group) => group.resolve_paths(base_dir),
SpeakerConfig::Dba(config) => config.resolve_paths(base_dir),
SpeakerConfig::Cardioid(config) => config.resolve_paths(base_dir),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SpeakerGroup {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speaker_name: Option<String>,
pub measurements: Vec<MeasurementSource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub crossover: Option<String>,
}
impl SpeakerGroup {
pub fn resolve_paths(&mut self, base_dir: &std::path::Path) {
for m in &mut self.measurements {
m.resolve_paths(base_dir);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MultiSubGroup {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speaker_name: Option<String>,
pub subwoofers: Vec<MeasurementSource>,
#[serde(default)]
pub allpass_optimization: bool,
}
impl MultiSubGroup {
pub fn resolve_paths(&mut self, base_dir: &std::path::Path) {
for m in &mut self.subwoofers {
m.resolve_paths(base_dir);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CardioidConfig {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speaker_name: Option<String>,
pub front: MeasurementSource,
pub rear: MeasurementSource,
pub separation_meters: f64,
}
impl CardioidConfig {
pub fn resolve_paths(&mut self, base_dir: &std::path::Path) {
self.front.resolve_paths(base_dir);
self.rear.resolve_paths(base_dir);
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DBAConfig {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speaker_name: Option<String>,
pub front: Vec<MeasurementSource>,
pub rear: Vec<MeasurementSource>,
}
impl DBAConfig {
pub fn resolve_paths(&mut self, base_dir: &std::path::Path) {
for m in &mut self.front {
m.resolve_paths(base_dir);
}
for m in &mut self.rear {
m.resolve_paths(base_dir);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct CrossoverConfig {
#[serde(rename = "type")]
pub crossover_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequencies: Option<Vec<f64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency_range: Option<(f64, f64)>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum TargetCurveConfig {
Predefined(String),
Path(PathBuf),
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TargetTiltConfig {
#[serde(default)]
pub tilt_type: TiltType,
#[serde(default = "default_tilt_slope")]
pub slope_db_per_octave: f64,
#[serde(default = "default_tilt_reference_freq")]
pub reference_freq: f64,
#[serde(default)]
pub bass_shelf_db: f64,
#[serde(default = "default_bass_shelf_freq")]
pub bass_shelf_freq: f64,
}
fn default_tilt_slope() -> f64 {
-0.8
}
fn default_tilt_reference_freq() -> f64 {
1000.0
}
fn default_bass_shelf_freq() -> f64 {
200.0
}
impl Default for TargetTiltConfig {
fn default() -> Self {
Self {
tilt_type: TiltType::Flat,
slope_db_per_octave: 0.0,
reference_freq: default_tilt_reference_freq(),
bass_shelf_db: 0.0,
bass_shelf_freq: default_bass_shelf_freq(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct UserPreference {
#[serde(default)]
pub bass_shelf_db: f64,
#[serde(default = "default_bass_shelf_freq")]
pub bass_shelf_freq: f64,
#[serde(default)]
pub treble_shelf_db: f64,
#[serde(default = "default_treble_shelf_freq")]
pub treble_shelf_freq: f64,
}
fn default_treble_shelf_freq() -> f64 {
8000.0
}
impl Default for UserPreference {
fn default() -> Self {
Self {
bass_shelf_db: 0.0,
bass_shelf_freq: default_bass_shelf_freq(),
treble_shelf_db: 0.0,
treble_shelf_freq: default_treble_shelf_freq(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TargetResponseConfig {
#[serde(default)]
pub shape: TargetShape,
#[serde(default = "default_tilt_slope")]
pub slope_db_per_octave: f64,
#[serde(default = "default_tilt_reference_freq")]
pub reference_freq: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub curve_path: Option<std::path::PathBuf>,
#[serde(default)]
pub preference: UserPreference,
#[serde(default)]
pub broadband_precorrection: bool,
}
impl Default for TargetResponseConfig {
fn default() -> Self {
Self {
shape: TargetShape::Flat,
slope_db_per_octave: 0.0,
reference_freq: default_tilt_reference_freq(),
curve_path: None,
preference: UserPreference::default(),
broadband_precorrection: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct FirConfig {
#[serde(default = "default_fir_taps")]
pub taps: usize,
#[serde(default = "default_fir_phase")]
pub phase: String,
#[serde(default)]
pub correct_excess_phase: bool,
#[serde(default = "default_phase_smoothing")]
pub phase_smoothing: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pre_ringing: Option<PreRingingSerdeConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PreRingingSerdeConfig {
#[serde(default = "default_pre_ringing_threshold")]
pub threshold_db: f64,
#[serde(default = "default_pre_ringing_time")]
pub max_time_s: f64,
}
fn default_pre_ringing_threshold() -> f64 {
-30.0
}
fn default_pre_ringing_time() -> f64 {
0.005
}
fn default_fir_taps() -> usize {
4096
}
fn default_fir_phase() -> String {
"kirkeby".to_string()
}
fn default_phase_smoothing() -> f64 {
0.167
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MixedPhaseSerdeConfig {
#[serde(default = "default_mixed_phase_fir_length")]
pub max_fir_length_ms: f64,
#[serde(default = "default_pre_ringing_threshold")]
pub pre_ringing_threshold_db: f64,
#[serde(default = "default_mixed_phase_spatial_depth")]
pub min_spatial_depth: f64,
#[serde(default = "default_mask_smoothing")]
pub phase_smoothing_octaves: f64,
}
fn default_mixed_phase_fir_length() -> f64 {
10.0
}
fn default_mixed_phase_spatial_depth() -> f64 {
0.5
}
fn default_mask_smoothing() -> f64 {
1.0 / 6.0
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MixedModeConfig {
#[serde(default = "default_crossover_freq")]
pub crossover_freq: f64,
#[serde(default = "default_crossover_type")]
pub crossover_type: String,
#[serde(default = "default_fir_band")]
pub fir_band: String,
}
fn default_crossover_freq() -> f64 {
300.0
}
fn default_crossover_type() -> String {
"LR24".to_string()
}
fn default_fir_band() -> String {
"low".to_string()
}
impl Default for MixedModeConfig {
fn default() -> Self {
Self {
crossover_freq: default_crossover_freq(),
crossover_type: default_crossover_type(),
fir_band: default_fir_band(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ExcursionProtectionConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_true")]
pub auto_detect_f3: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub manual_f3_hz: Option<f64>,
#[serde(default = "default_filter_order")]
pub filter_order: usize,
#[serde(default)]
pub filter_type: HighpassType,
#[serde(default = "default_margin_octaves")]
pub margin_octaves: f64,
}
fn default_true() -> bool {
true
}
fn default_filter_order() -> usize {
4
}
fn default_margin_octaves() -> f64 {
0.25
}
impl Default for ExcursionProtectionConfig {
fn default() -> Self {
Self {
enabled: false,
auto_detect_f3: true,
manual_f3_hz: None,
filter_order: default_filter_order(),
filter_type: HighpassType::LinkwitzRiley,
margin_octaves: default_margin_octaves(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct LowFreqFilterConfig {
#[serde(default = "default_low_freq_max_q")]
pub max_q: f64,
#[serde(default = "default_min_q")]
pub min_q: f64,
#[serde(default)]
pub allow_boost: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_db: Option<f64>,
}
fn default_low_freq_max_q() -> f64 {
5.0
}
impl Default for LowFreqFilterConfig {
fn default() -> Self {
Self {
max_q: default_low_freq_max_q(),
min_q: default_min_q(),
allow_boost: false,
max_db: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct HighFreqFilterConfig {
#[serde(default = "default_high_freq_max_q")]
pub max_q: f64,
#[serde(default)]
pub shelving_only: bool,
}
fn default_high_freq_max_q() -> f64 {
1.0
}
impl Default for HighFreqFilterConfig {
fn default() -> Self {
Self {
max_q: default_high_freq_max_q(),
shelving_only: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RoomDimensions {
pub length: f64,
pub width: f64,
pub height: f64,
}
impl RoomDimensions {
pub fn schroeder_frequency(&self) -> f64 {
let volume = self.length * self.width * self.height;
11885.0 / volume.sqrt()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SchroederSplitConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_schroeder_freq")]
pub schroeder_freq: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub room_dimensions: Option<RoomDimensions>,
#[serde(default)]
pub low_freq_config: LowFreqFilterConfig,
#[serde(default)]
pub high_freq_config: HighFreqFilterConfig,
}
fn default_schroeder_freq() -> f64 {
300.0
}
impl Default for SchroederSplitConfig {
fn default() -> Self {
Self {
enabled: false,
schroeder_freq: default_schroeder_freq(),
room_dimensions: None,
low_freq_config: LowFreqFilterConfig::default(),
high_freq_config: HighFreqFilterConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PhaseAlignmentConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_phase_min_freq")]
pub min_freq: f64,
#[serde(default = "default_phase_max_freq")]
pub max_freq: f64,
#[serde(default = "default_true")]
pub optimize_polarity: bool,
#[serde(default = "default_max_delay_ms")]
pub max_delay_ms: f64,
}
fn default_phase_min_freq() -> f64 {
60.0
}
fn default_phase_max_freq() -> f64 {
100.0
}
fn default_max_delay_ms() -> f64 {
3.0
}
impl Default for PhaseAlignmentConfig {
fn default() -> Self {
Self {
enabled: true,
min_freq: default_phase_min_freq(),
max_freq: default_phase_max_freq(),
optimize_polarity: true,
max_delay_ms: default_max_delay_ms(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MultiSeatMeasurement {
pub name: String,
pub seat_measurements: Vec<MeasurementSource>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MultiSeatConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub strategy: MultiSeatStrategy,
#[serde(default)]
pub primary_seat: usize,
#[serde(default = "default_max_deviation_db")]
pub max_deviation_db: f64,
}
fn default_max_deviation_db() -> f64 {
6.0
}
impl Default for MultiSeatConfig {
fn default() -> Self {
Self {
enabled: false,
strategy: MultiSeatStrategy::MinimizeVariance,
primary_seat: 0,
max_deviation_db: default_max_deviation_db(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ChannelMatchingConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_channel_matching_threshold")]
pub threshold_db: f64,
#[serde(default = "default_channel_matching_max_filters")]
pub max_filters: usize,
}
fn default_channel_matching_threshold() -> f64 {
0.75
}
fn default_channel_matching_max_filters() -> usize {
5
}
impl Default for ChannelMatchingConfig {
fn default() -> Self {
Self {
enabled: true,
threshold_db: default_channel_matching_threshold(),
max_filters: default_channel_matching_max_filters(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SubOptimizerConfig {
#[serde(default = "default_sub_num_filters")]
pub num_filters: usize,
#[serde(default = "default_sub_max_db")]
pub max_db: f64,
#[serde(default = "default_sub_min_db")]
pub min_db: f64,
#[serde(default = "default_min_q")]
pub min_q: f64,
#[serde(default = "default_sub_max_q")]
pub max_q: f64,
}
fn default_sub_num_filters() -> usize {
10
}
fn default_sub_max_db() -> f64 {
18.0
}
fn default_sub_min_db() -> f64 {
-18.0
}
fn default_sub_max_q() -> f64 {
10.0
}
impl Default for SubOptimizerConfig {
fn default() -> Self {
Self {
num_filters: default_sub_num_filters(),
max_db: default_sub_max_db(),
min_db: default_sub_min_db(),
min_q: default_min_q(),
max_q: default_sub_max_q(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct InterChannelDeviation {
pub deviation_per_freq: Vec<(f64, f64)>,
pub midrange_rms_db: f64,
pub passband_rms_db: f64,
pub midrange_peak_db: f64,
pub midrange_peak_freq: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct BroadbandTargetMatchingConfig {
#[serde(default = "default_true")]
pub enabled: bool,
}
impl Default for BroadbandTargetMatchingConfig {
fn default() -> Self {
Self { enabled: true }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SpatialRobustnessSerdeConfig {
#[serde(default = "default_variance_threshold")]
pub variance_threshold_db: f64,
#[serde(default = "default_transition_width")]
pub transition_width_db: f64,
#[serde(default = "default_min_correction_depth")]
pub min_correction_depth: f64,
#[serde(default = "default_mask_smoothing_octaves")]
pub mask_smoothing_octaves: f64,
}
fn default_variance_threshold() -> f64 {
3.0
}
fn default_transition_width() -> f64 {
2.0
}
fn default_min_correction_depth() -> f64 {
0.1
}
fn default_mask_smoothing_octaves() -> f64 {
1.0 / 6.0
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MultiMeasurementConfig {
#[serde(default)]
pub strategy: MultiMeasurementStrategy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub weights: Option<Vec<f64>>,
#[serde(default = "default_variance_lambda")]
pub variance_lambda: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spatial_robustness: Option<SpatialRobustnessSerdeConfig>,
}
fn default_variance_lambda() -> f64 {
1.0
}
impl Default for MultiMeasurementConfig {
fn default() -> Self {
Self {
strategy: MultiMeasurementStrategy::default(),
weights: None,
variance_lambda: default_variance_lambda(),
spatial_robustness: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct DecomposedCorrectionSerdeConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_decomposed_schroeder")]
pub schroeder_freq: f64,
#[serde(default = "default_decomposed_min_q")]
pub min_mode_q: f64,
#[serde(default = "default_decomposed_prominence")]
pub min_mode_prominence_db: f64,
#[serde(default = "default_decomposed_mode_weight")]
pub mode_correction_weight: f64,
#[serde(default = "default_decomposed_reflection_weight")]
pub early_reflection_weight: f64,
#[serde(default = "default_decomposed_steady_weight")]
pub steady_state_weight: f64,
}
fn default_decomposed_schroeder() -> f64 {
250.0
}
fn default_decomposed_min_q() -> f64 {
3.0
}
fn default_decomposed_prominence() -> f64 {
3.0
}
fn default_decomposed_mode_weight() -> f64 {
1.0
}
fn default_decomposed_reflection_weight() -> f64 {
0.3
}
fn default_decomposed_steady_weight() -> f64 {
0.4
}
impl Default for DecomposedCorrectionSerdeConfig {
fn default() -> Self {
Self {
enabled: true,
schroeder_freq: default_decomposed_schroeder(),
min_mode_q: default_decomposed_min_q(),
min_mode_prominence_db: default_decomposed_prominence(),
mode_correction_weight: default_decomposed_mode_weight(),
early_reflection_weight: default_decomposed_reflection_weight(),
steady_state_weight: default_decomposed_steady_weight(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Cea2034CorrectionConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speaker_name: Option<String>,
#[serde(default = "default_cea2034_version")]
pub version: String,
#[serde(default)]
pub correction_mode: Cea2034CorrectionMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub listening_distance_m: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_latency_ms: Option<f64>,
#[serde(default = "default_nearfield_threshold")]
pub nearfield_threshold_m: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_freq: Option<f64>,
#[serde(default = "default_cea2034_num_filters")]
pub num_filters: usize,
#[serde(default = "default_cea2034_max_q")]
pub max_q: f64,
#[serde(default = "default_cea2034_max_db")]
pub max_db: f64,
#[serde(default = "default_cea2034_min_db")]
pub min_db: f64,
}
fn default_cea2034_version() -> String {
"asr".to_string()
}
fn default_nearfield_threshold() -> f64 {
2.0
}
fn default_cea2034_num_filters() -> usize {
5
}
fn default_cea2034_max_q() -> f64 {
3.0
}
fn default_cea2034_max_db() -> f64 {
3.0
}
fn default_cea2034_min_db() -> f64 {
-12.0
}
impl Default for Cea2034CorrectionConfig {
fn default() -> Self {
Self {
enabled: false,
speaker_name: None,
version: default_cea2034_version(),
correction_mode: Cea2034CorrectionMode::default(),
listening_distance_m: None,
system_latency_ms: None,
nearfield_threshold_m: default_nearfield_threshold(),
min_freq: None,
num_filters: default_cea2034_num_filters(),
max_q: default_cea2034_max_q(),
max_db: default_cea2034_max_db(),
min_db: default_cea2034_min_db(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct VoiceOfGodConfig {
#[serde(default)]
pub enabled: bool,
pub reference_channel: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GroupDelayOptimizationConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub target_ms: f64,
}
impl Default for GroupDelayOptimizationConfig {
fn default() -> Self {
Self {
enabled: false,
target_ms: 0.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct OptimizerConfig {
#[serde(default = "default_opt_mode")]
pub mode: String,
#[serde(default)]
pub processing_mode: ProcessingMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub fir: Option<FirConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mixed_config: Option<MixedModeConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mixed_phase: Option<MixedPhaseSerdeConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phase_correction: Option<MixedPhaseSerdeConfig>,
#[serde(default = "default_loss_type")]
pub loss_type: String,
#[serde(default = "default_algorithm")]
pub algorithm: String,
#[serde(default = "default_strategy")]
pub strategy: String,
#[serde(default = "default_num_filters")]
pub num_filters: usize,
#[serde(default = "default_min_filter_improvement")]
pub min_filter_improvement: f64,
#[serde(default = "default_elimination_threshold")]
pub elimination_threshold: f64,
#[serde(default = "default_min_q")]
pub min_q: f64,
#[serde(default = "default_max_q")]
pub max_q: f64,
#[serde(default = "default_min_db")]
pub min_db: f64,
#[serde(default = "default_max_db")]
pub max_db: f64,
#[serde(default = "default_min_freq")]
pub min_freq: f64,
#[serde(default = "default_max_freq")]
pub max_freq: f64,
#[serde(default = "default_max_iter")]
pub max_iter: usize,
#[serde(default = "default_population")]
pub population: usize,
#[serde(default = "default_peq_model")]
pub peq_model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seed: Option<u64>,
#[serde(default = "default_refine")]
pub refine: bool,
#[serde(default = "default_local_algo")]
pub local_algo: String,
#[serde(default = "default_psychoacoustic")]
pub psychoacoustic: bool,
#[serde(default = "default_smooth_n")]
pub smooth_n: usize,
#[serde(default = "default_asymmetric_loss")]
pub asymmetric_loss: bool,
#[serde(default = "default_tolerance")]
pub tolerance: f64,
#[serde(default = "default_atolerance")]
pub atolerance: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_delay: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_response: Option<TargetResponseConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_tilt: Option<TargetTiltConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub excursion_protection: Option<ExcursionProtectionConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schroeder_split: Option<SchroederSplitConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phase_alignment: Option<PhaseAlignmentConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub multi_seat: Option<MultiSeatConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gd_opt: Option<GroupDelayOptimizationConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vog: Option<VoiceOfGodConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub broadband_target_matching: Option<BroadbandTargetMatchingConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub multi_measurement: Option<MultiMeasurementConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub decomposed_correction: Option<DecomposedCorrectionSerdeConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cea2034_correction: Option<Cea2034CorrectionConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sub_config: Option<SubOptimizerConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel_matching: Option<ChannelMatchingConfig>,
#[serde(skip)]
pub ssir_wav_path: Option<std::path::PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_boost_envelope: Option<Vec<(f64, f64)>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_cut_envelope: Option<Vec<(f64, f64)>>,
}
fn default_loss_type() -> String {
"flat".to_string()
}
fn default_algorithm() -> String {
"autoeq:de".to_string()
}
fn default_strategy() -> String {
"lshade".to_string()
}
fn default_peq_model() -> String {
"pk".to_string()
}
fn default_opt_mode() -> String {
"iir".to_string()
}
fn default_num_filters() -> usize {
7
}
fn default_min_filter_improvement() -> f64 {
0.01
}
fn default_elimination_threshold() -> f64 {
0.005
}
fn default_min_q() -> f64 {
0.5
}
fn default_max_q() -> f64 {
3.0
}
fn default_min_db() -> f64 {
-12.0
}
fn default_max_db() -> f64 {
4.0
}
fn default_min_freq() -> f64 {
20.0
}
fn default_max_freq() -> f64 {
1600.0
}
fn default_max_iter() -> usize {
50000
}
fn default_population() -> usize {
50
}
fn default_refine() -> bool {
true
}
fn default_local_algo() -> String {
"cobyla".to_string()
}
fn default_psychoacoustic() -> bool {
true
}
fn default_smooth_n() -> usize {
2
}
fn default_asymmetric_loss() -> bool {
true
}
fn default_tolerance() -> f64 {
1e-5
}
fn default_atolerance() -> f64 {
1e-5
}
impl Default for OptimizerConfig {
fn default() -> Self {
Self {
loss_type: default_loss_type(),
algorithm: default_algorithm(),
strategy: default_strategy(),
num_filters: default_num_filters(),
min_filter_improvement: default_min_filter_improvement(),
elimination_threshold: default_elimination_threshold(),
min_q: default_min_q(),
max_q: default_max_q(),
min_db: default_min_db(),
max_db: default_max_db(),
min_freq: default_min_freq(),
max_freq: default_max_freq(),
max_iter: default_max_iter(),
population: default_population(),
peq_model: default_peq_model(),
mode: default_opt_mode(),
processing_mode: ProcessingMode::LowLatency,
fir: None,
mixed_config: None,
mixed_phase: None,
phase_correction: None,
seed: None,
refine: default_refine(),
local_algo: default_local_algo(),
psychoacoustic: default_psychoacoustic(),
smooth_n: default_smooth_n(),
asymmetric_loss: default_asymmetric_loss(),
tolerance: default_tolerance(),
atolerance: default_atolerance(),
allow_delay: None,
target_response: None,
target_tilt: None,
excursion_protection: None,
schroeder_split: None,
phase_alignment: None,
multi_seat: None,
gd_opt: None,
vog: None,
broadband_target_matching: None,
multi_measurement: None,
decomposed_correction: Some(DecomposedCorrectionSerdeConfig {
enabled: true,
..Default::default()
}),
cea2034_correction: None,
sub_config: None,
channel_matching: None,
ssir_wav_path: None,
max_boost_envelope: None,
min_cut_envelope: None,
}
}
}
impl OptimizerConfig {
pub fn allow_delay(&self) -> bool {
self.allow_delay.unwrap_or(self.mode != "iir")
}
pub fn max_boost_at_freq(&self, freq_hz: f64) -> f64 {
let envelope = match &self.max_boost_envelope {
Some(env) if !env.is_empty() => env,
_ => return self.max_db,
};
if freq_hz <= envelope[0].0 {
return envelope[0].1;
}
let last = envelope.len() - 1;
if freq_hz >= envelope[last].0 {
return envelope[last].1;
}
for i in 0..last {
let (f0, db0) = envelope[i];
let (f1, db1) = envelope[i + 1];
if freq_hz >= f0 && freq_hz <= f1 {
let t = (freq_hz.ln() - f0.ln()) / (f1.ln() - f0.ln());
return db0 + t * (db1 - db0);
}
}
self.max_db
}
pub fn migrate_target_config(&mut self) {
if self.target_response.is_some() {
if self.target_tilt.is_some() {
log::warn!(
"Both target_response and target_tilt are set; target_tilt is ignored. Use target_response exclusively."
);
}
if self
.broadband_target_matching
.as_ref()
.is_some_and(|b| b.enabled)
{
log::warn!(
"Both target_response and broadband_target_matching are set; broadband_target_matching is ignored. Set target_response.broadband_precorrection instead."
);
}
self.target_tilt = None;
self.broadband_target_matching = None;
return;
}
if self.target_tilt.is_none() && self.broadband_target_matching.is_none() {
return;
}
let tilt = self.target_tilt.take();
let bb = self.broadband_target_matching.take();
let (shape, slope) = match tilt.as_ref() {
Some(t) if t.tilt_type == TiltType::Harman => (TargetShape::Harman, -0.8),
Some(t) if t.tilt_type == TiltType::Custom => {
(TargetShape::Custom, t.slope_db_per_octave)
}
Some(t)
if t.tilt_type == TiltType::Flat
&& (t.slope_db_per_octave.abs() > 1e-6 || t.bass_shelf_db.abs() > 1e-6) =>
{
(TargetShape::Custom, t.slope_db_per_octave)
}
_ => (TargetShape::Flat, 0.0),
};
self.target_response = Some(TargetResponseConfig {
shape,
slope_db_per_octave: slope,
reference_freq: tilt.as_ref().map(|t| t.reference_freq).unwrap_or(1000.0),
curve_path: None,
preference: UserPreference {
bass_shelf_db: tilt.as_ref().map(|t| t.bass_shelf_db).unwrap_or(0.0),
bass_shelf_freq: tilt.as_ref().map(|t| t.bass_shelf_freq).unwrap_or(200.0),
treble_shelf_db: 0.0,
treble_shelf_freq: 8000.0,
},
broadband_precorrection: bb.as_ref().map(|b| b.enabled).unwrap_or(false),
});
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RoomConfig {
#[serde(default = "default_config_version")]
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system: Option<SystemConfig>,
pub speakers: HashMap<String, SpeakerConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub crossovers: Option<HashMap<String, CrossoverConfig>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_curve: Option<TargetCurveConfig>,
#[serde(default)]
pub optimizer: OptimizerConfig,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recording_config: Option<RecordingConfiguration>,
#[serde(skip)]
#[schemars(skip)]
pub cea2034_cache: Option<HashMap<String, crate::read::Cea2034Data>>,
}
impl RoomConfig {
pub fn resolve_paths(&mut self, base_dir: &std::path::Path) {
for speaker in self.speakers.values_mut() {
speaker.resolve_paths(base_dir);
}
if let Some(TargetCurveConfig::Path(ref mut path)) = self.target_curve
&& path.is_relative()
{
*path = base_dir.join(&*path);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_optimizer_config_default_has_decomposed_correction_enabled() {
let config = OptimizerConfig::default();
let dc = config
.decomposed_correction
.expect("decomposed_correction should be Some by default");
assert!(dc.enabled, "decomposed_correction should be enabled by default");
assert_eq!(dc.schroeder_freq, 250.0);
assert_eq!(dc.steady_state_weight, 0.4);
}
#[test]
fn test_decomposed_correction_serde_config_default() {
let dc = DecomposedCorrectionSerdeConfig::default();
assert!(dc.enabled);
assert_eq!(dc.schroeder_freq, 250.0);
assert_eq!(dc.steady_state_weight, 0.4);
assert_eq!(dc.min_mode_q, 3.0);
assert_eq!(dc.min_mode_prominence_db, 3.0);
assert_eq!(dc.mode_correction_weight, 1.0);
assert_eq!(dc.early_reflection_weight, 0.3);
}
#[test]
fn test_channel_matching_config_defaults() {
let cfg = ChannelMatchingConfig::default();
assert!(cfg.enabled);
assert_eq!(cfg.threshold_db, 0.75);
assert_eq!(cfg.max_filters, 5);
}
#[test]
fn test_max_boost_envelope_interpolation() {
let mut config = OptimizerConfig::default();
assert_eq!(config.max_boost_at_freq(100.0), config.max_db);
config.max_boost_envelope = Some(vec![
(20.0, 6.0),
(200.0, 4.0),
(1000.0, 2.0),
(8000.0, 0.0),
]);
assert!((config.max_boost_at_freq(20.0) - 6.0).abs() < 1e-10);
assert!((config.max_boost_at_freq(200.0) - 4.0).abs() < 1e-10);
assert!((config.max_boost_at_freq(1000.0) - 2.0).abs() < 1e-10);
assert!((config.max_boost_at_freq(8000.0) - 0.0).abs() < 1e-10);
assert!((config.max_boost_at_freq(10.0) - 6.0).abs() < 1e-10);
assert!((config.max_boost_at_freq(16000.0) - 0.0).abs() < 1e-10);
let mid_freq = (200.0_f64 * 1000.0).sqrt();
let mid_boost = config.max_boost_at_freq(mid_freq);
assert!(
(mid_boost - 3.0).abs() < 1e-6,
"geometric midpoint should give 3.0 dB, got {:.6}",
mid_boost
);
}
}