86 lines
2.8 KiB
Rust
86 lines
2.8 KiB
Rust
//! 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.0–1.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);
|
||
}
|
||
}
|