use crate::Curve;
use ndarray::Array1;
use super::types::{TargetResponseConfig, TargetShape, TargetTiltConfig, TiltType};
pub fn build_target_curve_with_tilt(freqs: &Array1<f64>, config: &TargetTiltConfig) -> Curve {
let slope = match config.tilt_type {
TiltType::Flat => 0.0,
TiltType::Harman => -0.8, TiltType::Custom => config.slope_db_per_octave,
};
let ref_freq = config.reference_freq;
let bass_shelf_db = config.bass_shelf_db;
let bass_shelf_freq = config.bass_shelf_freq;
let spl = Array1::from_shape_fn(freqs.len(), |i| {
let f = freqs[i].max(1.0);
let tilt_db = slope * (f / ref_freq).log2();
let bass_boost = if bass_shelf_db.abs() > 0.001 && f < bass_shelf_freq * 2.0 {
let ratio = f / bass_shelf_freq;
let transition = 1.0 / (1.0 + ratio.powi(2));
bass_shelf_db * transition
} else {
0.0
};
tilt_db + bass_boost
});
Curve {
freq: freqs.clone(),
spl,
phase: None,
}
}
pub fn build_harman_target_curve(freqs: &Array1<f64>) -> Curve {
let config = TargetTiltConfig {
tilt_type: TiltType::Harman,
slope_db_per_octave: -0.8,
reference_freq: 1000.0,
bass_shelf_db: 0.0,
bass_shelf_freq: 200.0,
};
build_target_curve_with_tilt(freqs, &config)
}
pub fn build_harman_target_curve_with_bass_boost(freqs: &Array1<f64>, bass_boost_db: f64) -> Curve {
let config = TargetTiltConfig {
tilt_type: TiltType::Harman,
slope_db_per_octave: -0.8,
reference_freq: 1000.0,
bass_shelf_db: bass_boost_db,
bass_shelf_freq: 200.0,
};
build_target_curve_with_tilt(freqs, &config)
}
pub fn build_complete_target_curve(freqs: &Array1<f64>, config: &TargetResponseConfig) -> Curve {
let slope = match config.shape {
TargetShape::Flat => 0.0,
TargetShape::Harman => -0.8,
TargetShape::Custom => config.slope_db_per_octave,
TargetShape::File => {
log::warn!(
"build_complete_target_curve called with File shape but no curve provided; falling back to flat"
);
0.0
}
};
let ref_freq = config.reference_freq;
let pref = &config.preference;
let spl = Array1::from_shape_fn(freqs.len(), |i| {
let f = freqs[i].max(1.0);
let tilt_db = slope * (f / ref_freq).log2();
let bass_adj = if pref.bass_shelf_db.abs() > 0.001 && f < pref.bass_shelf_freq * 2.0 {
let ratio = f / pref.bass_shelf_freq;
let transition = 1.0 / (1.0 + ratio.powi(2));
pref.bass_shelf_db * transition
} else {
0.0
};
let treble_adj = if pref.treble_shelf_db.abs() > 0.001 && f > pref.treble_shelf_freq * 0.25
{
let ratio = f / pref.treble_shelf_freq;
let transition = 1.0 / (1.0 + (1.0 / ratio).powi(4));
pref.treble_shelf_db * transition
} else {
0.0
};
tilt_db + bass_adj + treble_adj
});
Curve {
freq: freqs.clone(),
spl,
phase: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_frequencies() -> Array1<f64> {
Array1::from(vec![
20.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 2000.0, 5000.0, 10000.0, 20000.0,
])
}
#[test]
fn test_flat_target() {
let freqs = test_frequencies();
let config = TargetTiltConfig {
tilt_type: TiltType::Flat,
..Default::default()
};
let curve = build_target_curve_with_tilt(&freqs, &config);
for &spl in curve.spl.iter() {
assert!((spl - 0.0).abs() < 1e-10, "Expected 0.0, got {}", spl);
}
}
#[test]
fn test_harman_target() {
let freqs = test_frequencies();
let curve = build_harman_target_curve(&freqs);
let idx_1k = freqs
.iter()
.position(|&f| (f - 1000.0).abs() < 1.0)
.unwrap();
assert!(
(curve.spl[idx_1k] - 0.0).abs() < 1e-10,
"At 1kHz should be 0 dB"
);
let idx_2k = freqs
.iter()
.position(|&f| (f - 2000.0).abs() < 1.0)
.unwrap();
assert!(
(curve.spl[idx_2k] - (-0.8)).abs() < 1e-10,
"At 2kHz should be -0.8 dB"
);
let idx_500 = freqs.iter().position(|&f| (f - 500.0).abs() < 1.0).unwrap();
assert!(
(curve.spl[idx_500] - 0.8).abs() < 1e-10,
"At 500Hz should be +0.8 dB"
);
}
#[test]
fn test_custom_slope() {
let freqs = test_frequencies();
let config = TargetTiltConfig {
tilt_type: TiltType::Custom,
slope_db_per_octave: -1.5, reference_freq: 1000.0,
bass_shelf_db: 0.0,
bass_shelf_freq: 200.0,
};
let curve = build_target_curve_with_tilt(&freqs, &config);
let idx_2k = freqs
.iter()
.position(|&f| (f - 2000.0).abs() < 1.0)
.unwrap();
assert!(
(curve.spl[idx_2k] - (-1.5)).abs() < 1e-10,
"At 2kHz should be -1.5 dB"
);
}
#[test]
fn test_bass_shelf() {
let freqs = test_frequencies();
let config = TargetTiltConfig {
tilt_type: TiltType::Flat, slope_db_per_octave: 0.0,
reference_freq: 1000.0,
bass_shelf_db: 3.0,
bass_shelf_freq: 200.0,
};
let curve = build_target_curve_with_tilt(&freqs, &config);
let idx_20 = 0; assert!(
curve.spl[idx_20] > 2.5,
"At 20Hz should have significant bass boost"
);
let idx_1k = freqs
.iter()
.position(|&f| (f - 1000.0).abs() < 1.0)
.unwrap();
assert!(
curve.spl[idx_1k].abs() < 0.1,
"At 1kHz should have negligible boost"
);
}
#[test]
fn test_combined_tilt_and_bass() {
let freqs = test_frequencies();
let curve = build_harman_target_curve_with_bass_boost(&freqs, 3.0);
let idx_1k = freqs
.iter()
.position(|&f| (f - 1000.0).abs() < 1.0)
.unwrap();
assert!(curve.spl[idx_1k].abs() < 0.1, "At 1kHz should be ~0 dB");
let idx_20 = 0;
assert!(
curve.spl[idx_20] > 5.0,
"At 20Hz should have significant boost from tilt + bass shelf"
);
}
#[test]
fn test_complete_target_flat() {
let freqs = test_frequencies();
let config = TargetResponseConfig::default(); let curve = build_complete_target_curve(&freqs, &config);
for &spl in curve.spl.iter() {
assert!(
(spl).abs() < 1e-10,
"Flat target should be all zeros, got {}",
spl
);
}
}
#[test]
fn test_complete_target_harman() {
let freqs = test_frequencies();
let config = TargetResponseConfig {
shape: TargetShape::Harman,
..Default::default()
};
let curve = build_complete_target_curve(&freqs, &config);
let idx_1k = freqs
.iter()
.position(|&f| (f - 1000.0).abs() < 1.0)
.unwrap();
assert!((curve.spl[idx_1k]).abs() < 1e-10);
let idx_2k = freqs
.iter()
.position(|&f| (f - 2000.0).abs() < 1.0)
.unwrap();
assert!((curve.spl[idx_2k] - (-0.8)).abs() < 1e-10);
}
#[test]
fn test_complete_target_with_treble_shelf() {
let freqs = test_frequencies();
let config = TargetResponseConfig {
shape: TargetShape::Flat,
preference: super::super::types::UserPreference {
treble_shelf_db: -2.0,
treble_shelf_freq: 8000.0,
..Default::default()
},
..Default::default()
};
let curve = build_complete_target_curve(&freqs, &config);
let idx_20k = freqs
.iter()
.position(|&f| (f - 20000.0).abs() < 1.0)
.unwrap();
assert!(
curve.spl[idx_20k] < -1.5,
"At 20kHz should have treble cut, got {:.2}",
curve.spl[idx_20k]
);
let idx_1k = freqs
.iter()
.position(|&f| (f - 1000.0).abs() < 1.0)
.unwrap();
assert!(
curve.spl[idx_1k].abs() < 0.1,
"At 1kHz should be near 0, got {:.2}",
curve.spl[idx_1k]
);
}
#[test]
fn test_complete_target_harman_plus_bass_boost() {
let freqs = test_frequencies();
let config = TargetResponseConfig {
shape: TargetShape::Harman,
preference: super::super::types::UserPreference {
bass_shelf_db: 3.0,
bass_shelf_freq: 200.0,
..Default::default()
},
..Default::default()
};
let curve = build_complete_target_curve(&freqs, &config);
assert!(
curve.spl[0] > 5.0,
"20Hz should have tilt + bass boost, got {:.2}",
curve.spl[0]
);
let idx_10k = freqs
.iter()
.position(|&f| (f - 10000.0).abs() < 1.0)
.unwrap();
assert!(
curve.spl[idx_10k] < -2.0,
"10kHz should be tilted down, got {:.2}",
curve.spl[idx_10k]
);
}
}