Skip to content

Effects Phase 6: pitch, stereo, racks, and the soundtrack's classes - #141

Merged
bdbarnett merged 115 commits into
mainfrom
effects/phase6
Oct 14, 2026
Merged

bdbarnett merged 115 commits into
mainfrom
effects/phase6

Conversation

@bdbarnett

Copy link
Copy Markdown
Contributor

Merges the Phase 6 branch into main. Seven classes through the class gate, each on the phase branch and adopted there (#135, #136, #137, #140); the kit (#133's commits, already on this branch) and the lifecycle matrix with Phase 6's rows.

The merge with main auto-resolved in tests/test_lifecycle_matrix.py: main's run_cpython_all split and the Phase 6 rows are both kept. The branch CI is the three-job PR run; the push to main then runs all six.

Supersedes #133, whose commits are already on this branch.

…, input ceiling and paperwork checks

Six mechanical checks on one class, each printing one line per cell and a
SUMMARY per check. The four render checks are numpy-free and print the
same lines on CPython, MicroPython and CircuitPython; the claims and
paperwork checks read source and git, on CPython. kit_planted.py holds
the tiny planted components the checks are proven on.
tests/test_kit.py holds every kit check red on a planted fault with its
control green on the same cell, a do-nothing class never read as a pass,
and the render checks printing the same lines on CPython, MicroPython and
CircuitPython (with a plant whose bytes differ natively to show that
agreement can fail). KIT.md says what each check measures, its bars and
conventions, and how a class declares an exception.

The ceiling's wrap rule now asks that the jump unwraps to a neighbour, so
a full-scale click followed by a delay's repeat is not read as a wrap.
…nd Character

Haas crossfades the right channel toward a copy of itself 1 to 40 ms late
and turns the left down above Width 1; Mid/Side scales the difference
between the channels on audioroute.MidSide. A mono source is a wire, a
Delay move glides, and the six traits each have a test shown red on a
planted fault (KIT_PLANTS), with every docstring claim in CLAIMS. The
lifecycle matrix declares the family's control-jump step and one glide
cell; the kit declares the slew plant, which only a moving control shows.
program_change's body moves into a private _apply_positions(positions,
patch_index), which program_change calls with a patch's positions. A rack
calls it to put a child back exactly as it built it, which the public
set_macro cannot do for a measured ConvolutionReverb or a setting off the
0-127 grid. The classes that wrapped program_change to do work once per
patch (the deferred refreshes, the lean re-routes, GraphicEQ's clean bank,
the old Reverb's single rebuild) now wrap _apply_positions, so a patch and
a restore take the same path. The test plants that stood for the base's
per-macro path move with them.
rebuilt/rack.py beside the old class. The chain renders exactly the same
pedals patched by hand; latency_samples and tail_samples are live sums;
reset() is each child's reset then patch 0, which puts every child back as
built; deinit() releases every child and a failed build releases what it
built; children get the rack's transport and transport_changed() reaches
nested racks; capabilities is the union of the children's. Tests for T1-T6
with their planted faults, the CLAIMS table and KIT_PLANTS.

The kit runs Rack on the probe chain K (KIT_OPTIONS), and the lifecycle
CLI reads the same table; the lifecycle skips its macro events for a class
with no macros. Declared: the plant cells whose faults no render reaches,
ConvolutionReverb's own 0 dBFS sine ceiling, and the E1 P4 cells Vibrato
and Fuzz are red on alone.
…lock without get_buffer

The constructor's exact delay_ms plays until Delay moves or a patch
loads, and the patches' Delays play their round numbers by step, so
patch 0 and reset() are 14 ms whatever the constructor said. Where
audiocore.get_buffer is compiled out, a constructor patch no longer wires
Haas, and a return to Haas re-plays the voices, so a block a voice held
is dropped rather than played; the one-block skip that remains is in the
docstring. The clipping sentence says below half of full scale, tested
at the bound, and the H1, W and M1 tests run every seed and rate the
traits list.
The send runs through an audioecho.FeedbackDelay with its loop filters, an
audiobiquad Top Cut and the rebuilt Reverb as a chamber; the dry comes back
untouched beside it. Eleven macros and seven patches, the card's five
traits each tested and shown red on its planted fault (the plants live in
tests/airspace_plants.py so the kit checks them natively), every docstring
claim tied to a test. Declared: the chamber's P4 rows (a reset or a return
from Mix 0 empties the tank, which never re-converges with an uninterrupted
one) and the glide plant, which only a Time move can show.
…rt) and the CPython dry check (audiodsp#228)
… refused program_change keeps the exact Delay
… row no longer hides Rack's own reset faults
Where audiocore.get_buffer is compiled out, the class could not hand out
the block a voice primes, so after wiring (or a re-play) a voice could
hold a block read from the line that a reset then emptied, or read at a
Delay the reset then left: the copy of that block never played, and the
right channel was silent for one block. The class now pulls the mixer
through a one-voice mixer of its own playing the Haas tail, so wiring
primes from silence and hands it out on every build, and no voice ever
holds a block between pulls. The re-play special cases are gone, and so
is the disclosed one-block skip when leaving Haas before its first block.

Renders with get_buffer are unchanged byte for byte. The new test
compares both channels with and without get_buffer from the first frame,
twelve paths at 48, 16 and 8 kHz.
The OC-2's octave-down on a clock leg (two Q 0.5 low-passes, a dead-band
comparator, SubOctave) and a signal leg (a matched two-stage AllPass and
Multiply), Octave 2 dividing Tone 1's output, three voices on a Mixer.
Seven macros, six patches; constructor values land on the 0-127 grid.
Declares the divider's free count (lifecycle P4), the family P5 rows,
CircuitPython's own Mixer on a host reset, and clicks under Sensitivity
in the ceiling check.
…ntence

