use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use crate::dsp::validate_sample_rate;
use crate::error::Result;
use crate::rng::Rng;
const BEAM_RATIOS: [f32; 16] = [
1.0, 2.757, 5.404, 8.933, 13.344, 18.637, 24.812, 31.870, 39.810, 48.632, 58.336, 68.922,
80.390, 92.741, 105.973, 120.088,
];
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ModeSpec {
pub frequency: f32,
pub amplitude: f32,
pub decay: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ModePattern {
Harmonic,
Beam,
Plate,
StiffString,
Damped,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModalBank {
state_re: Vec<f32>,
state_im: Vec<f32>,
coeff_re: Vec<f32>,
coeff_im: Vec<f32>,
amplitude: Vec<f32>,
sample_rate: f32,
}
impl ModalBank {
pub fn new(specs: &[ModeSpec], sample_rate: f32) -> Result<Self> {
validate_sample_rate(sample_rate)?;
let nyquist = sample_rate * 0.5;
let valid: Vec<_> = specs
.iter()
.filter(|s| s.frequency >= 20.0 && s.frequency < nyquist && s.decay > 0.0)
.collect();
let n = valid.len();
let state_re = alloc::vec![0.0f32; n];
let state_im = alloc::vec![0.0f32; n];
let mut coeff_re = Vec::with_capacity(n);
let mut coeff_im = Vec::with_capacity(n);
let mut amplitude = Vec::with_capacity(n);
for spec in &valid {
let omega = core::f32::consts::TAU * spec.frequency / sample_rate;
let radius = crate::math::f32::exp(-6.908 / (spec.decay.max(0.001) * sample_rate))
.clamp(0.0, 0.9999);
coeff_re.push(radius * crate::math::f32::cos(omega));
coeff_im.push(radius * crate::math::f32::sin(omega));
amplitude.push(spec.amplitude);
}
Ok(Self {
state_re,
state_im,
coeff_re,
coeff_im,
amplitude,
sample_rate,
})
}
#[inline]
#[must_use]
pub fn mode_count(&self) -> usize {
self.amplitude.len()
}
#[inline]
pub fn process_sample(&mut self, excitation: f32) -> f32 {
let n = self.amplitude.len();
let mut out = 0.0f32;
for i in 0..n {
let new_re = excitation + self.coeff_re[i] * self.state_re[i]
- self.coeff_im[i] * self.state_im[i];
let new_im = self.coeff_im[i] * self.state_re[i] + self.coeff_re[i] * self.state_im[i];
self.state_re[i] = new_re;
self.state_im[i] = new_im;
out += self.amplitude[i] * new_re;
}
out
}
#[inline]
pub fn process_block(&mut self, excitation: &[f32], output: &mut [f32]) {
debug_assert_eq!(
excitation.len(),
output.len(),
"excitation and output buffers must be the same length"
);
let len = excitation.len().min(output.len());
for i in 0..len {
output[i] = self.process_sample(excitation[i]);
}
}
pub fn reset(&mut self) {
for s in &mut self.state_re {
*s = 0.0;
}
for s in &mut self.state_im {
*s = 0.0;
}
}
}
#[must_use]
pub fn generate_modes(
props: &crate::material::MaterialProperties,
pattern: ModePattern,
count: usize,
damping_factor: f32,
) -> Vec<ModeSpec> {
let f0 = props.resonance;
let rolloff_exp = 2.0 - 1.5 * props.brightness;
let mut rng = Rng::new((f0 * 1000.0) as u64);
(1..=count)
.filter_map(|k| {
let kf = k as f32;
let freq = match pattern {
ModePattern::Harmonic => f0 * kf,
ModePattern::Beam => {
if k <= BEAM_RATIOS.len() {
f0 * BEAM_RATIOS[k - 1]
} else {
f0 * (kf + 0.5) * (kf + 0.5) / (1.5056 * 1.5056)
}
}
ModePattern::Plate => {
f0 * crate::math::f32::powf(kf, 1.7)
}
ModePattern::StiffString => {
let b = 0.001f32;
f0 * kf * crate::math::f32::sqrt(1.0 + b * kf * kf)
}
ModePattern::Damped => f0 * (1.0 + rng.next_f32_range(-0.1, 0.1)),
};
if freq > 20000.0 {
return None;
}
let amplitude = 1.0 / crate::math::f32::powf(kf, rolloff_exp);
let freq_ratio = freq / f0;
let decay =
props.decay / (1.0 + damping_factor * (freq_ratio - 1.0) * (freq_ratio - 1.0));
Some(ModeSpec {
frequency: freq,
amplitude,
decay: decay.max(0.001),
})
})
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ExcitationType {
Impulse,
NoiseBurst {
duration_samples: usize,
},
HalfSine {
duration_samples: usize,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Exciter {
excitation_type: ExcitationType,
amplitude: f32,
position: usize,
active: bool,
rng: Rng,
}
impl Exciter {
#[must_use]
pub fn new(excitation_type: ExcitationType, amplitude: f32) -> Self {
Self {
excitation_type,
amplitude,
position: 0,
active: false,
rng: Rng::new(31337),
}
}
pub fn trigger(&mut self) {
self.position = 0;
self.active = true;
}
#[inline]
pub fn next_sample(&mut self) -> f32 {
if !self.active {
return 0.0;
}
let sample = match self.excitation_type {
ExcitationType::Impulse => {
if self.position == 0 {
self.active = false; self.amplitude
} else {
0.0
}
}
ExcitationType::NoiseBurst { duration_samples } => {
if self.position < duration_samples {
self.amplitude * self.rng.next_f32()
} else {
self.active = false;
0.0
}
}
ExcitationType::HalfSine { duration_samples } => {
if self.position < duration_samples {
let t = self.position as f32 / duration_samples as f32;
self.amplitude * crate::math::f32::sin(core::f32::consts::PI * t)
} else {
self.active = false;
0.0
}
}
};
self.position += 1;
sample
}
#[inline]
#[must_use]
pub fn is_active(&self) -> bool {
self.active
}
}