-
Notifications
You must be signed in to change notification settings - Fork 1
Home
A 16-bit audio synthesis library for ESP8266, ESP32, RP2040/RP2350 (Pico), and Teensy 4.x using I2S DACs.
#include "M16.h"
#include "Osc.h"
Osc osc;
void setup() {
Serial.begin(115200);
setIsDualCore(false);
osc.sinGen();
osc.setPitch(69); // A4
audioStart();
}
void loop() {}
void audioUpdate() {
int16_t sample = osc.next();
audioBlockWrite(sample, sample);
}Every sketch must include M16.h, call audioStart() in setup(), and implement audioUpdate() which calls audioBlockWrite(left, right).
| Function | Description |
|---|---|
audioStart() |
Initialize I2S and start audio tasks. Call once in setup(). |
audioBlockWrite(left, right) |
Submit a stereo frame. Use in all new sketches. |
setIsDualCore(bool) |
false = dedicated audio core (default, recommended for serial chains). true = block-partitioned dual-core for independent voice arrays. Must be called before audioStart(). |
setSampleRate(uint32_t) |
Set sample rate in Hz (default 44100). Call before audioStart(). |
seti2sPins(bck, ws, dout, din) |
Configure I2S pins. Call before audioStart(). |
useInternalDAC() |
Enable ESP32/ESP32-S2 internal 8-bit DAC output. Call before audioStart(). |
For polyphonic sketches with independent voice arrays:
void audioUpdate() {
int32_t mix = 0;
for (int i = audioPartitionOffset(); i < voiceCount; i += audioPartitionStride()) {
mix += voices[i].next();
}
audioBlockWrite(mix, mix);
}| Function | Description |
|---|---|
audioPartitionOffset() |
First voice index for this core. |
audioPartitionStride() |
Step between voices (cores interleave). |
audioIsFinalizerCore() |
True on the post-processing core (Core 0 on ESP32, Core 1 on RP2040). |
setAudioPostProcessCallback(fn) |
Register a master effects callback that runs on the combined mix once per frame. |
| Function | Description |
|---|---|
audioLoop() |
Service Pico cooperative jobs. Required in loop() on Pico; harmless no-op on Pico 2. |
| Function | Description |
|---|---|
mtof(midi) |
MIDI note to frequency (Hz). |
ftom(freq) |
Frequency (Hz) to MIDI note. |
clip16(val) |
Clamp to 16-bit signed range. |
panLeft(pos) |
Constant-power left gain. 0.0 = full left, 1.0 = full right. |
panRight(pos) |
Constant-power right gain. |
bpmToMs(bpm) |
BPM to milliseconds per beat. |
rand(max) |
Fast random integer 0 to max-1. |
audioRand(max) |
ISR-safe random integer. |
audioFrameCount() |
Global audio frame counter (advances once per output frame). |
Band-limited oscillator with FM, morphing, spread, and phase modulation.
Osc osc;
osc.sinGen(); // Allocate and fill sine table
osc.sawGen(); // Band-limited sawtooth
osc.sqrGen(); // Band-limited square
osc.triGen(); // Band-limited triangle
osc.noiseGen(); // White noise
osc.brownNoiseGen(); // Brown noise
osc.pinkNoiseGen(); // Pink noise
osc.crackleGen(); // Sparse impulses
osc.pulseGen(0.3); // Band-limited pulse, duty 0.0-1.0WaveTable sharedSine;
Osc carrier;
Osc modulator;
void setup() {
sharedSine.sinGen();
carrier.setTable(sharedSine);
modulator.setTable(sharedSine);
}| Function | Description |
|---|---|
setFreq(hz) |
Set frequency in Hz. |
setPitch(midi) |
Set frequency from MIDI note. |
setPhase(0.0-1.0) |
Set phase position. |
setSpread(amount) |
Detuning thickness (e.g. 0.01). |
setPulseWidth(0.05-0.95) |
PWM duty cycle. |
setCMRatio(ratio) |
C:M ratio for FM anti-aliasing depth cap. |
disableAntiAlias() |
Disable FM depth cap (for feedback FM, intentional aliasing). |
next() |
Get next sample (fast). |
next2() |
Get next sample (interpolated, higher quality). |
| Function | Description |
|---|---|
phMod(modulator, index) |
Phase modulation (FM). Pass int16_t value. |
phMod(modOsc, index) |
FM with atomically paired carrier+modulator advance (dual-core safe). |
phModInt(modulator, scaledIndex) |
Integer FM (faster). |
phModInt(modOsc, scaledIndex) |
Integer FM, dual-core safe paired advance. |
ringMod(audioIn) |
Ring modulation. |
feedback(index) |
Self-feedback FM. |
nextMorph(table, amount) |
Crossfade between current and target table. |
nextWTrans(table, windowSize, dual, invert) |
Window transform. |
Dual-core FM rule: Always use the Osc& overloads (phMod(osc, ...)) instead of calling .next() separately to avoid race conditions.
Attack-Hold-Decay-Sustain-Release envelope with millisecond timing.
Env env;
env.setAttack(10);
env.setHold(0);
env.setDecay(100);
env.setSustain(0.5);
env.setRelease(200);
env.setMaxLevel(1.0); // 0.0-1.0 peak gain| Function | Description |
|---|---|
start() |
Trigger the envelope. |
startRelease() |
Begin release phase (note off). |
getValue() |
Get current level (call per sample in audioUpdate). |
next() |
Same as getValue(). |
setAttack(ms) |
Attack time in ms. |
setHold(ms) |
Hold time in ms. |
setDecay(ms) |
Decay time in ms. |
setSustain(0.0-1.0) |
Sustain level. |
setRelease(ms) |
Release time in ms. |
setMaxLevel(0.0-1.0) |
Peak level (gain control). |
setAttackCurve(0.0-1.0) |
0.0 = linear, 0.5 = gentle (default), 1.0 = quadratic. |
setResetOnStart(bool) |
Each trigger begins from zero. |
setResetTransition(ms) |
Glide to zero before attack when reset enabled. |
setDecayRepeats(n) |
Repeat decay phase (claps, guiro). |
Pattern: Trigger in loop(), read getValue() per sample in audioUpdate().
Multi-mode resonant filter with simultaneous LPF, HPF, BPF, and Notch.
Safe frequency range: 40 Hz to ~21% of sample rate (~9200 Hz at 44.1 kHz).
SVF svf;
svf.setFreq(1000); // Hz
svf.setRes(0.8); // 0.3-0.84
int16_t lp = svf.nextLPF(input);
int16_t hp = svf.nextHPF(input);
int16_t bp = svf.nextBPF(input);
int16_t notch = svf.nextNotch(input);
int16_t mix = svf.nextFiltMix(input, 0.5); // LPF-BPF-HPF crossfade
svf.reset(); // Clear state64-bit math with gain compensation at high resonance. Higher CPU than SVF.
SVF2 svf2;
svf2.setFreq(1000);
svf2.setRes(0.8); // 0.01-1.0, gain-compensated
int16_t lp = svf2.nextLPF(input);
svf2.reset();4-pole lowpass with tanh saturation. Warmer character than SVF.
Bob bob;
bob.setFreq(1000);
bob.setRes(0.8); // 0.0-1.0
int16_t out = bob.next(input);Minimal CPU single-pole low/high pass.
EMA ema;
ema.setFreq(5000);
int16_t lp = ema.nextLPF(input);
int16_t hp = ema.nextHPF(input);Audio delay with feedback and filtering.
Del del;
del.setMaxDelayTime(500); // ms, allocates buffer
del.setTime(200); // ms
del.setLevel(0.8); // 0.0-1.0
del.setFeedback(true);
del.setFeedbackLevel(0.5);
del.setFiltered(2); // 0-4, higher = duller
int16_t out = del.next(input);
int16_t read = del.read(); // Read without writing
del.write(sample); // Write without readingAnalog BBD / tape delay emulation with pitch-shift artefact when delay time changes.
BBD bbd;
bbd.setTime(200); // ms
bbd.setScanRate(1.0); // Clock rate multiplier
bbd.setLevel(0.8);
bbd.setDelayMix(0.5); // 0=dry, 1=wet
bbd.setFeedback(true);
bbd.setFeedbackLevel(0.5);
bbd.setFiltered(2); // 0-4
int16_t out = bbd.next(input);FX fx;
int16_t out = fx.softClip(sample, 3.0); // Tube-style
int16_t out = fx.softClipAtan(sample, 3.0); // Warm atan
int16_t out = fx.softClipCubic(sample, 3.0); // Bright
int16_t out = fx.softClipTanh(sample, 3.0); // Balanced
int16_t out = fx.waveFold(sample, 2.0); // Wave folding
int16_t out = fx.overdrive(sample, 2.0); // Filtered overdrive
int16_t out = fx.bitCrush(sample, 8); // Bit depth 1-16int16_t out = fx.softLimit(sample); // Linear below 85% full scale
int16_t out = fx.softLimit(sample, 0.9f); // Custom thresholdfx.initReverbSafe(); // Call in setup()
fx.setReverbLength(0.8); // 0.0-1.0
fx.setReverbMix(0.3); // Wet 0.0-1.0
fx.setDampening(0.3); // HF absorption
fx.setReverbSize(4.0); // Memory multiplier
int16_t mono = fx.reverb(input);
fx.reverbStereo(inL, inR, outL, outR);
fx.reverbStereoInterp(inL, inR, outL, outR); // Half-rate CPU saverFor dual-core: use setAudioPostProcessCallback() to run reverb on the combined mix.
fx.setChorusMix(0.5); // Dry/wet 0.0-1.0
fx.setChorusDepth(0.5); // Internal balance
fx.setChorusWidth(0.3); // LFO pitch depth
fx.setChorusRate(0.5); // LFO Hz
fx.setChorusFeedback(0.3); // 0.0-1.0
fx.setChorusDelayTime(30); // Base delay ms (20-40)
fx.setChorusStereoDetune(0.6); // L/R rate difference
fx.setChorusSpread(0.5); // Stereo width 0.0-1.0
int16_t mono = fx.chorus(input);
fx.chorusStereo(inL, inR, outL, outR);fx.setCompression(threshold, ratio, attack, release); // Unity makeup
fx.setCompression(threshold, ratio, attack, release, makeup); // Explicit makeup
int32_t mono = fx.compression(sample);
fx.compressionStereo(inL, inR, outL, outR);fx.setShapeTableSoftClip(5.0);
int16_t out = fx.waveShaper(input, 0.8);fx.smooth(sample, 0.1);
fx.smoothStereo(inL, inR, outL, outR, 0.1);Cross-core-safe fixed-point gain stage.
Gain gain;
gain.setLevel(768); // 0-1024
gain.setLevel(0.75f); // 0.0-1.0
int32_t out = gain.next(input);Mono/stereo sample playback with variable speed, looping, and granular features.
Samp samp;
samp.setTable(buffer, frameCount, sampleRate, numChannels);
samp.setStart(0);
samp.setEnd(10000);
samp.setSpeed(1.0);
samp.setFreq(880); // Pitch-based speed
samp.setBasePitch(69); // Reference MIDI pitch
samp.setPitch(72); // Playback pitch
samp.setLoopingOn();
samp.setReverse(true);
samp.setInterpolation(true);
samp.setEdgeFade(true); // Granular click reduction
samp.setNearZeroSmooth(true); // Reduce quantisation clicks
samp.start();
int16_t mono = samp.next();
bool playing = samp.nextStereo(leftOut, rightOut);#include "sample_adpcm.h" // Generated offline
Wav wav;
Samp samp;
samp.loadFromFlash(wav, SAMPLE_DATA, SAMPLE_DATA_SIZE);Karplus-Strong plucked string and digital waveguide.
Phys phys;
phys.setPluckPosition(0.5); // 0.0=read head, 1.0=write head
int16_t out = phys.pluck(noiseInput, 440.0, 0.99); // input, freq, depth
// Or with stored frequency:
phys.setPluckFreq(440.0f);
int16_t out = phys.pluck(noiseInput, 0.99);
phys.resetPluck();
// Digital waveguide:
int16_t out = phys.waveguide(noiseInput, 440.0, 0.99);Standalone Freeverb-style reverb (different character to FX.h reverb).
Verb verb;
verb.setHighQuality(true); // 8 combs + 4 allpass. Call before init().
verb.init(); // Allocate buffers. Call in setup().
verb.setRoomSize(0.8); // 0.0-1.0
verb.setDamping(0.3); // 0.0-1.0
verb.setWetMix(0.5);
verb.setStereoWidth(1.0);
int16_t mono = verb.next(input);
verb.nextStereo(inL, inR, outL, outR);Schroeder allpass for reverb diffusion and phase effects.
All all;
all.setDelayTime(50); // ms
all.setFeedbackLevel(0.7); // 0.0-1.0
int16_t out = all.next(input);Comb comb;
comb.setDelayTime(10); // ms
comb.setInputLevel(1.0);
comb.setFeedforwardLevel(0.7);
comb.setFeedbackLevel(0.5);
int16_t out = comb.next(input);int notes[] = {60, 64, 67, 72};
Arp arp(notes, 4, 2, ARP_UP_DOWN); // values, count, octaves, direction
arp.start();
int pitch = arp.next();
double ms = arp.calcStepDelta(120, 4); // BPM, subdivisionDirections: ARP_ORDER, ARP_UP, ARP_UP_DOWN, ARP_DOWN, ARP_RANDOM, ARP_RANDOM2.
int pattern[] = {60, 0, 64, 0, 67, 0, 72, 0};
Seq seq(pattern, 8, 4); // values, size, stepDiv
seq.setRandom(true);
seq.euclideanGen(100, 5, 0); // value, hits, rotate
seq.randWalkGen(60, 3, 48, 72); // start, maxDev, min, max
int val = seq.next();
double ms = Seq::calcStepDelta(120, 4, 2); // BPM, slice, divMIDI16 midi(rxPin, txPin); // ESP32 defaults: rx=37, tx=38
midi.sendNoteOn(channel, pitch, velocity);
midi.sendNoteOff(channel, pitch, velocity);
midi.sendControlChange(channel, cc, value);
midi.sendClock();
uint8_t status;
while ((status = midi.read()) != 0) {
int ch = midi.getChannel();
int d1 = midi.getData1();
int d2 = midi.getData2();
}
int16_t bpm = midi.clockToBpm();midi.beginClockTask(); // Start high-priority clock task
midi.setClockSendBpm(120.0); // Auto-send clock pulsesMic mic;
int16_t l = mic.nextLeft();
int16_t r = mic.nextRight();
mic.nextStereo(l, r);Call audioInputStart() after audioStart() on RP2040 and Teensy.
Sends/receives analogue sync pulses (Korg Volca, Pocket Operators).
Sync sync(outPin, inPin);
sync.setPPQN(2);
sync.setOutBpm(120.0f);
// In loop():
if (sync.pulseOnTime(millis())) sync.startPulse();
if (sync.pulseOffTime(millis())) sync.endPulse();
bool beat = sync.receivePulse(millis());Requires a ~2:1 voltage divider on output (3.3V -> ~1.4V).
TLV tlv(sdaPin, sclPin);
tlv.begin(SAMPLE_RATE); // Call after audioStart()Move I2S DIN from GPIO21 when using this codec (I2C conflict).
| Platform | BCLK | LRCLK | DOUT | DIN |
|---|---|---|---|---|
| ESP8266 | GPIO15 | GPIO2 | GPIO3 | - |
| ESP32 | GPIO16 | GPIO17 | GPIO18 | GPIO21 |
| RP2040 | GPIO16 | GPIO17 | GPIO18 | GPIO19 |
- Call
fx.initReverbSafe()and other heavy allocations insetup(), never inaudioUpdate(). - Avoid
Serial.print(),malloc(), andrand()inaudioUpdate(). - Use PSRAM for large buffers:
isPSRAMAvailable()checks availability. - SVF is stable up to ~21% of sample rate. Use SVF2 or Bob for high resonance.
- For dual-core polyphony, use
setAudioPostProcessCallback()for master reverb/chorus. - Pico requires
audioLoop()inloop().