Files

5.9 KiB
Raw Permalink Blame History

synth-core

A no_std DSP library providing analogue-modelling synthesizer building blocks. Designed to compile for any target — embedded bare-metal (Cortex-M), WebAssembly, and native — with no heap allocation.

Overview

synth-core is the shared engine that powers both the RP2350 firmware (synth-embedded) and the browser visualiser (synth-visualiser). All modules operate on fixed-size blocks of f32 samples using const-generic arrays, so every allocation happens on the stack at compile time.

VCO → Filter → VCA ← ADSR
 ↑              ↑
LFO            LFO

Modules

oscillator — Voltage-Controlled Oscillator

Phase-accumulator oscillator with five waveforms.

use synth_core::{config::SR_48000, oscillator::{Vco, Waveform}, AudioProcessor};

let mut vco = Vco::new(SR_48000, 440.0, Waveform::Saw);
vco.waveform = Waveform::Square;
vco.freq_hz  = 880.0;

let mut buf = [0.0f32; 256];
vco.process(&mut buf);  // fills buf with one block of audio

Waveforms: Sine, Saw, Square, Triangle, Pulse(f32) (duty cycle 01).

filter — State-Variable Filter

Two-integrator SVF with selectable mode and voltage-controlled cutoff.

use synth_core::{config::SR_48000, filter::{FilterMode, Svf}, AudioProcessor};

let mut filt = Svf::new(SR_48000, 2_000.0, 0.5, FilterMode::LowPass);
filt.cutoff_hz = 1_000.0;
filt.resonance = 0.8;   // 0.0 = flat, 1.0 = self-oscillation

filt.process(&mut buf); // in-place

Modes: LowPass, HighPass, BandPass, Notch.

envelope — ADSR Envelope Generator

Classic Attack / Decay / Sustain / Release envelope.

use synth_core::{config::SR_48000, envelope::Adsr, AudioProcessor};

let mut adsr = Adsr::new(SR_48000);
adsr.attack_s  = 0.01;
adsr.decay_s   = 0.2;
adsr.sustain   = 0.7;   // 0.01.0
adsr.release_s = 0.5;

adsr.gate_on();          // key press
let mut env = [0.0f32; 256];
adsr.process(&mut env);  // outputs 0.01.0 amplitude envelope

adsr.gate_off();         // key release

vca — Voltage-Controlled Amplifier

Multiplies audio by a gain value, optionally driven by an envelope.

use synth_core::{config::SR_48000, vca::Vca};

let mut vca = Vca::new(0.8);        // fixed gain
vca.apply_envelope(&mut audio, &env); // per-sample multiply by envelope

lfo — Low-Frequency Oscillator

Same waveform engine as the VCO, tuned to sub-audio rates.

use synth_core::{config::SR_48000, lfo::Lfo, oscillator::Waveform, AudioProcessor};

let mut lfo = Lfo::new(SR_48000);
lfo.rate_hz  = 5.0;
lfo.depth    = 0.5;
lfo.waveform = Waveform::Sine;

let mut cv = [0.0f32; 256];
lfo.process(&mut cv);   // outputs ±depth CV

midi — MIDI Byte-Stream Parser

Parses a raw MIDI byte stream (running status supported) into typed events.

use synth_core::midi::{MidiParser, MidiEvent};

let mut parser = MidiParser::new();

// Feed bytes from UART / USB
if let Some(event) = parser.push_byte(byte) {
    match event {
        MidiEvent::NoteOn  { channel, note, velocity } => { /* ... */ }
        MidiEvent::NoteOff { channel, note, velocity } => { /* ... */ }
        MidiEvent::ControlChange { channel, controller, value } => { /* ... */ }
        MidiEvent::PitchBend    { channel, value }              => { /* ... */ }
        MidiEvent::ProgramChange { channel, program }           => { /* ... */ }
        _ => {}
    }
}

patch — Signal Routing / Patch Bay

Fixed-capacity cable graph connecting named outputs to inputs, no heap required.

use synth_core::patch::Patch;

let mut patch: Patch<16> = Patch::new(); // up to 16 cables
patch.connect("vco.out", "filter.in");
patch.connect("lfo.out", "filter.cv");

for cable in patch.cables() {
    println!("{}{}", cable.src, cable.dst);
}

math — Utility Functions

use synth_core::math::{midi_note_to_hz, db_to_linear, linear_to_db, lerp};

let freq = midi_note_to_hz(69); // 440.0 Hz  (A4)
let gain = db_to_linear(-6.0);  // ≈ 0.501

config — Sample Rate

use synth_core::config::{SampleRate, SR_44100, SR_48000};

let period = SR_48000.period(); // 1.0 / 48000.0

Traits

AudioProcessor<const BLOCK: usize>

Implemented by every DSP module.

Method Description
process(&mut self, out: &mut [f32; BLOCK]) Fill out with one block of samples
reset(&mut self) Reset all internal state (phase, integrators, …)

CVProcessor<const BLOCK: usize>

Extends AudioProcessor for modules that accept a control-voltage input.

Method Description
set_cv(&mut self, cv: f32) Apply modulation (1 V/octave convention for pitch)

Complete Voice Example

use synth_core::{
    config::SR_48000,
    oscillator::{Vco, Waveform},
    filter::{FilterMode, Svf},
    envelope::Adsr,
    vca::Vca,
    AudioProcessor,
};

const BLOCK: usize = 256;

let sr = SR_48000;
let mut vco  = Vco::new(sr, 440.0, Waveform::Saw);
let mut filt = Svf::new(sr, 2_000.0, 0.4, FilterMode::LowPass);
let mut adsr = Adsr::new(sr);
let mut vca  = Vca::new(1.0);

adsr.gate_on();

let mut audio = [0.0f32; BLOCK];
let mut env   = [0.0f32; BLOCK];

loop {
    vco.process(&mut audio);
    adsr.process(&mut env);
    filt.process(&mut audio);
    vca.apply_envelope(&mut audio, &env);
    // send `audio` to output device
}

Dependencies

Crate Purpose
libm no_std-compatible transcendental math (sin, pow, …)
micromath Fast numerical approximations
num-traits Numeric trait abstractions
heapless Fixed-size Vec/ArrayVec without allocation
midi-types MIDI type definitions
serde Optional serialization support

Building

# Native (for tests)
cargo test -p synth-core

# WebAssembly
cargo build -p synth-core --target wasm32-unknown-unknown

# Embedded (Cortex-M33, RP2350)
cargo build -p synth-core --target thumbv8m.main-none-eabihf