use crate::material::NUM_BANDS;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CrossoverConfig {
pub crossover_hz: f32,
pub transition_octaves: f32,
}
impl Default for CrossoverConfig {
fn default() -> Self {
Self {
crossover_hz: 500.0,
transition_octaves: 1.0,
}
}
}
#[must_use]
#[inline]
pub fn blend_weights(config: &CrossoverConfig) -> [f32; NUM_BANDS] {
let fc = config.crossover_hz;
let bw = config.transition_octaves.max(0.1);
std::array::from_fn(|band| {
let f = crate::material::FREQUENCY_BANDS[band];
if fc <= 0.0 {
return 1.0; }
let octaves_above = (f / fc).log2() / bw;
let weight = 1.0 / (1.0 + (-4.0 * octaves_above).exp());
weight.clamp(0.0, 1.0)
})
}
#[must_use]
#[inline]
pub fn blend_results(
wave_result: &[f32; NUM_BANDS],
geometric_result: &[f32; NUM_BANDS],
config: &CrossoverConfig,
) -> [f32; NUM_BANDS] {
let weights = blend_weights(config);
std::array::from_fn(|band| {
wave_result[band] * (1.0 - weights[band]) + geometric_result[band] * weights[band]
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn low_freq_favours_wave() {
let config = CrossoverConfig {
crossover_hz: 500.0,
transition_octaves: 0.5,
};
let weights = blend_weights(&config);
assert!(
weights[0] < 0.3,
"63 Hz should favour wave, got weight {}",
weights[0]
);
}
#[test]
fn high_freq_favours_geometric() {
let config = CrossoverConfig {
crossover_hz: 500.0,
transition_octaves: 0.5,
};
let weights = blend_weights(&config);
assert!(
weights[7] > 0.9,
"8 kHz should favour geometric, got weight {}",
weights[7]
);
}
#[test]
fn crossover_frequency_is_midpoint() {
let config = CrossoverConfig {
crossover_hz: 500.0,
transition_octaves: 1.0,
};
let weights = blend_weights(&config);
assert!(
(weights[3] - 0.5).abs() < 0.15,
"crossover freq should be ~0.5, got {}",
weights[3]
);
}
#[test]
fn blend_results_works() {
let wave = [1.0; NUM_BANDS];
let geom = [0.0; NUM_BANDS];
let config = CrossoverConfig::default();
let blended = blend_results(&wave, &geom, &config);
assert!(blended[0] > blended[7]);
}
#[test]
fn weights_in_valid_range() {
let config = CrossoverConfig::default();
let weights = blend_weights(&config);
for &w in &weights {
assert!((0.0..=1.0).contains(&w), "weight {w} out of range");
}
}
}