Skip to content

Turning a macro changes the notes already sounding - #172

Merged
bdbarnett merged 2 commits into
mainfrom
fix/166-live-macros
Oct 15, 2026
Merged

bdbarnett merged 2 commits into
mainfrom
fix/166-live-macros

Conversation

@bdbarnett

Copy link
Copy Markdown
Contributor

Hold a chord and turn Cutoff: you now hear the sweep. Until now every instrument read its macros when a note started, so set_macro() reached only the next note, and a filter sweep over a held note, the most basic synth gesture, wasn't possible.

Fixes #166.

What you'll hear

On all 43 melodic instruments, the continuous macros reach the notes that are already sounding, on their next audio block: volume, filter cutoffs and resonance, layer mixes, filter-envelope amounts, brightness and tone controls, drive, and modulation depths and rates. That is 246 macros in all.

A macro set before a note sounds exactly as it did before this change. Every instrument, every macro at 0, 64 and 127, renders byte-identical to the previous release on CPython, MicroPython and CircuitPython.

What still waits for the next note, and why

  • Envelope times and levels, glide, voice and unison modes, patches. These describe how a note is struck, so they belong to the next one.
  • Pitch: detune, master tune, tuning, oscillator ratios. A note's frequency is fixed when it starts. Moving it live would mean routing it through pitch bend, which would change the sound even with the knob set before the note.
  • Anything built into a waveform when the note starts: pulse-width tables, ring-modulation depth tables, waveform and wavetable choices, string models (Karplus pluck position and damping).
  • Tremolo and ring-mod rates (Rhodes, Wurlitzer, CP-70, B3 Leslie, CS-80, DX7): synthio takes a ring frequency as a plain number, not something that can move.
  • Layers that are off when the note starts. A layer whose mix is at zero isn't sounded at all, to save the voice. Its mix macro reaches a held note while that layer is sounding, but turning it up from zero brings it in on the next note. The same goes for a vibrato or wah whose depth was at zero when the note started: its rate and depth follow the knob only once it is there.
  • Drum machines (all twelve kits): hits are short, and every macro already reaches the next hit.

Per instrument

