use crate::params::Waveform;
use std::f32::consts::TAU;
pub struct Oscillator {
phase: f32,
noise_lfsr: u32,
last_noise: f32,
just_wrapped: bool,
}
impl Default for Oscillator {
fn default() -> Self {
Self {
phase: 0.0,
noise_lfsr: 0xACE1_FEED,
last_noise: 0.0,
just_wrapped: false,
}
}
}
impl Oscillator {
pub fn reset(&mut self) {
self.phase = 0.0;
}
pub fn next_sample(
&mut self,
freq_hz: f32,
sample_rate: f32,
waveform: Waveform,
pulse_width: f32,
noise_mix: f32,
) -> f32 {
self.just_wrapped = false;
let inc = freq_hz / sample_rate;
self.phase += inc;
if self.phase >= 1.0 {
self.phase -= 1.0;
self.just_wrapped = true;
self.last_noise = self.tick_lfsr();
}
let p = self.phase;
let osc = match waveform {
Waveform::Pulse => {
if p < pulse_width {
1.0_f32
} else {
-1.0_f32
}
}
Waveform::Sawtooth => 2.0 * p - 1.0,
Waveform::Triangle => {
if p < 0.5 {
4.0 * p - 1.0
} else {
3.0 - 4.0 * p
}
}
Waveform::Noise => self.last_noise,
Waveform::PulseSaw => {
let pulse = if p < pulse_width { 1.0_f32 } else { -1.0_f32 };
let saw = 2.0 * p - 1.0;
(pulse + saw) * 0.5
}
Waveform::Sine => (TAU * p).sin(),
};
if noise_mix > 0.001 {
osc * (1.0 - noise_mix) + self.last_noise * noise_mix
} else {
osc
}
}
#[must_use]
pub fn just_wrapped(&self) -> bool {
self.just_wrapped
}
#[allow(clippy::cast_precision_loss)] fn tick_lfsr(&mut self) -> f32 {
let bit = self.noise_lfsr & 1;
self.noise_lfsr >>= 1;
if bit != 0 {
self.noise_lfsr ^= 0xB4BC_D35C;
}
self.noise_lfsr.cast_signed() as f32 / 2_147_483_648.0
}
}
#[inline]
#[must_use]
pub fn midi_to_hz(note: impl Into<crate::params::MidiNote>) -> f32 {
let note = note.into();
440.0 * 2.0_f32.powf((f32::from(note.as_u8()) - 69.0) / 12.0)
}
#[inline]
#[must_use]
pub fn detune_hz(base_hz: f32, cents: f32) -> f32 {
base_hz * 2.0_f32.powf(cents / 1200.0)
}
pub struct Lfo {
phase: f32,
}
impl Default for Lfo {
fn default() -> Self {
Self { phase: 0.0 }
}
}
impl Lfo {
pub fn next(&mut self, rate_hz: f32, sample_rate: f32) -> f32 {
self.phase += rate_hz / sample_rate;
if self.phase >= 1.0 {
self.phase -= 1.0;
}
(TAU * self.phase).sin()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn osc_sawtooth_bounds() {
let mut osc = Oscillator::default();
for _ in 0..4410 {
let s = osc.next_sample(440.0, 44100.0, Waveform::Sawtooth, 0.5, 0.0);
assert!((-1.0..=1.0).contains(&s), "sawtooth out of bounds: {s}");
}
}
#[test]
#[allow(clippy::float_cmp)] fn osc_pulse_bounds() {
let mut osc = Oscillator::default();
for _ in 0..4410 {
let s = osc.next_sample(440.0, 44100.0, Waveform::Pulse, 0.5, 0.0);
assert!(s == 1.0 || s == -1.0);
}
}
#[test]
fn osc_sine_bounds() {
let mut osc = Oscillator::default();
for _ in 0..4410 {
let s = osc.next_sample(440.0, 44100.0, Waveform::Sine, 0.5, 0.0);
assert!((-1.0..=1.0).contains(&s), "sine out of bounds: {s}");
}
}
#[test]
fn osc_just_wrapped_fires() {
let mut osc = Oscillator::default();
let mut wrapped_count = 0;
for _ in 0..4410 {
osc.next_sample(440.0, 44100.0, Waveform::Sawtooth, 0.5, 0.0);
if osc.just_wrapped() {
wrapped_count += 1;
}
}
assert!(
(40..=50).contains(&wrapped_count),
"unexpected wrap count: {wrapped_count}"
);
}
#[test]
fn midi_to_hz_a4() {
let hz = midi_to_hz(crate::params::MidiNote::A4);
assert!((hz - 440.0).abs() < 0.01);
}
#[test]
fn midi_to_hz_c4() {
let hz = midi_to_hz(crate::params::MidiNote::MIDDLE_C);
assert!((hz - 261.626).abs() < 0.1);
}
}