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::smooth::SmoothedParam;
use crate::traits::Synthesizer;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BeltDrive {
pulley_diameter: f32,
tension: f32,
sample_rate: f32,
rpm: f32,
sample_position: usize,
smooth_rpm: SmoothedParam,
dc_blocker: DcBlocker,
#[cfg(feature = "naad-backend")]
squeal_osc: naad::oscillator::Oscillator,
#[cfg(feature = "naad-backend")]
noise_gen: naad::noise::NoiseGenerator,
#[cfg(feature = "naad-backend")]
flap_filter: naad::filter::BiquadFilter,
#[cfg(not(feature = "naad-backend"))]
rng: crate::rng::Rng,
}
impl BeltDrive {
pub fn new(pulley_diameter: f32, tension: f32, sample_rate: f32) -> Result<Self> {
validate_sample_rate(sample_rate)?;
let pulley_diameter = pulley_diameter.clamp(20.0, 500.0);
let tension = tension.clamp(0.0, 1.0);
let nyquist = sample_rate * 0.49;
#[allow(unused_variables)]
let squeal_freq = (2000.0 + (1.0 - tension) * 3000.0).min(nyquist);
Ok(Self {
pulley_diameter,
tension,
sample_rate,
rpm: 0.0,
sample_position: 0,
smooth_rpm: SmoothedParam::new(0.0, 0.03, sample_rate),
dc_blocker: DcBlocker::new(sample_rate),
#[cfg(feature = "naad-backend")]
squeal_osc: naad::oscillator::Oscillator::new(
naad::oscillator::Waveform::Sine,
squeal_freq,
sample_rate,
)
.map_err(|e| GhurniError::SynthesisFailed(alloc::format!("{e}")))?,
#[cfg(feature = "naad-backend")]
noise_gen: naad::noise::NoiseGenerator::new(
naad::noise::NoiseType::Pink,
pulley_diameter.to_bits() + tension.to_bits(),
),
#[cfg(feature = "naad-backend")]
flap_filter: naad::filter::BiquadFilter::new(
naad::filter::FilterType::BandPass,
sample_rate,
500.0,
2.0,
)
.map_err(|e| GhurniError::SynthesisFailed(alloc::format!("{e}")))?,
#[cfg(not(feature = "naad-backend"))]
rng: crate::rng::Rng::new(pulley_diameter.to_bits() as u64),
})
}
pub fn set_tension(&mut self, tension: f32) {
self.tension = tension.clamp(0.0, 1.0);
}
#[must_use]
#[inline]
pub fn belt_speed(&self, rpm: f32) -> f32 {
core::f32::consts::PI * self.pulley_diameter * rpm / 60000.0
}
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)
}
pub fn process_block(&mut self, output: &mut [f32]) {
self.smooth_rpm.set_target(self.rpm);
#[allow(unused_variables)]
let nyquist = self.sample_rate * 0.49;
let squeal_amp = (1.0 - self.tension) * 0.3;
let speed = self.belt_speed(self.rpm);
let flap_amp = (speed / 20.0).clamp(0.0, 0.2);
let belt_circumference = core::f32::consts::PI * self.pulley_diameter / 1000.0; let belt_rotation_freq = if belt_circumference > 0.0 {
speed / belt_circumference.max(0.1)
} else {
0.0
};
for (i, sample) in output.iter_mut().enumerate() {
let _smooth = self.smooth_rpm.next_value();
#[cfg(feature = "naad-backend")]
{
let squeal_freq = (2000.0 + (1.0 - self.tension) * 3000.0).min(nyquist);
let _ = self.squeal_osc.set_frequency(squeal_freq);
let squeal = self.squeal_osc.next_sample() * squeal_amp;
let raw_flap = self.noise_gen.next_sample() * flap_amp;
let flap = self.flap_filter.process_sample(raw_flap);
let abs_pos = (self.sample_position + i) as f32;
let belt_mod = if belt_rotation_freq > 0.0 {
let belt_omega = core::f32::consts::TAU * belt_rotation_freq / self.sample_rate;
0.8 + 0.2 * libm::sinf(belt_omega * abs_pos)
} else {
1.0
};
*sample = (squeal + flap) * belt_mod;
}
#[cfg(not(feature = "naad-backend"))]
{
let abs_pos = (self.sample_position + i) as f32;
let squeal_freq = 2000.0 + (1.0 - self.tension) * 3000.0;
let squeal_omega = core::f32::consts::TAU * squeal_freq / self.sample_rate;
let squeal = crate::math::f32::sin(squeal_omega * abs_pos) * squeal_amp;
let flap = self.rng.next_f32() * flap_amp;
let belt_mod = if belt_rotation_freq > 0.0 {
let belt_omega = core::f32::consts::TAU * belt_rotation_freq / self.sample_rate;
0.8 + 0.2 * crate::math::f32::sin(belt_omega * abs_pos)
} else {
1.0
};
*sample = (squeal + flap) * belt_mod;
}
}
for sample in output.iter_mut() {
*sample = self.dc_blocker.process(*sample);
}
self.sample_position += output.len();
}
}
impl Synthesizer for BeltDrive {
fn process_block(&mut self, output: &mut [f32]) {
BeltDrive::process_block(self, output);
}
fn set_rpm(&mut self, rpm: f32) {
self.rpm = rpm.clamp(0.0, 20000.0);
}
fn rpm(&self) -> f32 {
self.rpm
}
fn sample_rate(&self) -> f32 {
self.sample_rate
}
}