At 22.05 kHz a macro move that reaches the tape's loop or the chamber
leaves the tank off the uninterrupted control for good (P4, as Reverb's),
and Mix, Top Cut and patch moves step the output (P5, the family's
disclosed limit). The docstring's sentence on jumping controls now names
those declared rows.
…is compiled out

On a board's own audiocore the constructor's wiring primes the source's
first block into the voices, and a Mix 0 before the first pull would play
that block stale on the return. The return now re-plays the voices there;
mid-stream it renders byte for byte what the quiet wiring renders. Tested
with get_buffer hidden from the class and _component, with the replay
taken out as the red control.
A rack's reset used to run each pedal's own reset, which loads patch 0,
then put the rack's settings back over the top. A pedal built at Mix 0
re-armed its voices inside that patch 0 and pulled a block of the source,
so the rack's output skipped 256 frames and the held block played stale
when Mix came back up (Compressor, Saturation, Bitcrusher, DynamicEQ,
Exciter, Overdrive). The put-back also lost the exact constructor values a
delay keeps beside its knobs, so DigitalDelay(glide_ms=10) glided for
815 ms on its next Time move.

Component gains a small private hook: _record() takes an instance's
positions, patch index and the values its class names in _KEPT, and
_restore() puts them back with _seeding set. Component.reset() reloads a
record a parent hands in through _reset_to in place of patch 0, so a
child's own reset clears and restores in one step and never passes
through patch 0. With no record handed in, every class's reset is what it
was: 550 renders of every class at every patch, reset mid-stream, are
byte-identical before and after.

The five delays that keep exact values (DigitalDelay, TapeDelay,
AnalogDelay, SlapbackDelay, PingPongDelay) name them in _KEPT. A refused
patch= now releases every pedal the rack built.
… 2, a return from Mix 0 to 127 and a Mix move before the first pull
…tion names it and is held to a table the scan finds
…player's words, each with a test; a seventh patch, Octave Bloom - lean (Rise at Rise 0), for an S3; the lower-rate band claim struck
# Conflicts:
#	CHANGELOG.md
#	tests/support/kit_common.py
#	tests/support/lifecycle.py
At -12 dBFS a square railed at Mix 1 with Feedback, Decay or Time at 127, on
Long Send and on Dark Loop. The worst peak gain over every patch, every other
macro at its stops, all macros open, six low squares and noise is 16.47 dB
(Decay 127, 220 Hz), so -17 leaves 0.51 dB under the rail. The ceiling test
renders the worst cells for 12 s.
…he -26 dBFS for Feedback, Decay and Time open together

Pairs and triples of Feedback, Decay and Time at 127 on every patch, over 110,
147 and 220 Hz squares and 12 s, gain up to 24.71 dB (Short Send with Feedback
and Decay). One test pins that gain under the quoted 25 dB.
Rebuilt StereoWidener, Rack and Octaver
…ys clean at 22.05 kHz; a sweep of every upward patch, red on the card's Slow Rise
# Conflicts:
#	CHANGELOG.md
#	tests/support/kit_common.py
#	tests/test_cpython_effects_rack.py
…to play a Rise patch there, pinned to the patches in Bloom
# Conflicts:
#	CHANGELOG.md
#	tests/support/lifecycle.py
… Rise 0, which an ESP32-S3 keeps up with; Octave Bloom moves to patch 6

Patch 0 is now Rise at Rise 0 (steps 127, 89, 0, 90, 96, 94, 64, 91, 32),
and the old defaults are patch 6, Octave Bloom. The S3 sentence names the
Bloom patches as they now are. Tests that measured Bloom at the defaults
ask for it by name, so every trait is still held where it was.
Rebuilt PitchShifter, Harmonizer and AirSpace
…ShimmerHall are adopted: Phase 6's seven

ADOPTED names all seven, so audioeffects.<Name> and create() serve the
rebuilds. They stay under rebuilt/, and pitch.py and rack.py keep the old
classes until the rebuilds are promoted.

The old ShimmerHall built its octave branch by NAME with the old Octaver's
down= and up=, which the rebuild refuses, so it now names pitch.Octaver
directly, as it already does for the tape delay and the reverb. Its render
and AirSpace's old rack are unchanged when built from rack.py.

Tests: each class gains an "adopted is what the package serves" test (create()
returns the rebuild), and the registry names all fifteen adopted classes; with
ADOPTED cut back to the first eight, all of them go red. The tests written
against the old classes by their served name now name them from their own
module: test_cpython_effects_pitch (the shifter-based Octaver's octave reach
and shifter count) and test_cpython_effects_racks (the old Rack, ShimmerHall
and AirSpace). Their assertions are unchanged.

The CHANGELOG entries for the seven say they are adopted (2026-10-13), and the
note on the soundtrack racks now includes the octaver.

Gates, this tree: validate_api 55 instruments / 45 effects; metadata valid;
flake8 clean; contract and the seven classes' scoped tests, the kit and the
registry OK; the library smoke 45 classes, 318 patches, 0 failures on
CPython, MicroPython and CircuitPython. The CPython digest census (burst plus
two seconds, every served class at the defaults and every patch) moves only
the six served names whose rebuilds differ from the old classes, onto the
rebuilds' own digests; Rack's default chain is a wire either way, and every
other class is byte-identical.
At the Rise defaults the hall sounds at once, so a Size or Mix jump early in
the render lands on sound and steps the output: the disclosed family limit
(a control that jumps makes the output step; moving Size empties the hall).
On CircuitPython, Octave Bloom loaded before the first pull now re-arms
Bloom's shifter from the defaults, and CircuitPython's own PitchShift keeps
its positions through a clear, as for the other Bloom patches.
ShimmerHall plays on every board; Phase 6's classes adopted
@bdbarnett
bdbarnett merged commit 76036e2 into main Oct 14, 2026
6 checks passed
@bdbarnett
bdbarnett deleted the effects/phase6 branch October 14, 2026 01:18
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