Files
analogue_synth/crates/synth-core/src/oscillator.rs

86 lines
2.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Voltage-controlled oscillator (VCO).
//!
//! Waveforms: Sine, Saw, Square, Triangle, Pulse (variable width).
//! Uses phase accumulation; bandlimited variants (BLEP/BLAMP) to follow.
use crate::{AudioProcessor, CVProcessor, config::SampleRate};
use crate::descriptor::{ComponentDescriptor, Direction, JackDescriptor, ParamDescriptor, SignalKind};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Waveform {
Sine,
Saw,
Square,
Triangle,
Pulse(f32), // pulse width 0.01.0
}
pub struct Vco {
pub waveform: Waveform,
pub freq_hz: f32,
phase: f32,
sample_rate: SampleRate,
}
impl Vco {
pub fn new(sample_rate: SampleRate, freq_hz: f32, waveform: Waveform) -> Self {
Self { waveform, freq_hz, phase: 0.0, sample_rate }
}
pub const DESCRIPTOR: ComponentDescriptor = ComponentDescriptor {
kind: "vco",
label: "VCO",
jacks: &[
JackDescriptor { id: "audio_out", label: "Out", direction: Direction::Output, signal: SignalKind::Audio },
],
params: &[
ParamDescriptor { id: "freq_hz", label: "Freq", min: 20.0, max: 20_000.0, default: 440.0, unit: "Hz", labels: &[] },
ParamDescriptor { id: "waveform", label: "Wave", min: 0.0, max: 4.0, default: 1.0, unit: "", labels: &["Sine", "Saw", "Sqr", "Tri", "Pls"] },
],
description: "\
## VCO — Voltage-Controlled Oscillator
Generates a periodic audio-rate waveform at the set frequency.
**Waveforms:** Sine · Saw · Square · Triangle · Pulse
**Freq** sets the base pitch. Connect an LFO or keyboard CV to `cv_freq_hz` for 1 V/oct pitch modulation (0 V = 440 Hz, +1 V = 880 Hz).
**Typical chain:** VCO Out → Filter In → VCA In → Output In",
};
#[inline]
fn next_sample(&mut self) -> f32 {
let p = self.phase;
let sample = match self.waveform {
Waveform::Sine => libm::sinf(p * core::f32::consts::TAU),
Waveform::Saw => 2.0 * p - 1.0,
Waveform::Square => if p < 0.5 { 1.0 } else { -1.0 },
Waveform::Triangle => 4.0 * (p - libm::floorf(p + 0.5)).abs() - 1.0,
Waveform::Pulse(w) => if p < w { 1.0 } else { -1.0 },
};
let next = p + self.freq_hz * self.sample_rate.period();
self.phase = next - libm::floorf(next);
sample
}
}
impl<const B: usize> AudioProcessor<B> for Vco {
fn process(&mut self, out: &mut [f32; B]) {
for s in out.iter_mut() {
*s = self.next_sample();
}
}
fn reset(&mut self) {
self.phase = 0.0;
}
}
impl<const B: usize> CVProcessor<B> for Vco {
/// CV is 1 V/oct: 0 V = 440 Hz, +1 V = 880 Hz, 1 V = 220 Hz.
fn set_cv(&mut self, cv: f32) {
self.freq_hz = 440.0 * libm::powf(2.0, cv);
}
}