Skip to content
Andrew Brown edited this page Aug 31, 2026 · 8 revisions

M16 Library API Reference

A 16-bit audio synthesis library for ESP8266, ESP32, RP2040/RP2350 (Pico), and Teensy 4.x using I2S DACs.


Quick Start

#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).


Core (M16.h)

Audio System

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().

Dual-Core Partition API

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.

Pico Family

Function Description
audioLoop() Service Pico cooperative jobs. Required in loop() on Pico; harmless no-op on Pico 2.

Utilities

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).

Osc.h — Wavetable Oscillator

Band-limited oscillator with FM, morphing, spread, and phase modulation.

Generating a Table

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.0

Sharing Tables

WaveTable sharedSine;
Osc carrier;
Osc modulator;

void setup() {
  sharedSine.sinGen();
  carrier.setTable(sharedSine);
  modulator.setTable(sharedSine);
}

Key Methods

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).

Modulation

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.


Env.h — Envelope Generator

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().


SVF.h — State Variable Filter

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 state

SVF2.h — Higher-Quality SVF

64-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();

Bob.h — Moog Ladder Filter

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);

EMA.h — Simple IIR Filter

Minimal CPU single-pole low/high pass.

EMA ema;
ema.setFreq(5000);
int16_t lp = ema.nextLPF(input);
int16_t hp = ema.nextHPF(input);

Del.h — Delay Line

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 reading

BBD.h — Bucket Brigade Delay

Analog 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.h — Effects Processor

Distortion

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-16

Transparent Limiting

int16_t out = fx.softLimit(sample);           // Linear below 85% full scale
int16_t out = fx.softLimit(sample, 0.9f);     // Custom threshold

Reverb

fx.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 saver

For dual-core: use setAudioPostProcessCallback() to run reverb on the combined mix.

Chorus

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);

Compression

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);

Wave Shaping

fx.setShapeTableSoftClip(5.0);
int16_t out = fx.waveShaper(input, 0.8);

Smoothing

fx.smooth(sample, 0.1);
fx.smoothStereo(inL, inR, outL, outR, 0.1);

Gain.h — Level Control

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);

Samp.h — Sample Playback

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);

Flash-Stored Samples

#include "sample_adpcm.h" // Generated offline

Wav wav;
Samp samp;
samp.loadFromFlash(wav, SAMPLE_DATA, SAMPLE_DATA_SIZE);

Phys.h — Physical Modelling

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);

Verb.h — Freeverb Reverb

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);

All.h — Allpass Filter

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.h — Comb Filter

Comb comb;
comb.setDelayTime(10);          // ms
comb.setInputLevel(1.0);
comb.setFeedforwardLevel(0.7);
comb.setFeedbackLevel(0.5);
int16_t out = comb.next(input);

Arp.h — Arpeggiator

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, subdivision

Directions: ARP_ORDER, ARP_UP, ARP_UP_DOWN, ARP_DOWN, ARP_RANDOM, ARP_RANDOM2.

Seq.h — Step Sequencer

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, div

MIDI16.h — MIDI I/O

MIDI16 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();

ESP32 Clock Task (optional, recommended)

midi.beginClockTask();       // Start high-priority clock task
midi.setClockSendBpm(120.0); // Auto-send clock pulses

Mic.h — Audio Input

Mic mic;
int16_t l = mic.nextLeft();
int16_t r = mic.nextRight();
mic.nextStereo(l, r);

Call audioInputStart() after audioStart() on RP2040 and Teensy.


Sync.h — GPIO Sync Clock

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.h — TLV320AIC3104 Codec

TLV tlv(sdaPin, sclPin);
tlv.begin(SAMPLE_RATE); // Call after audioStart()

Move I2S DIN from GPIO21 when using this codec (I2C conflict).


Default I2S Pins

Platform BCLK LRCLK DOUT DIN
ESP8266 GPIO15 GPIO2 GPIO3 -
ESP32 GPIO16 GPIO17 GPIO18 GPIO21
RP2040 GPIO16 GPIO17 GPIO18 GPIO19

Tips

  • Call fx.initReverbSafe() and other heavy allocations in setup(), never in audioUpdate().
  • Avoid Serial.print(), malloc(), and rand() in audioUpdate().
  • 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() in loop().

Clone this wiki locally