use serde::{Deserialize, Serialize};
use crate::error::{self, Result};
use crate::oscillator::{Oscillator, Waveform};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum LfoShape {
Sine,
Triangle,
Square,
SawUp,
SawDown,
SampleAndHold,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum LfoMode {
Bipolar,
Unipolar,
}
fn default_sh_value() -> f32 {
0.5
}
fn default_rng_state() -> u32 {
42
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Lfo {
shape: LfoShape,
frequency: f32,
sample_rate: f32,
phase: f32,
mode: LfoMode,
pub depth: f32,
#[serde(skip, default = "default_sh_value")]
sh_value: f32,
#[serde(skip, default = "default_rng_state")]
rng_state: u32,
}
impl Lfo {
pub fn new(shape: LfoShape, frequency: f32, sample_rate: f32) -> Result<Self> {
if let Some(e) = error::validate_sample_rate(sample_rate) {
return Err(e);
}
if frequency < 0.0 || !frequency.is_finite() {
return Err(crate::NaadError::InvalidParameter {
name: "frequency".to_string(),
reason: "must be >= 0 and finite".to_string(),
});
}
let mut rng = 42u32;
rng ^= rng << 13;
rng ^= rng >> 17;
rng ^= rng << 5;
let initial_sh = (rng as f32 / u32::MAX as f32) * 2.0 - 1.0;
Ok(Self {
shape,
frequency,
sample_rate,
phase: 0.0,
mode: LfoMode::Bipolar,
depth: 1.0,
sh_value: initial_sh,
rng_state: rng,
})
}
pub fn from_waveform(waveform: Waveform, frequency: f32, sample_rate: f32) -> Result<Self> {
let shape = match waveform {
Waveform::Sine => LfoShape::Sine,
Waveform::Triangle => LfoShape::Triangle,
Waveform::Square => LfoShape::Square,
Waveform::Saw => LfoShape::SawDown,
_ => LfoShape::Sine,
};
Self::new(shape, frequency, sample_rate)
}
#[inline]
#[must_use]
pub fn next_value(&mut self) -> f32 {
let raw = self.raw_sample();
let dt = self.frequency / self.sample_rate;
let prev_phase = self.phase;
self.phase += dt;
if self.phase >= 1.0 {
self.phase -= 1.0;
}
if matches!(self.shape, LfoShape::SampleAndHold) && self.phase < prev_phase {
self.rng_state ^= self.rng_state << 13;
self.rng_state ^= self.rng_state >> 17;
self.rng_state ^= self.rng_state << 5;
self.sh_value = (self.rng_state as f32 / u32::MAX as f32) * 2.0 - 1.0;
}
let output = match self.mode {
LfoMode::Bipolar => raw,
LfoMode::Unipolar => (raw + 1.0) * 0.5,
};
output * self.depth
}
#[inline]
fn raw_sample(&self) -> f32 {
let t = self.phase;
match self.shape {
LfoShape::Sine => (t * std::f32::consts::TAU).sin(),
LfoShape::Triangle => {
if t < 0.25 {
4.0 * t
} else if t < 0.75 {
2.0 - 4.0 * t
} else {
4.0 * t - 4.0
}
}
LfoShape::Square => {
if t < 0.5 {
1.0
} else {
-1.0
}
}
LfoShape::SawUp => 2.0 * t - 1.0,
LfoShape::SawDown => 1.0 - 2.0 * t,
LfoShape::SampleAndHold => self.sh_value,
}
}
pub fn set_frequency(&mut self, freq: f32) -> Result<()> {
if freq < 0.0 || !freq.is_finite() {
return Err(crate::NaadError::InvalidParameter {
name: "frequency".to_string(),
reason: "must be >= 0 and finite".to_string(),
});
}
self.frequency = freq;
Ok(())
}
pub fn set_shape(&mut self, shape: LfoShape) {
self.shape = shape;
}
pub fn set_mode(&mut self, mode: LfoMode) {
self.mode = mode;
}
#[inline]
#[must_use]
pub fn shape(&self) -> LfoShape {
self.shape
}
#[inline]
#[must_use]
pub fn mode(&self) -> LfoMode {
self.mode
}
}
pub trait ModulationSource {
fn next_modulation_value(&mut self) -> f32;
}
impl ModulationSource for Lfo {
fn next_modulation_value(&mut self) -> f32 {
self.next_value()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FmModulator {
carrier: Oscillator,
modulator: Oscillator,
pub mod_index: f32,
carrier_base_freq: f32,
}
impl FmModulator {
pub fn new(carrier_freq: f32, mod_freq: f32, mod_index: f32, sample_rate: f32) -> Result<Self> {
if let Some(e) = error::validate_sample_rate(sample_rate) {
return Err(e);
}
let carrier = Oscillator::new(Waveform::Sine, carrier_freq, sample_rate)?;
let modulator = Oscillator::new(Waveform::Sine, mod_freq, sample_rate)?;
Ok(Self {
carrier,
modulator,
mod_index,
carrier_base_freq: carrier_freq,
})
}
#[inline]
#[must_use]
pub fn carrier(&self) -> &Oscillator {
&self.carrier
}
#[inline]
#[must_use]
pub fn modulator(&self) -> &Oscillator {
&self.modulator
}
#[inline]
pub fn fm_next_sample(&mut self) -> f32 {
let mod_out = self.modulator.next_sample();
let freq_deviation = mod_out * self.mod_index * self.modulator.frequency();
let inst_freq = self.carrier_base_freq + freq_deviation;
let nyquist = self.carrier.sample_rate() / 2.0;
let clamped_freq = inst_freq.clamp(0.1, nyquist - 1.0);
let dt = clamped_freq / self.carrier.sample_rate();
self.carrier.advance_phase_sine(dt)
}
#[inline]
pub fn fill_buffer(&mut self, buffer: &mut [f32]) {
for sample in buffer.iter_mut() {
*sample = self.fm_next_sample();
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RingModulator {
modulator: Oscillator,
}
impl RingModulator {
pub fn new(waveform: Waveform, mod_freq: f32, sample_rate: f32) -> Result<Self> {
let modulator = Oscillator::new(waveform, mod_freq, sample_rate)?;
Ok(Self { modulator })
}
#[inline]
#[must_use]
pub fn modulator(&self) -> &Oscillator {
&self.modulator
}
#[inline]
#[must_use]
pub fn process_sample(&mut self, input: f32) -> f32 {
input * self.modulator.next_sample()
}
#[inline]
pub fn process_buffer(&mut self, buffer: &mut [f32]) {
for sample in buffer.iter_mut() {
*sample = self.process_sample(*sample);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lfo_basic() {
let mut lfo = Lfo::new(LfoShape::Sine, 5.0, 44100.0).unwrap();
let val = lfo.next_value();
assert!(val.is_finite());
}
#[test]
fn test_lfo_depth() {
let mut lfo = Lfo::new(LfoShape::Sine, 5.0, 44100.0).unwrap();
lfo.depth = 0.5;
for _ in 0..10000 {
let val = lfo.next_value();
assert!(
val.abs() <= 0.51,
"LFO with depth 0.5 should stay within bounds, got {val}"
);
}
}
#[test]
fn test_lfo_all_shapes() {
let shapes = [
LfoShape::Sine,
LfoShape::Triangle,
LfoShape::Square,
LfoShape::SawUp,
LfoShape::SawDown,
LfoShape::SampleAndHold,
];
for shape in &shapes {
let mut lfo = Lfo::new(*shape, 5.0, 44100.0).unwrap();
for _ in 0..1000 {
let val = lfo.next_value();
assert!(
(-1.01..=1.01).contains(&val),
"LFO {shape:?} out of bipolar range: {val}"
);
}
}
}
#[test]
fn test_lfo_unipolar() {
let mut lfo = Lfo::new(LfoShape::Sine, 5.0, 44100.0).unwrap();
lfo.set_mode(LfoMode::Unipolar);
for _ in 0..10000 {
let val = lfo.next_value();
assert!(
(-0.01..=1.01).contains(&val),
"Unipolar LFO should be 0..1, got {val}"
);
}
}
#[test]
fn test_fm_synthesis() {
let mut fm = FmModulator::new(440.0, 220.0, 2.0, 44100.0).unwrap();
let mut buf = [0.0f32; 1024];
fm.fill_buffer(&mut buf);
assert!(buf.iter().all(|s| s.is_finite()));
assert!(buf.iter().any(|&s| s != 0.0));
}
#[test]
fn test_ring_modulator() {
let mut ring = RingModulator::new(Waveform::Sine, 300.0, 44100.0).unwrap();
let output = ring.process_sample(1.0);
assert!(output.is_finite());
}
#[test]
fn test_modulation_source_trait() {
let mut lfo = Lfo::new(LfoShape::Sine, 5.0, 44100.0).unwrap();
let val = lfo.next_modulation_value();
assert!(val.is_finite());
}
#[test]
fn test_serde_roundtrip() {
let fm = FmModulator::new(440.0, 220.0, 2.0, 44100.0).unwrap();
let json = serde_json::to_string(&fm).unwrap();
let back: FmModulator = serde_json::from_str(&json).unwrap();
assert!((fm.mod_index - back.mod_index).abs() < f32::EPSILON);
}
}