Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
5f92313
The lifecycle matrix on single audiodsp nodes
bdbarnett Oct 13, 2026
e793fad
test_node_matrix: drop an unused import
bdbarnett Oct 13, 2026
644b68a
The effects kit: tail, plant validity, claims, readback and arguments…
bdbarnett Oct 13, 2026
d8e96a6
The kit's own proof and its page
bdbarnett Oct 13, 2026
f8e1f5b
Kit: read sources as UTF-8 on every platform
bdbarnett Oct 13, 2026
3cb1075
Node matrix: the comparator and all-pass the Octaver stands on
bdbarnett Oct 13, 2026
22bf4c6
Merge remote-tracking branch 'origin/effects-p6/node-matrix' into HEAD
bdbarnett Oct 13, 2026
626b7e6
Node matrix: the shimmer loop's and Echo's by-design cells
bdbarnett Oct 13, 2026
40ec61a
StereoWidener (rebuilt, Phase 6): Haas and Mid/Side on Width, Delay a…
bdbarnett Oct 13, 2026
7a057e0
Node matrix: the pitch nodes' by-design cells
bdbarnett Oct 13, 2026
36f4ed9
Merge remote-tracking branch 'origin/effects-p6/node-matrix' into HEAD
bdbarnett Oct 13, 2026
a95af9f
One path for a patch and a restore: Component._apply_positions
bdbarnett Oct 13, 2026
265982d
Rack (rebuilt): the serial chain on the contract's base
bdbarnett Oct 13, 2026
056318f
Merge remote-tracking branch 'origin/effects/phase6' into effects-p6/…
bdbarnett Oct 13, 2026
93dbb9b
StereoWidener: patches play their Delay on every instance, no stale b…
bdbarnett Oct 13, 2026
4cdcd06
AirSpace: a tape echo sent into a chamber, rebuilt from its Phase 6 card
bdbarnett Oct 13, 2026
3f890cc
AUDIODSP_PIN: move to audiodsp v0.6.6rc1 (30cbbf6)
bdbarnett Oct 13, 2026
82e2724
MultiTapDelay: restate the Mixer-tail plant (audiodsp#229 made it ine…
bdbarnett Oct 13, 2026
396de63
StereoWidener: reset() re-plays the Haas voices without get_buffer; a…
bdbarnett Oct 13, 2026
9fbe2e8
Rack lifecycle runs on a chain without Vibrato and Fuzz, so the E1 P4…
bdbarnett Oct 13, 2026
884d176
StereoWidener: wire the voices the same way without get_buffer
bdbarnett Oct 13, 2026
8e59787
Effects kit: refused-call cells in args and a nogb check (get_buffer …
bdbarnett Oct 13, 2026
2b73862
Merge remote-tracking branch 'origin/effects-p6/kit2' into effects-p6…
bdbarnett Oct 13, 2026
baa0c90
Octaver: the Phase 6 rebuild, its trait tests, plants and claims
bdbarnett Oct 13, 2026
682abe3
Merge remote-tracking branch 'origin/effects-p6/kit2' into effects-p6…
bdbarnett Oct 13, 2026
029b317
AirSpace: declare the 22.05 kHz lifecycle cells and tie the family se…
bdbarnett Oct 13, 2026
54d2040
AirSpace: re-play the voices on a return from Mix 0 where get_buffer …
bdbarnett Oct 13, 2026
04dc203
DeEsser: restate the STATE plant that audiodsp#229 made inert; record…
bdbarnett Oct 13, 2026
f34b98e
Rack reset returns every pedal to exactly how the rack built it
bdbarnett Oct 13, 2026
fc9279c
Merge remote-tracking branch 'origin/effects-p6/kit2' into effects-p6…
bdbarnett Oct 13, 2026
8d6d771
Harmonizer: three pitch shifters at fixed intervals under the dry, it…
bdbarnett Oct 13, 2026
80557c7
Kit: nogb runs the lifecycle events, so it takes the lifecycle's options
bdbarnett Oct 13, 2026
d535b5a
Node matrix: declare the control-jump step on every node (the family'…
bdbarnett Oct 13, 2026
3f2c9ce
Merge remote-tracking branch 'origin/effects-p6/node-matrix' into HEAD
bdbarnett Oct 13, 2026
07bf68a
Merge remote-tracking branch 'origin/effects-p6/kit2' into effects-p6…
bdbarnett Oct 13, 2026
b17bdf3
Lifecycle matrix: CircuitPython's MultiTapDelay keeps upstream's stra…
bdbarnett Oct 13, 2026
914dc4e
Merge remote-tracking branch 'origin/effects/phase6' into effects-p6/…
bdbarnett Oct 13, 2026
f3d6849
PitchShifter (rebuilt, Phase 6): a splice shifter with Pitch, Fine, M…
bdbarnett Oct 13, 2026
ee14893
Merge remote-tracking branch 'origin/effects-p6/kit2' into effects-p6…
bdbarnett Oct 13, 2026
32c5a0e
Octaver: review fixes (Octave 2 on tails, ceiling, range, decay, wire)
bdbarnett Oct 13, 2026
310fa23
Harmonizer: declare the CircuitPython-only E10 cells (its PitchShift …
bdbarnett Oct 13, 2026
1d4658c
Lifecycle matrix: skip a cell that cannot finish on one interpreter; …
bdbarnett Oct 13, 2026
93b1a1d
Harmonizer T1: hold each solo channel to the input's at the voice's g…
bdbarnett Oct 13, 2026
14bc307
Merge remote-tracking branch 'origin/effects/phase6' into effects-p6/…
bdbarnett Oct 13, 2026
2dd4731
Merge remote-tracking branch 'origin/effects/phase6' into effects-p6/…
bdbarnett Oct 13, 2026
916341e
ShimmerHall (rebuilt, Phase 6): a pitch shifter around the rebuilt Re…
bdbarnett Oct 13, 2026
49d9dfa
Node matrix: CircuitPython's upstream Echo, pitch shifters and Mixer …
bdbarnett Oct 13, 2026
0a69a2f
Octaver: Octave 2 on its own flip-flop; voices armed quietly
bdbarnett Oct 13, 2026
71f1c39
PitchShifter: the stranded mark clears at the first wiring, a cleared…
bdbarnett Oct 13, 2026
7c3fd92
PitchShifter lifecycle: E10-reset and E10-mix0 P4 declared as Circuit…
bdbarnett Oct 13, 2026
cad9079
Harmonizer: the input ceiling covers every setting (-12.1 dBFS), with…
bdbarnett Oct 13, 2026
9d3622b
ShimmerHall: declare the hall's lifecycle P4 cells and the N1 step, a…
bdbarnett Oct 13, 2026
dcf8ffa
ShimmerHall tests: the ceiling test puts the stated ceiling back
bdbarnett Oct 13, 2026
a9012fd
Merge remote-tracking branch 'origin/effects-p6/kit2' into effects-p6…
bdbarnett Oct 13, 2026
4e357f8
Merge remote-tracking branch 'origin/effects-p6/pin-066rc1' into effe…
bdbarnett Oct 13, 2026
cd68fd1
Kit round 3: scoped DECLARED rows, pre-pull moves against a fresh ins…
bdbarnett Oct 13, 2026
d63bfa7
AirSpace: pull the mixer through a one-voice mixer where get_buffer i…
bdbarnett Oct 13, 2026
2a47e12
Merge pull request #134 from PyDevices/effects-p6/pin-066rc1
bdbarnett Oct 13, 2026
c1f3b38
Merge remote-tracking branch 'origin/effects-p6/stereowidener' into e…
bdbarnett Oct 13, 2026
3735f86
Merge remote-tracking branch 'origin/effects-p6/rack' into effects-p6…
bdbarnett Oct 13, 2026
c211db1
PitchShifter: levels set before the first pull hold from frame 0, the…
bdbarnett Oct 13, 2026
bc0559d
ShimmerHall tests: read the 48 kHz traits off a stereo render's left …
bdbarnett Oct 13, 2026
970e5f5
ShimmerHall tests: read the left channel without a slice step, for Mi…
bdbarnett Oct 13, 2026
d69b056
Octaver: dividers clock on ticks under Sensitivity, so a fading note …
bdbarnett Oct 13, 2026
ee5ebe3
ShimmerHall: declare only the macros whose E4 cells are red
bdbarnett Oct 13, 2026
a00bb96
Lifecycle PR: a return from Mix 0 is judged against a fresh instance …
bdbarnett Oct 13, 2026
f0ebdbb
Harmonizer: a voice turned off plays silence from the move on, not th…
bdbarnett Oct 13, 2026
ee6a5c7
Merge remote-tracking branch 'origin/effects/phase6' into effects-p6/…
bdbarnett Oct 13, 2026
712f482
ShimmerHall: skip E8 on CircuitPython (upstream PitchShift), disclose…
bdbarnett Oct 13, 2026
35fc556
StereoWidener: a rack's reset keeps the constructor's Delay
bdbarnett Oct 13, 2026
94191e3
Merge remote-tracking branch 'origin/effects-p6/octaver' into effects…
bdbarnett Oct 13, 2026
a7ec4d7
PitchShifter: a reset or a return from Mix 0 is new again, only a mov…
bdbarnett Oct 13, 2026
f8aebae
Merge remote-tracking branch 'origin/effects-p6/kit3' into effects-p6…
bdbarnett Oct 13, 2026
2a8c3a8
Harmonizer: name CircuitPython's own pitch shifter in the reset line,…
bdbarnett Oct 13, 2026
47dd60c
PitchShifter: a reset or a return from Mix 0 no longer hands the firs…
bdbarnett Oct 13, 2026
8a17b0d
Merge effects-p6/land-1 into effects-p6/pitchshifter
bdbarnett Oct 13, 2026
524c4fe
PitchShifter: patches and a rack's put-back share one refresh (_apply…
bdbarnett Oct 13, 2026
ae46a41
Octaver: a patch and a return from the wire start where a fresh insta…
bdbarnett Oct 13, 2026
31412a1
ShimmerHall: a reset, a return from Mix 0 and a Mix move before the f…
bdbarnett Oct 13, 2026
01c8887
Merge remote-tracking branch 'origin/effects-p6/land-1' into effects-…
bdbarnett Oct 13, 2026
f113cb0
Harmonizer: merge the landing branch; a rack's reset restores through…
bdbarnett Oct 13, 2026
3701a1e
Kit paperwork: a card's range row (1-3, Voice 1 ... Voice 3) names on…
bdbarnett Oct 13, 2026
bd6ef46
Merge remote-tracking branch 'origin/effects-p6/land-1' into effects-…
bdbarnett Oct 13, 2026
1801807
AirSpace: a whole-step check that takes inf and NaN
bdbarnett Oct 13, 2026
d565ebc
PitchShifter: the lifecycle and kit tables on the landing branch's fo…
bdbarnett Oct 13, 2026
9423287
Kit: declare PitchShifter's BIPOLAR Mix against its card (a recorded …
bdbarnett Oct 13, 2026
3e24f98
ShimmerHall: patches and a rack's put-back share one refresh (_apply_…
bdbarnett Oct 13, 2026
a1aaac0
ShimmerHall tests: the get_buffer-hidden cases cover a reset from Mix…
bdbarnett Oct 13, 2026
1eda46c
Curve tables: the Octaver's comparator is a step by design; the exemp…
bdbarnett Oct 13, 2026
ab0e3b5
Merge effects-p6/land-1 into effects-p6/airspace: AirSpace restores i…
bdbarnett Oct 13, 2026
3f3cf19
CHANGELOG: tidy the merge
bdbarnett Oct 13, 2026
2f25cae
ShimmerHall: the fold-back and the splice's cancelled line told in a …
bdbarnett Oct 13, 2026
e9402ae
Merge branch 'effects-p6/pitchshifter' into effects-p6/land-2
bdbarnett Oct 13, 2026
e6113b0
Merge branch 'effects-p6/harmonizer' into effects-p6/land-2
bdbarnett Oct 13, 2026
478ff1b
ShimmerHall tests: the fold-back figures sweep the note and name a si…
bdbarnett Oct 13, 2026
d147a0d
AirSpace: the input ceiling is -17 dBFS, measured at Mix 1 over 12 s
bdbarnett Oct 13, 2026
002f4c3
test_kit: write the fixture card and claims module as UTF-8, as the c…
bdbarnett Oct 13, 2026
94e1a92
Merge branch 'effects-p6/land-1' into effects-p6/land-2
bdbarnett Oct 13, 2026
854c14e
AirSpace: the docstring names what the -17 dBFS ceiling covers, and t…
bdbarnett Oct 13, 2026
7d03ff3
Merge pull request #135 from PyDevices/effects-p6/land-1
bdbarnett Oct 13, 2026
839a5be
Merge remote-tracking branch 'origin/effects/phase6' into effects-p6/…
bdbarnett Oct 13, 2026
49dc74f
ShimmerHall: Slow Rise's Damping from step 102 to 86 so the climb sta…
bdbarnett Oct 13, 2026
a2d130f
Merge branch 'effects-p6/airspace' into effects-p6/land-2
bdbarnett Oct 13, 2026
8886b31
ShimmerHall: say the Bloom patches cannot keep up on an ESP32-S3 and …
bdbarnett Oct 13, 2026
ee4e2f8
Merge branch 'effects-p6/shimmerhall' into effects-p6/land-3
bdbarnett Oct 13, 2026
daf9bfa
ShimmerHall: the defaults become Octave Rise, Octave Bloom in Rise at…
bdbarnett Oct 13, 2026
7f3e89b
Merge pull request #136 from PyDevices/effects-p6/land-2
bdbarnett Oct 13, 2026
72a68a9
StereoWidener, Rack, Octaver, PitchShifter, Harmonizer, AirSpace and …
bdbarnett Oct 13, 2026
c8385e2
Merge commit 'daf9bfa' into effects-p6/final
bdbarnett Oct 13, 2026
a426ade
Merge pull request #137 from PyDevices/effects-p6/land-3
bdbarnett Oct 13, 2026
75110a2
Merge remote-tracking branch 'origin/effects/phase6' into effects-p6/…
bdbarnett Oct 13, 2026
d529f3b
Lifecycle: ShimmerHall's rows for the Rise defaults
bdbarnett Oct 13, 2026
1da0ebc
Merge branch 'effects-p6/shimmerhall' into effects-p6/final
bdbarnett Oct 13, 2026
577532d
Merge pull request #140 from PyDevices/effects-p6/final
bdbarnett Oct 13, 2026
f590152
Merge remote-tracking branch 'origin/main' into effects/phase6
bdbarnett Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion AUDIODSP_PIN
Original file line number Diff line number Diff line change
Expand Up @@ -321,5 +321,29 @@
# plants that #211 made inert; and the lifecycle matrix's E8-dry event,
# retired because a native source that runs dry has ended.
#
# 2026-10-12: eb2d20d -> v0.6.6rc1 (30cbbf6), a pre-release of the next
# patch, for the effects program's Phase 6; TestPyPI only, so CI's
# `git+...@v0.6.6rc1` install is the one that reaches it. The pyproject
# floors stay at the last final release: the pin is not the floor. What
# moved beneath the components, by `git log eb2d20d..v0.6.6rc1 -- src`:
# audiodelays.Echo and MultiTapDelay start a line shortened under the write
# head at its top, so a Time brought back after a shorter one plays nothing
# from before, and the CPython Echo clears past a shorter line as the native
# one does (audiodsp#241); Echo lets go of a looped source that comes back
# empty instead of hanging the pull (#242); PitchShift and
# GranularPitchShift start again from a new node's state on a reset (#240)
# and return from a pull when the source has nothing to give (#243), and
# PitchShift gains upstream's freeze (#222); Convolver's load() keeps the
# audio in flight and crossfades the block being played, as synthesize()
# already did (#237); a Mixer's host reset keeps every voice where it was
# (#229); a set() on FeedbackDelay, Tank, Dynamics, Waveshaper, Ladder and
# Bank is staged off the lock and lands whole (#233); an audiopump swap
# that outlasts the ring fades out and back in (#234); Tank's filters and
# modulation brought back from 0 start from what is playing (#227); a mono
# SplitterTap's buffer is the tap's own on CPython too (#228); FeedbackDelay
# takes its wow and pitch coast rate from what played, and the CPython Echo
# holds its line still at mix 0 (#226); audiovorbis (#238, #239); and an
# fmod-free angle reduction for the RP2040 (#225).
#
# <ref> <commit> -- ref is what CI installs (git+https://...@<ref>)
eb2d20daa7f3be2965f8f641b3fdf2d9a55aca9c eb2d20daa7f3be2965f8f641b3fdf2d9a55aca9c
v0.6.6rc1 30cbbf619751ab6ff7c40d99336907af0a081fa6
85 changes: 80 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,78 @@ there, and are recorded in its changelog.

### Added

- **`ShimmerHall` (rebuilt, Phase 6; adopted 2026-10-13):** a pitch shifter around a hall, in
two characters. Bloom sends the shifted copy into the hall once, Bloom
after you play, on `audiodelays.PitchShift`; Rise puts the shift inside
an `audioecho.FeedbackDelay` loop (`loop_semitones`), so each pass comes
back shifted again until Damping darkens it away. Nine macros (Character,
Shimmer, Rise, Bloom, Interval, Decay, Size, Damping, Mix) and seven
patches replace the old Shimmer / Echo / Space / Tone surface and its
constructor arguments. The defaults, patch 0 "Octave Rise", are Rise at
Rise 0: one octave, once, with the hall starting at once, which an
ESP32-S3 keeps up with where the Bloom patches (Octave Bloom, Short
Stagger, Fifth Halo) do not. The hall is the rebuilt `Reverb`. The dry passes
untouched up to Mix 1, Mix 0 is a byte-exact wire, and the input ceiling
is -24 dBFS. It is what `audioeffects.ShimmerHall` and `create()` now
serve. It lives in `lib/audioeffects/rebuilt/shimmerhall.py`, beside the
old class in `rack.py`, until it is promoted.
- **`AirSpace` (rebuilt, Phase 6; adopted 2026-10-13):** a tape echo sent
into a chamber, with the dry untouched beside it, in
`audioeffects/rebuilt/airspace.py`. Eleven macros (Time, Feedback, Glide,
Drive, Loop Top, Loop Bass, Top Cut, Low Cut, Decay, Damping, Mix) and
seven patches replace the old rack's Space, Echo and Tone; the loop
darkens each repeat, Top Cut darkens the send once, Feedback stretches the
chamber's tail, and a Time move bends the repeats' pitch. The chamber is
the rebuilt `Reverb`, and `reset()` renders exactly as a fresh instance.
It is what `audioeffects.AirSpace` and `create()` now serve; the old
`rack.AirSpace` stays beside it until the rebuild is promoted.
- **`Harmonizer` (rebuilt, Phase 6; adopted 2026-10-13):** three pitch shifters at fixed
intervals under the dry, after three Eventide H910s played from one
keyboard. Each voice is its own `audiodelays.PitchShift` on its own
splitter tap and lands on the nearest step the shifter can play; a
voice at its Level's bottom step is off and its shifter is not pulled.
Ten controls (Pitch, Voice 1 to 3, Level 1 to 3, Detune, Spread, Mix),
seven patches including a lean two-voice one, and a `window_ms` option.
It drops the old class's `intervals=` and `level=` options, and is what
`audioeffects.Harmonizer` and `create()` now serve.
- **`PitchShifter` (rebuilt, Phase 6; adopted 2026-10-13):** a fixed-interval splice shifter
in the manner of the first harmonizers, on a Pitch, Fine, Mix, Delay and
Feedback surface. Pitch moves in whole semitones an octave either way and
Fine detunes by up to 50 cents; the dry passes untouched up to Mix 1;
Delay holds the shifted voice back up to 112.5 ms; and with Feedback up
every repeat is shifted again, so a fifth climbs a fifth at a time.
`window_ms` (15 to 60, 30 by default) sets the splice window at
construction. The old `semitones=` option is gone: Pitch and Fine
replace it. It is what `audioeffects.PitchShifter` and `create()` now
serve.
- **`StereoWidener` (rebuilt, Phase 6; adopted 2026-10-13):** two characters on a Width, Delay
and Character surface. Haas delays the right channel 1 to 40 ms behind the
left, crossfading the right toward its late copy up to Width 1 and turning
the left down above it; Mid/Side scales the difference between the
channels and leaves the sum alone. A mono source passes untouched, and a
Delay move glides instead of clicking. The old `delay_ms=`/`width=`
options keep their names with these meanings. It is what
`audioeffects.StereoWidener` and `create()` now serve.
- **`Rack` (rebuilt, Phase 6; adopted 2026-10-13):** the serial chain on the contract's base, in
`rebuilt/rack.py` beside the old class, which stays until the rebuild is
promoted. It is what `audioeffects.Rack` and `create()` now serve. `chain=` keeps its meaning and also takes a bare effect class. It
renders exactly the same pedals patched by hand; `latency_samples` and
`tail_samples` are live sums; `reset()` clears every pedal and puts each
back to the settings the rack built it with; `deinit()` releases every
pedal, and a chain that fails part-way releases what it built; each pedal
gets the rack's transport and `transport_changed()` reaches nested racks;
`capabilities` is the union of the pedals'. `effects` is a tuple. Patches
and a rack's restore now share one private path in the base,
`_apply_positions`, which the classes that did work once per patch moved
onto.
- **`Octaver` (rebuilt, Phase 6; adopted 2026-10-13):** the analog octave-down, after the
Boss OC-2. The octave is your own note with every other cycle turned
over, clocked from a low-passed copy and multiplied into an all-passed
one, so it keeps your attack and decay and tracks from E2 to A4 at the
default. Seven controls: Direct, Octave 1, Octave 2, Tracking, Tone 1,
Tone 2 and Sensitivity, and six patches. It drops the shifter-based
class's octave up and its `down=`, `up=`, `down2=` and `up2=` options,
and is what `audioeffects.Octaver` and `create()` now serve.
- MultiTapDelay: the docstring says which patches need a P4-class board (patch 4 does, patch 3 may), from the boards' figures at audiodsp v0.6.3
- **`DigitalDelay` (rebuilt, Phase 5; adopted 2026-09-28):** a clean
interpolated line with the Boss DD-2's control law on one
Expand Down Expand Up @@ -225,11 +297,12 @@ there, and are recorded in its changelog.
### Changed

- **`ShimmerHall` and `AirSpace` keep the classes they were ported
against.** With the Phase 5 `TapeDelay` and `Reverb` adopted, the two
racks name the old `delay.TapeDelay` and `reverb.Reverb` directly (a
`Rack` chain entry may now be a class in place of a `NAME`), so they
build and sound as before; moving them onto the rebuilds would re-voice
them. A chain that names `"Reverb"` gets the rebuild, which takes
against.** With the Phase 5 `TapeDelay` and `Reverb` and the Phase 6
`Octaver` adopted, the two old racks (`rack.ShimmerHall` and
`rack.AirSpace`) name the old `delay.TapeDelay`, `reverb.Reverb` and
`pitch.Octaver` directly (a `Rack` chain entry may now be a class in
place of a `NAME`), so they build and sound as before; moving them onto
the rebuilds would re-voice them. A chain that names `"Reverb"` gets the rebuild, which takes
`character=` where the old class took `preset=`.
- **The audiodsp pin moves to v0.6.3rc3.** The release carries the
convolution node's two fixes (audiodsp#165: the block in flight kept
Expand Down Expand Up @@ -291,6 +364,8 @@ there, and are recorded in its changelog.

### Fixed

- A Time brought back after a shorter one no longer plays a stray sample from before the move in MultiTapDelay and the delays built on `audiodelays.Echo` (on CPython and MicroPython; CircuitPython's own `audiodelays` still plays it); PitchShifter, Harmonizer and Octaver start again from scratch on a reset and no longer hang on a source with nothing to give; and a host reset that reaches a Mixer inside an effect keeps the audio its voices hold. All of it is audiodsp's, at the pre-release `AUDIODSP_PIN` now names (v0.6.6rc1).

- The delays, Reverb and ConvolutionReverb now ring out when their source ends, then rest at exact zero, instead of freezing the tail until the source came back; a one-shot source plays once instead of feeding them for ever. A source that comes up short part-way through a block leaves a whole block with the rest silent, and a host `reset_buffer` on ConvolutionReverb keeps the frames it holds. All of it is audiodsp's, at the commit `AUDIODSP_PIN` now names (audiocomponents#127).

- **Ten effects no longer replay old audio when a control brings a bypassed
Expand Down
76 changes: 70 additions & 6 deletions lib/audioeffects/_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,11 @@ def __init__(self, source, *options, **keywords):
#: `Saturation` and `Overdrive` each grew a private flag for the
#: same hazard; this is the one every class can read.
self._constructing = True
#: True while a class seeds its knobs from its constructor's
#: arguments (the class sets it in `_build`) or `_restore` puts them
#: back: a class that keeps an exact constructor value beside a knob
#: (`_KEPT`) keeps it through a knob applied while this is set.
self._seeding = False

try:
self._build(*options, **keywords)
Expand Down Expand Up @@ -1027,11 +1032,60 @@ def program_change(self, index, channel=0, note_id=-1,
patch = type(self).PATCHES.get(index)
if patch is None:
return
for macro in range(len(patch[1])):
self._macros[macro] = position_of_midi(self._mode(macro),
patch[1][macro])
self._apply_macro(macro, self._macros[macro])
self._patch_index = index
self._apply_positions([position_of_midi(self._mode(macro), value)
for macro, value in enumerate(patch[1])],
index)

def _apply_positions(self, positions, patch_index):
"""Set the macros to `positions` (0..1, one per macro from index 0),
apply each, and record `patch_index`.

This is the body of `program_change`, and the one path a patch and a
restore share: a rack puts a child back as it built it by handing
this the positions and patch index it recorded, which the public
`set_macro` cannot do for a class whose grid is coarser than its
positions or whose macros are not all live. A class that does work
once per patch (a refresh, a re-route) does it here, not in
`program_change`, so both paths get it.
"""
for macro in range(len(positions)):
self._macros[macro] = positions[macro]
self._apply_macro(macro, positions[macro])
self._patch_index = patch_index

#: Attributes beyond the macros and the patch index that a class's
#: constructor leaves and a put-back must restore with them: an exact
#: constructor value kept beside its knob (`_glide_exact`). A class that
#: keeps one names it here; `_record` and `_restore` carry it.
_KEPT = ()

#: What `reset()` reloads in place of patch 0: `None`, or a `_record()`
#: a parent hands in for the length of one reset, so a child it built
#: comes back as built in one step, never passing through patch 0.
_reset_to = None

def _record(self):
"""This instance's settings as they stand, for `_restore`: the
positions, the patch index and the `_KEPT` values."""
return (list(self._macros), self._patch_index,
tuple(getattr(self, name) for name in type(self)._KEPT))

def _restore(self, record):
"""Put back what `_record` returned, through `_apply_positions`.

The kept values go back first and the positions are applied with
`_seeding` set, the flag a class's constructor holds while it seeds
the knobs: a class that drops an exact value when its knob moves
keeps it while seeding, so the values and the knobs land together.
"""
positions, patch_index, kept = record
for name, value in zip(type(self)._KEPT, kept):
setattr(self, name, value)
self._seeding = True
try:
self._apply_positions(list(positions), patch_index)
finally:
self._seeding = False

def pitch_bend(self, value, channel=0, sample_position=0):
self._check_live()
Expand Down Expand Up @@ -1094,7 +1148,17 @@ def reset(self):
# Everything is clear now, so nothing is stranded: patch 0 coming
# back off a bypass is the reset's own re-arm, not a second clear.
self._stranded = False
self.program_change(0)
if self._reset_to is None:
self.program_change(0)
else:
# A parent's reset: straight back to the settings it built this
# with. Loading patch 0 on the way could re-arm a graph those
# settings route around, and pull the source to do it. Those
# settings may route around the graph, but it has not played
# since it was cleared, so it is no more stranded than a graph
# `_build` routes around.
self._restore(self._reset_to)
self._stranded = False

def deinit(self):
"""Release every node this class built, tail first. Idempotent, and
Expand Down
16 changes: 12 additions & 4 deletions lib/audioeffects/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,10 +368,18 @@ def program_change(self, index, channel=0, note_id=-1,
patch = self.PATCHES.get(index)
if patch is None:
return
for macro, value in enumerate(patch[1]):
self._macros[macro] = min(1.0, max(0.0, float(value) / 127.0))
self._apply_macro(macro, self._macros[macro])
self._patch_index = index
self._apply_positions([min(1.0, max(0.0, float(value) / 127.0))
for value in patch[1]], index)

def _apply_positions(self, positions, patch_index):
"""Set the macros to `positions` (0..1), apply each, and record
`patch_index`: `program_change`'s body, which a rack also calls to
put a child back as it built it (`_component.Component`'s
counterpart says why)."""
for macro in range(len(positions)):
self._macros[macro] = positions[macro]
self._apply_macro(macro, positions[macro])
self._patch_index = patch_index

def macro(self, index):
"""What macro `index` currently stands for, in its own units."""
Expand Down
5 changes: 2 additions & 3 deletions lib/audioeffects/compressor.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,9 +486,8 @@ def _push_emphasis(self):
else:
self._fast.set(sidechain_hz=self._hz(self.macro(11) * emphasis))

def program_change(self, index, channel=0, note_id=-1,
sample_position=0):
super().program_change(index, channel, note_id, sample_position)
def _apply_positions(self, positions, patch_index):
_component.Component._apply_positions(self, positions, patch_index)
self._route()

def _lean(self):
Expand Down
6 changes: 2 additions & 4 deletions lib/audioeffects/fuzz.py
Original file line number Diff line number Diff line change
Expand Up @@ -1016,10 +1016,8 @@ def _rearm(self):
self._tone_mix.voice[1].play(self._tone_hp)
self._tone_mix.voice[2].play(self._dry)

def program_change(self, index, channel=0, note_id=-1,
sample_position=0):
_component.Component.program_change(self, index, channel, note_id,
sample_position)
def _apply_positions(self, positions, patch_index):
_component.Component._apply_positions(self, positions, patch_index)
self._route()
self._refresh_output()

Expand Down
15 changes: 4 additions & 11 deletions lib/audioeffects/graphiceq.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,9 +411,9 @@ def _level(self, node, index):
node.gain_db = gain_db
node.mix = 1.0

def program_change(self, index, channel=0, note_id=-1,
sample_position=0):
"""Jump to a patch, and start the new curve from a clean bank.
def _apply_positions(self, positions, patch_index):
"""Jump to a patch (or a rack's restore), and start the new curve
from a clean bank.

The difference between this and `set_macro` is deliberate, and it is
the difference between a jump and a sweep. A slider being pushed
Expand All @@ -424,16 +424,9 @@ def program_change(self, index, channel=0, note_id=-1,
peaks at **32767 and stays above its own steady level for 306 ms**
without this, and at **5826 for 8 ms** with it.
"""
if type(self).PATCHES.get(index) is None:
return _component.Component.program_change(
self, index, channel=channel, note_id=note_id,
sample_position=sample_position)
result = _component.Component.program_change(
self, index, channel=channel, note_id=note_id,
sample_position=sample_position)
_component.Component._apply_positions(self, positions, patch_index)
for node in self._nodes:
node.clear()
return result

@property
def tail_samples(self):
Expand Down
14 changes: 8 additions & 6 deletions lib/audioeffects/rack.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@
used by other racks: a `chain` entry may itself be a `("Rack", {...})`.

Those two racks name the classes they were ported against, `delay.py`'s
`TapeDelay` and `reverb.py`'s `Reverb`, directly rather than by NAME: the
rebuilt classes the package serves under those names since their adoption
on 2026-09-29 take different settings (no `preset`, no `wow`) and sound
different, and moving a rack onto them is a re-voicing of the rack, not a
part of the adoption.
`TapeDelay`, `pitch.py`'s `Octaver` and `reverb.py`'s `Reverb`, directly
rather than by NAME: the rebuilt classes the package serves under those
names (since 2026-09-29 for the delay and reverb, 2026-10-13 for the
octaver) take different settings (no `preset`, no `wow`, no `down` or `up`)
and sound different, and moving a rack onto them is a re-voicing of the
rack, not a part of the adoption.
"""

VENDOR = "PyDevices"
Expand All @@ -35,6 +36,7 @@
from ._component import midi_of_position as _midi_of_position
from ._component import port_target as _port_target
from .delay import TapeDelay as _OldTapeDelay
from .pitch import Octaver as _OldOctaver
from .reverb import Reverb as _OldReverb


Expand Down Expand Up @@ -192,7 +194,7 @@ def __init__(self, source, shimmer=0.55, echo=0.36, space=0.42,
# macro moves the branch's mixer level, and a branch that was
# skipped for starting at zero could never come back.
Rack.__init__(self, source, chain=(
("Octaver", {"down": 0.0, "up": 1.0}),
(_OldOctaver, {"down": 0.0, "up": 1.0}),
(_OldTapeDelay, {"time_ms": 420.0, "feedback": 0.5,
"wow": 0.3, "drive": 0.2}),
(_OldReverb, {"preset": "hall"}),
Expand Down
Loading
Loading