Initial commit of a toy analogue audio generator

Key components:
- no_std core so it can be used bare metal on embedded systems
- default implementation for RPi 2350, with midi input and I2s output using PIO
- Visualiser for the web to play with on a dev system
This commit is contained in:
2026-03-23 15:06:31 +00:00
commit 496b6bdc71
34 changed files with 3662 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
//! 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};
#[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 }
}
#[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);
}
}