Instrument Live on held notes Next note
andromeda Volume, Filter 1/2 Cutoff, Filter Mix, Resonance 1/2, Env 1 -> F1/F2 Unison Detune, envelopes, Master Tune
arp2600 Volume, VCF Cutoff, Resonance, FM Amount*, Reverb Mix* (level; tail length is set at the note) Osc 2/3 Detune, envelopes, Master Tune
b3 Volume, Drawbars 16/8/4/2*, Perc Level*, Key Click*, Overdrive Perc Decay, Leslie Fast (ring rate), Master Tune
clavinet Volume, Pickup Mix, Brilliance, Wah Depth*, Wah Rate* Mute Level, envelopes, Master Tune
cp70 Volume, Hammer Strike, String Body, Brilliance Tremolo Rate (ring rate), Tremolo Depth (table), Chorus (detune), Decay, Master Tune
cs80 Volume, VCF Cutoff, Resonance, HPF Cutoff, Layer II Mix, Brilliance Ring Mod Speed (ring rate), Ring Mod Depth (table), Poly AT Depth (next pressure message), envelopes
cz101 Volume, PD Env Depth, Resonance, Vibrato Rate*, Vibrato Depth* PD Attack/Decay, envelopes, Master Tune
d50 Volume, Transient Mix*, Synth Mix*, Cutoff, Resonance Transient Tune, Chorus (detune), envelopes, Master Tune
dx7 Volume, FM Amount, Feedback, Alg Mix, Brightness, Vibrato Depth*, Vibrato Rate* Mod Ratio, envelope times, Tremolo Depth (table), Tremolo Rate (ring rate), Master Tune
emulator2 Volume, Breath Noise*, Tone Brilliance, Filter Env, Vibrato Rate*, Vibrato Depth* envelopes, Master Tune
fairlight Volume, Bitcrush Approx (crushed layer*), Filter Env Patch Select, envelopes, Pitch Env Depth, Master Tune
farfisa Volume, Bass, Strings, Flute, Oboe, Trumpet (each*), Vibrato Rate*, Vibrato Depth* Multi-Tone Booster (switches tables and filter mode), Master Tune
fs1r Volume, FM Index*, Brightness Vowel A/B, Morph Speed, envelopes, Master Tune
jp8000 Volume, Cutoff, Resonance, Supersaw Mix, Env Depth, HPF Cutoff Supersaw Detune, Chorus (stereo spread), envelopes, Master Tune
juno106 Volume, Cutoff, Resonance, Sub Level, Noise Level*, Chorus Rate*, HPF, LFO Rate, Env Depth Chorus Depth (also detune and on/off), PWM Amount (table), envelopes
jupiter8 Volume, Cutoff, Resonance, Env Depth, Cross Mod*, LFO Rate, HPF Cutoff Unison, PW (table), envelopes
k2600 Volume, Layer 1-4 Mix*, Shimmer Delay*, Master Cutoff, Master Resonance, Mod Wheel*, Modulation Rate*, Modulation Depth* envelopes, Master Tune
karplus Volume, Body Resonance Pluck Position, String Damping (string table), Pick Hardness (gone in 70 ms), Decay, Master Tune
mellotron Volume, Tone, Flutter Rate*, Flutter Depth*, Tape Hiss Attack, Release, Master Tune
microwave Volume, Wavetable Pos, Cutoff, Resonance, Env to WT, Env to Filter Filter Attack, envelopes, Master Tune
minimoog Volume, Cutoff, Resonance, Env Amount, Noise Mix*, Overdrive Glide, Osc2/Osc3 Detune, envelopes
ms20 Volume, HPF Cutoff, HPF Peak, LPF Cutoff, LPF Peak, EG2 Sweep, Noise Level* Osc2 Pitch, Ring Mod (table), envelopes, Master Tune
ms2000 Volume, Cutoff, Resonance, Mod Seq Rate*, Mod Seq Depth*, Distortion* Osc Waveform, envelopes, Master Tune
music_easel Volume, Timbre Wavefold, FM Index*, Mod Osc Freq* LPG Strike, LPG Decay, envelopes, Master Tune
nord_lead Volume, Cutoff, Resonance, Morph 1-3 FM Amount, Osc Sync, Morph 4 (oscillator ratio), envelopes
obxa Volume, Cutoff, Resonance, Env Depth Detune Spread, envelopes, Master Tune
odyssey Volume, Cutoff, Resonance, LFO Rate, Env Sweep, HPF Cutoff Ring Mod (table), Osc 2 Detune, Sync, Glide, envelopes, Master Tune
pianet Volume, Mellow Tone, Vibrato Rate*, Vibrato Depth* Pluck Attack (gone in 0.1 s), Decay Time, envelopes, Master Tune
polysix Volume, Sub-Osc Level*, Cutoff, Resonance, Ensemble Depth*, PWM Rate envelopes, Master Tune
ppg_wave Volume, Wavetable Index, Cutoff, Resonance, Env Amount envelopes, Detune, Master Tune
prophet5 Volume, Cutoff, Resonance, Env Amount, LFO Rate, LFO Depth Poly Mod, Osc2 Detune, Sync, envelopes
prophet_vs Volume, Joystick X/Y*, Cutoff, Resonance, Env Amount, Chorus* envelopes, Master Tune
rhodes Volume, Tine Level, Body Level, Overdrive, Tone Tremolo Rate (ring rate), Tremolo Depth (table), envelopes, Key-Off Noise, Master Tune
sh101 Volume, Sub-Osc Level, Cutoff, Resonance, Env Depth Pulse Width (table), Fast Decay, Master Tune
solina Volume, Violin/Viola/Cello Mix*, Chorus Depth Attack, Release, Crescendo, Master Tune
taurus Volume, Cutoff, Resonance, Env Mod Osc B Detune, Beat Freq, Glide, envelopes, Master Tune
tb303 Volume, Cutoff, Resonance, Env Mod, Accent (level and envelope depth), Overdrive Tuning, Decay, Master Tune; Accent's shorter decay is set at the note
virus Volume, Sub Osc*, Cutoff, Resonance, Distortion, Env Depth Hypersaw Detune, envelopes, Master Tune
vl1 Volume, Breath Pressure, Embouchure, Growl Rate*, Growl Depth* envelopes, Master Tune
vox_continental Volume, Drawbars 16/8/4/IV*, Vibrato Rate*, Vibrato Depth*, Brilliance Master Tune
vp330 Volume, Male/Female Choir*, Bass*, Brilliance*, Chorus Depth*, Vibrato Rate*, Vibrato Depth*, Formant Shift Attack, Release, Master Tune
wasp Volume, Cutoff, Resonance, Noise Level* Filter Mode, envelopes, Master Tune
wurlitzer Volume, Bite, Bark Tremolo Rate (ring rate), Tremolo Depth (table), envelopes, Master Tune

* Live while its layer, LFO or wah is sounding; one that was off when the note started comes in on the next note.

Where a filter releases through its own sweep after note-off (arp2600, ms20, prophet_vs), the release follows Cutoff and Resonance too. Breath on the VL1 now stays live after pressure, where pressure used to freeze the filter for the rest of the note.

How it works

_support.live(value) gives a macro one shared synthio.Math block. Every note wires that block into its filter frequency, Q, amplitude or LFO, and the macro handler writes block.a. gain() multiplies blocks for amplitudes that combine macros with velocity. note_is_silent(), which voice stealing uses, now reads a live amplitude's current value.

Tests

tests/test_live_macros.py holds a note on each instrument, moves each live macro to 0 and to 127, and asserts the held note comes out differently. Against the previous code it fails 244 of its 246 cases; the two that already passed are Mellotron's Volume and Tape Hiss, whose shared hiss note already followed the knob.

The instrument, API, polyphony, scheduling, sequencer, tempo-follow, shared-circuit and reset tests pass, as does tools/validate_api.py.

…Env Amount, Noise Mix and Overdrive

Each continuous macro is one shared synthio block that every note reads, so a
macro change reaches the notes already sounding (#166). _support gains live(),
gain() and block_value(); note_is_silent() reads a live amplitude's current
value.
…struments

Volume, filter cutoff and resonance, layer mixes, envelope amounts and
modulation depths and rates are shared blocks every note reads, so a macro
change reaches a held chord. Envelope times, glide, pitch and anything
baked into a waveform stay next-note. Renders with macros set before the
note are byte-identical to before on CPython, MicroPython and CircuitPython.
tests/test_live_macros.py holds a note per live macro and asserts it moves.
@bdbarnett
bdbarnett merged commit 5699366 into main Oct 15, 2026
6 checks passed
@bdbarnett
bdbarnett deleted the fix/166-live-macros branch October 15, 2026 20:04
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.

Turning a macro doesn't change notes that are already sounding

1 participant