Skip to content

audiosampler: a resonant low-pass, a modulation envelope and two LFOs per voice - #337

Merged
bdbarnett merged 2 commits into
mainfrom
sampler/phase5
Oct 29, 2026
Merged

bdbarnett merged 2 commits into
mainfrom
sampler/phase5

Conversation

@bdbarnett

Copy link
Copy Markdown
Contributor

A sampler voice can now play the rest of what a SoundFont region says about a note:

  • a two-pole resonant low-pass (cutoff in Hz, resonance in dB),
  • a DAHDSR modulation envelope driving pitch and cutoff (mod_*),
  • a vibrato LFO on pitch (vibrato_delay, vibrato_rate, vibrato_depth),
  • a modulation LFO on pitch, cutoff and volume (lfo_*).

They're new keywords on play(), on CPython, MicroPython and CircuitPython. cutoff and vibrato_depth are also live properties, so a mod wheel or a velocity curve can move them on a sounding note.

How it renders

A voice that sets none of these takes the old path and renders the same bits; sampler_probe.py's output matched main's before its new section was added. A voice that does set them renders in 32-frame chunks. Once a chunk it works out its pitch, its filter coefficients and its volume, then runs the chunk through a state-variable low-pass. The coefficients come from a polynomial tangent and audiodsp_exp2_poly, so CPython and MicroPython print the same probe output.

What it costs

tests/parity/sampler_cost.py gives microseconds per 256-frame stereo block at 48 kHz, where the block lasts 5333 µs. filter is a 2 kHz low-pass with 6 dB of resonance. full adds the modulation envelope and both LFOs on everything they reach.

Voices CPython plain / filter / full ESP32-P4 plain before / plain / filter / full
32 32 / 56 / 65 1383 / 1384 / 2626 / 3465
48 47 / 83 / 95 2039 / 2040 / 3904 / 5179
64 62 / 102 / 122 2745 / 2804 / 5290 / 6970

On the P4, plain before is the same firmware built from main. At 64 voices the plain path reads 2% slower, which is about the spread between two images of the same code: main's sampler read 2704 µs at 64 voices in a bigger image. The voice struct grows from 112 to 256 bytes on a 64-bit desktop. The numbers are in docs/how-it-works/upstream-diff.md.

Tests

tests/test_cpython_audiosampler_modulation.py has ten traits, each with its bar:

  • An open filter renders the same bits as no filter.
  • The response at and below the cutoff is within 0.3 dB of the design, and two octaves up it is 21 to 27 dB down.
  • The envelope sweeps pitch and cutoff.
  • The LFOs' rate, depth and delay are measured.
  • The two live properties take effect.
  • 32, 48 and 64 filtered voices fit inside a block.

Three planted faults each failed tests:

  • resonance at half its dB failed the response trait,
  • a triangle at twice its phase failed the LFO traits,
  • the envelope at half speed failed the sweep.

The CircuitPython binding is written but hasn't been built here.

… per voice

A voice can now play the rest of what a SoundFont region says about a note:
a two-pole state-variable low-pass with resonance, a DAHDSR modulation
envelope on pitch and cutoff, a vibrato LFO on pitch, and a modulation LFO on
pitch, cutoff and volume. cutoff and vibrato_depth are live properties, so a
mod wheel or a velocity curve can move them on a sounding note.

They are worked out every 32 frames from polynomial approximations, so every
target renders the same bits. A voice that uses none of them takes the old
path and renders exactly as before, at the same cost.

Measured in tests/test_cpython_audiosampler_modulation.py (response at and
below the cutoff, the envelope's sweep, the LFOs' rate, depth and delay, the
live properties, 64 filtered voices inside a block), with planted faults that
fail them; timed by tests/parity/sampler_cost.py on any target.
@bdbarnett
bdbarnett merged commit 0c0824e into main Oct 29, 2026
31 checks passed
@bdbarnett
bdbarnett deleted the sampler/phase5 branch October 29, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant