use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use crate::dsp::{DcBlocker, validate_duration, validate_sample_rate};
#[cfg(feature = "naad-backend")]
use crate::error::GhurniError;
use crate::error::Result;
use crate::traits::Synthesizer;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum GearMaterial {
Steel,
CastIron,
Brass,
Nylon,
}
impl GearMaterial {
#[must_use]
fn properties(self) -> (f32, f32, f32) {
match self {
Self::Steel => (3500.0, 0.08, 0.9),
Self::CastIron => (2000.0, 0.04, 0.5),
Self::Brass => (2800.0, 0.06, 0.7),
Self::Nylon => (1000.0, 0.01, 0.2),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Gear {
teeth: u32,
material: GearMaterial,
resonance: f32,
decay: f32,
brightness: f32,
sample_rate: f32,
rpm: f32,
sample_position: usize,
dc_blocker: DcBlocker,
#[cfg(feature = "naad-backend")]
mesh_osc: naad::oscillator::Oscillator,
#[cfg(feature = "naad-backend")]
noise_gen: naad::noise::NoiseGenerator,
#[cfg(feature = "naad-backend")]
resonance_filter: naad::filter::BiquadFilter,
#[cfg(not(feature = "naad-backend"))]
rng: crate::rng::Rng,
}
impl Gear {
pub fn new(teeth: u32, material: GearMaterial, sample_rate: f32) -> Result<Self> {
validate_sample_rate(sample_rate)?;
let teeth = teeth.max(4);
let (resonance, decay, brightness) = material.properties();
let nyquist = sample_rate * 0.49;
#[allow(unused_variables)]
let initial_mesh_freq = 100.0_f32.min(nyquist);
Ok(Self {
teeth,
material,
resonance,
decay,
brightness,
sample_rate,
rpm: 1000.0,
sample_position: 0,
dc_blocker: DcBlocker::new(sample_rate),
#[cfg(feature = "naad-backend")]
mesh_osc: naad::oscillator::Oscillator::new(
naad::oscillator::Waveform::Sine,
initial_mesh_freq,
sample_rate,
)
.map_err(|e| GhurniError::SynthesisFailed(alloc::format!("{e}")))?,
#[cfg(feature = "naad-backend")]
noise_gen: naad::noise::NoiseGenerator::new(
naad::noise::NoiseType::White,
teeth * 7 + material as u32,
),
#[cfg(feature = "naad-backend")]
resonance_filter: naad::filter::BiquadFilter::new(
naad::filter::FilterType::BandPass,
sample_rate,
resonance.min(nyquist),
4.0,
)
.map_err(|e| GhurniError::SynthesisFailed(alloc::format!("{e}")))?,
#[cfg(not(feature = "naad-backend"))]
rng: crate::rng::Rng::new(teeth as u64 * 7 + material as u64),
})
}
pub fn set_rpm(&mut self, rpm: f32) {
self.rpm = rpm.clamp(1.0, 50000.0);
}
#[must_use]
#[inline]
pub fn mesh_frequency(&self, rpm: f32) -> f32 {
(rpm / 60.0) * self.teeth as f32
}
pub fn synthesize(&mut self, rpm: f32, duration: f32) -> Result<Vec<f32>> {
validate_duration(duration)?;
self.set_rpm(rpm);
let num_samples = (self.sample_rate * duration) as usize;
let mut output = alloc::vec![0.0f32; num_samples];
self.process_block(&mut output);
Ok(output)
}
#[inline]
pub fn process_block(&mut self, output: &mut [f32]) {
#[cfg(feature = "naad-backend")]
self.process_block_naad(output);
#[cfg(not(feature = "naad-backend"))]
self.process_block_fallback(output);
for sample in output.iter_mut() {
*sample = self.dc_blocker.process(*sample);
}
self.sample_position += output.len();
}
#[cfg(feature = "naad-backend")]
fn process_block_naad(&mut self, output: &mut [f32]) {
let mesh_freq = self.mesh_frequency(self.rpm);
let nyquist = self.sample_rate * 0.49;
let _ = self.mesh_osc.set_frequency(mesh_freq.min(nyquist));
let amp = 0.3;
for (i, sample) in output.iter_mut().enumerate() {
let mesh = self.mesh_osc.next_sample() * amp * 0.5;
let abs_pos = (self.sample_position + i) as f32;
let mesh_phase = (abs_pos * mesh_freq / self.sample_rate) % 1.0;
let ring_env = if mesh_phase < 0.05 {
1.0
} else {
naad::dsp_util::db_to_amplitude(
-mesh_phase / self.decay * 20.0 / core::f32::consts::LOG10_E,
)
};
let ring_excitation = self.noise_gen.next_sample() * ring_env;
let ring =
self.resonance_filter.process_sample(ring_excitation) * amp * self.brightness;
let noise = self.noise_gen.next_sample() * amp * 0.05;
*sample = mesh + ring + noise;
}
}
#[cfg(not(feature = "naad-backend"))]
fn process_block_fallback(&mut self, output: &mut [f32]) {
let mesh_freq = self.mesh_frequency(self.rpm);
let mesh_omega = core::f32::consts::TAU * mesh_freq / self.sample_rate;
let res_omega = core::f32::consts::TAU * self.resonance / self.sample_rate;
let amp = 0.3;
for (i, sample) in output.iter_mut().enumerate() {
let abs_pos = (self.sample_position + i) as f32;
let mesh = crate::math::f32::sin(mesh_omega * abs_pos) * amp * 0.5;
let mesh_phase = (abs_pos * mesh_freq / self.sample_rate) % 1.0;
let ring_env = if mesh_phase < 0.05 {
1.0
} else {
crate::math::f32::exp(-mesh_phase / self.decay)
};
let ring =
crate::math::f32::sin(res_omega * abs_pos) * ring_env * amp * self.brightness;
let noise = self.rng.next_f32() * amp * 0.05;
*sample = mesh + ring + noise;
}
}
}
impl Synthesizer for Gear {
fn process_block(&mut self, output: &mut [f32]) {
Gear::process_block(self, output);
}
fn set_rpm(&mut self, rpm: f32) {
Gear::set_rpm(self, rpm);
}
fn rpm(&self) -> f32 {
self.rpm
}
fn sample_rate(&self) -> f32 {
self.sample_rate
}
}