From 4755c71703738d208d44ab57a9624ffb0be52d68 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:22:34 -0500 Subject: [PATCH] TapeDelay, AnalogDelay, PingPongDelay, MultiTapDelay, Reverb and ConvolutionReverb are adopted: Phase 5's last six The board session at audiodsp v0.6.3 (2026-09-29, both boards at the same image) met the cost bar (at most 80 % of a 5.333 ms stereo block) and real time at every cell of the six on the ESP32-P4, and on the ESP32-S3 at every cell but MultiTapDelay's patch 4 (5.567 ms), which its docstring already discloses on Brad's ruling. Each board digest equals the desktop's or a single-precision desktop MicroPython's (audiocomponents#75); Reverb's patch 7 differs from that build by nine one-LSB samples of 65536 (audiodsp#183). ADOPTED names all eight Phase 5 classes, so audioeffects. and create() serve the rebuilds. They stay under rebuilt/ with the first two; delay.py and reverb.py keep the old classes until promotion. ShimmerHall and AirSpace built their tape delay and hall by NAME with the old classes' settings (preset=, wow=, drive=), which the rebuilds refuse, so both racks failed to construct. They now name delay.TapeDelay and reverb.Reverb directly: a Rack chain entry may be a class in place of a NAME. Their renders are unchanged (the census). Moving them onto the rebuilds is a re-voicing and is left for later. The Rack and _core docstring examples say character=. The README's time-and-space table gains the six with their v0.6.3 board figures; the old-class table and the audiofreeverb sentence go. The six CHANGELOG entries describe the adopted classes and their board rows, and two stray duplicated lines in the AnalogDelay and Reverb entries are folded back where they belong. No class docstring still said "pending hardware". Tests: each class's parked test flips to "adopted is what the package serves" (Reverb had none and gains one; ConvolutionReverb's registry test flips), and the registry names all eight; with ADOPTED cut back to the first two, all seven go red. Tests written against the old classes by their served name now name them from their family module: test_cpython_effects_time (TapeDelay wow/clear, AnalogDelay age, ConvolutionReverb decay_seconds and stereo), test_cpython_effects_library (Reverb preset, Reverb's empty macro surface), test_cpython_effects_racks (the Reverb preset chains). --- CHANGELOG.md | 98 ++++++++++++------- lib/audioeffects/README.md | 65 ++++++------ lib/audioeffects/_core.py | 2 +- lib/audioeffects/rack.py | 26 +++-- lib/audioeffects/rebuilt/__init__.py | 17 ++++ tests/test_cpython_effects_analogdelay.py | 18 ++-- .../test_cpython_effects_convolutionreverb.py | 13 ++- tests/test_cpython_effects_library.py | 10 +- tests/test_cpython_effects_multitapdelay.py | 18 +++- tests/test_cpython_effects_pingpongdelay.py | 18 ++-- tests/test_cpython_effects_racks.py | 10 +- tests/test_cpython_effects_reverb.py | 15 +++ tests/test_cpython_effects_tapedelay.py | 16 ++- tests/test_cpython_effects_time.py | 41 ++++---- tests/test_rebuilt_registry.py | 6 +- 15 files changed, 253 insertions(+), 120 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf1225f..bd100b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,7 +54,7 @@ there, and are recorded in its changelog. frame at 44.1 and 22.05 kHz, and Tone's flat top at 22.05 kHz. It lives in `lib/audioeffects/rebuilt/slapbackdelay.py` until it comes home. -- **`TapeDelay` (rebuilt, Phase 5, parked under `rebuilt/`):** a tape loop +- **`TapeDelay` (rebuilt, Phase 5; adopted 2026-09-29):** a tape loop on one `audioecho.FeedbackDelay` with two transports as characters: the RE-201's motor (`"varispeed"`, the default), where a Time move bends the pitch by the ratio of the two times for exactly the new time, and the @@ -63,22 +63,24 @@ there, and are recorded in its changelog. loss law whose corner follows Spacing and the tape speed; Wow and Flutter drive one periodic wow table with a slow drift; Record Level is the loop's cubic squash, without memory. Mix 0 is a wire while the loop keeps - recording. It lives in `lib/audioeffects/rebuilt/tapedelay.py`, and - `audioeffects.TapeDelay` is still the old class. Every sentence of the + recording. It is what `audioeffects.TapeDelay` and `create()` now serve, + from `lib/audioeffects/rebuilt/tapedelay.py` until it comes home; the old + `delay.TapeDelay` stays in `delay.py` until then. Every sentence of the docstring that makes a claim is tied to a test (`CLAIMS` in its test file); it says where the pitch bend stops, which Wow and Flutter moves still step, and the family's two limits (a control that jumps steps the - output; the tail rings only while the source feeds). On the boards the full class is over its - budget (P4 10.3-11.4 % against 9 %, S3 19.8-20.5 % against 15 %, at - audiodsp v0.6.2); patch 8 `Tape Delay - lean` (patch 0 with Record - Level at 0) built with `max_time_ms=800` met both bars in every run, and - gives up the tape saturation and Time above 800 ms. At audiodsp + output; the tail rings only while the source feeds). At audiodsp v0.6.3rc1 its Feedback is handed as set (no stepping clear of the stall windows; the bound counts one landing lap there), a Wow or Flutter move that changes only the wobble's depth glides over 20 ms, and a knob turned down to 0 keeps the last wow table while the depth ramps out; a move - that changes the balance of Wow and Flutter still steps, disclosed. -- **`AnalogDelay` (rebuilt, Phase 5, parked under `rebuilt/`):** a + that changes the balance of Wow and Flutter still steps, disclosed. At + v0.6.3rc3 Spread is handed as set: the node ends a cross-fed tail itself + (audiodsp#173), so the 1/4096 grid the class used is gone. On the boards + at audiodsp v0.6.3 every patch runs in real time at 13.4-14.1 % of a + block on the P4 and 10.0-12.8 % on the S3, and every digest equals the + desktop's or a single-precision desktop build's (audiocomponents#75). +- **`AnalogDelay` (rebuilt, Phase 5; adopted 2026-09-29):** a bucket-brigade delay on one `audioecho.FeedbackDelay`, with the Boss DM-2's one 4096-stage line (`"single-line"`, the default) and the Deluxe Memory Man's two in series (`"double-line"`) as characters. The Time knob @@ -89,22 +91,22 @@ there, and are recorded in its changelog. keeps recording. Modulation is a triangle of fixed millisecond swing, and Sync follows a host's beat by Division. There is no sample-and-hold, no compander and no fixed ~3 kHz pair, so short Times are brighter than - either pedal. It lives in `lib/audioeffects/rebuilt/analogdelay.py`, and - `audioeffects.AnalogDelay` is still the old class. The docstring states - the input ceiling (-4.4 dBFS peak at the default Mix 0.4) and that - turning Time through several positions takes seconds to settle. Board - cost is unmeasured. At audiodsp v0.6.3rc1 the Feedback is handed as set + either pedal. It is what `audioeffects.AnalogDelay` and `create()` now + serve, from `lib/audioeffects/rebuilt/analogdelay.py` until it comes + home. The docstring states the input ceiling (-4.4 dBFS peak at the + default Mix 0.4) and that turning Time through several positions takes + seconds to settle. At audiodsp v0.6.3rc1 the Feedback is handed as set (no stepping clear of the stall windows; the bound counts one landing lap there), and a Modulation move glides over 20 ms instead of stepping the read offset. At v0.6.3rc3 Spread is handed as set: the node's stereo cross-feed tail now ends on its own (audiodsp#170), so the 1/4096 grid the class used to dodge it is gone. A jumping control steps the output and a tail cut short by a stopped source carries on when it comes back, - disclosed as family limits (audiocomponents#117, audiodsp#180). - that changes the balance of Wow and Flutter still steps, disclosed. At - v0.6.3rc3 Spread is handed as set: the node ends a cross-fed tail itself - (audiodsp#173), so the 1/4096 grid the class used is gone. -- **`PingPongDelay` (rebuilt, Phase 5, parked under `rebuilt/`):** repeats + disclosed as family limits (audiocomponents#117, audiodsp#180). On the + boards at audiodsp v0.6.3 every patch runs in real time at 7.0-7.5 % of + a block on the P4 and 14.0-14.5 % on the S3, and every digest equals the + desktop's or a single-precision desktop build's (audiocomponents#75). +- **`PingPongDelay` (rebuilt, Phase 5; adopted 2026-09-29):** repeats that alternate between the speakers, on one `audioecho.FeedbackDelay` whose cross-feed and input pan Spread moves between two plain delays and the full bounce, by a stated law. The dry path is each channel's own @@ -116,18 +118,21 @@ there, and are recorded in its changelog. 20 to 1000 ms), which needs a node option. Time walks rather than clicks, Sync follows the host's beat, and Repeat Tone and Repeat Cut put a low-pass and a high-pass in the loop, defaulting out. It lives in - `lib/audioeffects/rebuilt/pingpongdelay.py`, and - `audioeffects.PingPongDelay` is still the old class. The docstring states + `lib/audioeffects/rebuilt/pingpongdelay.py` until it comes home, and it + is what `audioeffects.PingPongDelay` and `create()` now serve. The + docstring states the input ceiling (-3 dBFS peak at the defaults, -3.1 over the shipped patches, on `noise_det`; -7.96 dBFS on any material with Repeat Cut in at the default Mix), and that at full Spread the loop hears (L + R) / 2. - Board cost is unmeasured. At audiodsp v0.6.3rc1 both loop filters' out + On the boards at audiodsp v0.6.3 every patch runs in real time at + 11.9-12.2 % of a block on the P4 and 16.4-16.6 % on the S3, and every + digest equals the desktop's. At audiodsp v0.6.3rc1 both loop filters' out stops hand exactly 0 and a filter taken out is out whatever came before (no tracking Tone stop, no Cut held in at 20 Hz, so `tail_samples` is finite once Cut is out again), and the Feedback is handed as set (no stepping clear of the stall windows; the bound counts one landing lap there). -- **`ConvolutionReverb` (rebuilt, Phase 5, parked under `rebuilt/`):** a +- **`ConvolutionReverb` (rebuilt, Phase 5; adopted 2026-09-29):** a short room on one `audioconvolve.Convolver`, synthesized from Decay, Damping, Predelay, Diffusion and one of 64 Room seeds, or loaded from your own impulse (int16 frames or a 16-bit WAV at the graph's rate) with @@ -139,16 +144,20 @@ there, and are recorded in its changelog. delayed by exactly that, byte for byte, while the source keeps feeding it and nothing resets it. Measured mode loads at unit mean energy and refuses the synthesis macros with `IndexError`; no impulse ships. It - lives in `lib/audioeffects/rebuilt/convolutionreverb.py`, and - `audioeffects.ConvolutionReverb` is still the old class. It stands on + lives in `lib/audioeffects/rebuilt/convolutionreverb.py` until it comes + home, and it is what `audioeffects.ConvolutionReverb` and `create()` now + serve. It stands on audiodsp v0.6.3rc3: a room-knob move never drops or repeats a dry frame at any Mix, from the end of the block in flight the output is that of an instance that always had the new settings, and each side of a stereo room is unit energy on its own, so the room sits in the middle. `reset()` mid-stream silences the block in flight, dry included, and a Mix move lands at the end of it. The docstring is the player's text, - every claim in it tied to a test. Board cost is unmeasured. -- **`MultiTapDelay` (rebuilt, Phase 5, parked under `rebuilt/`):** the + every claim in it tied to a test. On the boards at audiodsp v0.6.3 every + patch runs in real time at 41.6-42.2 % of a block on the P4 and + 71.2-73.1 % on the S3, and every digest equals the desktop's or a + single-precision desktop build's (audiocomponents#75). +- **`MultiTapDelay` (rebuilt, Phase 5; adopted 2026-09-29):** the RE-201's multi-head modes, with the Echorec as a second reference. Heads sit on a grid of whole multiples of one base time, exactly, at every rate; Pattern is the RE-202's twelve head sets over that grid (mode 12 @@ -156,8 +165,9 @@ there, and are recorded in its changelog. `audioecho.FeedbackDelay` whose loop low-pass darkens once per lap, and Repeat Tone's out stop swaps to a lighter graph (patch 1, the one to stack on an S3). Mix 0 is a wire. It lives in - `lib/audioeffects/rebuilt/multitapdelay.py`, and - `audioeffects.MultiTapDelay` is still the old class. The tap node reads + `lib/audioeffects/rebuilt/multitapdelay.py` until it comes home, and it + is what `audioeffects.MultiTapDelay` and `create()` now serve. The tap + node reads its input one block behind the dry, so settings made before the first pull or after `reset()` land on the grid in both lanes. Any number of resets and returns from Mix 0, before the first pull or between two @@ -183,8 +193,12 @@ there, and are recorded in its changelog. docstring states the input ceiling, the click and channel crossing of a Time or Heads move while audio plays, and that on CircuitPython alone the stereo dry's right lane is one LSB hot within 32 LSB of the rails. - Board cost is unmeasured. -- **`Reverb` (rebuilt, Phase 5, parked under `rebuilt/`):** Dattorro's plate + On the boards at audiodsp v0.6.3 every patch runs in real time on the P4 + (36.0-72.5 % of a block); on the S3 patches 0-3, 5 and 6 do (57.8-78.1 %) + and patch 4 does not (5.567 ms, 104.4 %), so the docstring says patch 4 + needs a P4-class board and patch 3 may. Every digest equals the + desktop's or a single-precision desktop build's (audiocomponents#75). +- **`Reverb` (rebuilt, Phase 5; adopted 2026-09-29):** Dattorro's plate network on one `audioverb.Tank`, cut four ways as a Character macro: an EMT 140 plate that is dense at once and darkens as Decay shortens, and a room, a chamber and a hall that build. Decay is T60 at 500 Hz within 12 % @@ -194,10 +208,15 @@ there, and are recorded in its changelog. without losing a frame of the dry, `reset()` keeps the dry the Tank holds, and Tone's centre is handed as 0 dB. Mix 0 is a byte-exact wire while the tank keeps ringing; latency is zero. It lives in - `lib/audioeffects/rebuilt/reverb.py`, and `audioeffects.Reverb` is still - the old class. The Fender 6G15 spring character is parked until the node - carries a dispersive chain. Board cost is unmeasured. - Board cost is unmeasured. At audiodsp v0.6.3rc3 the class plays what it + `lib/audioeffects/rebuilt/reverb.py` until it comes home, and it is what + `audioeffects.Reverb` and `create()` now serve; the old `reverb.Reverb`, + with its `preset`, stays in `reverb.py` until then. The Fender 6G15 + spring character is parked until the node carries a dispersive chain. + On the boards at audiodsp v0.6.3 every patch runs in real time at + 19.0-23.4 % of a block on the P4 and 33.7-36.3 % on the S3; every digest + equals a single-precision desktop build's (audiocomponents#75) but patch + 7's, which differs from it by nine one-LSB samples of 65536 + (audiodsp#183). At audiodsp v0.6.3rc3 the class plays what it played at rc1; its docstring is now the player's text, each claim tied to a test, with a jumping control disclosed as a family limit (audiocomponents#117), and a return from Mix 0 said plainly to start @@ -205,6 +224,13 @@ 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 + `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 across a re-synthesis, each side of a stereo room normalised on its own), diff --git a/lib/audioeffects/README.md b/lib/audioeffects/README.md index 8478cfa..b12f16c 100644 --- a/lib/audioeffects/README.md +++ b/lib/audioeffects/README.md @@ -129,43 +129,43 @@ marginal share of one block on each board at construction defaults. | `Notch` | audiodsp (`audiobiquad`) | 5 | 0 samples | 5.6 % P4 / 9.6 % S3 of a block, **over** its 1.5 % / 5 % budget; a `" - lean"` patch is still owed and none was invented | none (the Twin-T was weighed and dropped on scope) - a band-stop whose `Width` is a bandwidth and not a depth, with a Harmonics toggle for mains hum. A `float` coefficient set cannot put the zeros exactly on the unit circle, so at 60 Hz it is a hum *reducer*, not an eliminator | | `LadderFilter` | audiodsp (`audioladder`) | 7 | 0 samples | 16.2 % P4 / 26.7 % S3 of a block at patch 4 against the palette-derived 17 % / 28 %; lean patch 6 is 8.6 % / 14.7 %. T5 and T1's stopband slope stay disconfirmed | the Moog transistor ladder - four one-pole stages round one global feedback loop with an odd saturator **inside** it, so the passband sinks as `Resonance` rises. That droop is the circuit | | `CombFilter` | audiodsp (`audioecho`, `audiobiquad`) | 6 | 0 samples | 7.4 % P4 / 12.2 % S3 of a block at patch 0 against the palette-derived 8 % / 13 %. The tail reaches exact zero at every Feedback and tuning since audiodsp v0.6.2 (#154; up to v0.6.1 it parked for ever above Feedback 0.5, +17.4 cents at 1760 Hz / 0.8): at most 7.8 s measured at 20 Hz / 0.95. Since 2026-09-28 `tail_samples` is finite at every setting: the lap bound on the longest line the read head may be at, plus Tone's memory, plus 1.5 s while the Trim (a fixed-point shelf, measured at up to 0.834 s) is in circuit; 900 renders over the surface end inside it. Feedback is handed as set; at a stall-window centre (the 0.95 stop is one) the bound counts one landing lap, since audiodsp v0.6.3rc1 lands a stalled loop low-pass (#157). Tone off is exactly 0 whatever came before, and Tone back in from silence plays nothing (the v0.6.2 node froze the low-pass and played 15 070 LSB at 48 kHz). The ring plays the asked fractional delay, 0.7 cents at 1760 Hz / 0.8, and the first-repeat tap lands within 0.01 cents. T2/T5 miss at fractional tunings | none - the naked textbook feedback comb `y(n) = x(n) + g·y(n−M)`: a delay short enough to be a pitch, fed back, so noise grows resonances on that note's harmonic series | -### Time and space - two adopted, the rest in `reverb.py`, `delay.py` - -Phase 5's first two classes, `DigitalDelay` and `SlapbackDelay`, were -**adopted on 2026-09-28**: `audioeffects.create()` serves the rebuilds, from -`lib/audioeffects/rebuilt/`, until they come home. Both are **audiodsp** tier -(one `audioecho.FeedbackDelay` each), zero latency at every setting, and -neither runs on a stock CircuitPython board. Cost is the class's own share of -one 256-frame stereo block at 48 kHz, measured on both boards at every -shipped patch; the S3 figures are the same-conditions reading (the class -measured beside the palette row it was priced from). +### Time and space - all eight adopted, served from `rebuilt/` + +Phase 5's eight classes are adopted: `DigitalDelay` and `SlapbackDelay` on +2026-09-28, the other six on 2026-09-29. `audioeffects.create()` serves the +rebuilds, from `lib/audioeffects/rebuilt/`, until they come home. All eight +are **audiodsp** tier, and none runs on a stock CircuitPython board. Cost is +the class's own share of one 256-frame stereo block at 48 kHz, measured on +both boards at every shipped patch. For the first two the S3 figures are the +same-conditions reading (the class measured beside the palette row it was +priced from); the other six were measured at audiodsp v0.6.3, where every +cell met the bar (at most 80 % of a block) and ran in real time on the P4, +and on the S3 every cell did but `MultiTapDelay`'s patch 4. Their digests +equal the desktop's, or a desktop MicroPython's built with single-precision +floats (a setting worked out in Python float, audiocomponents#75), except +`Reverb`'s patch 7 (below). | Class | Tier | Macros | Latency | Cost | Standout | |---|---|---|---|---|---| | `DigitalDelay` | audiodsp (`audioecho`) | 8 | 0 samples | **P4 at most 7.0 %** (0.372 ms, patch 5, rt 5.49) **/ S3 about 14.6 %** at patch 5 (about 0.78 ms corrected; 11.8 % as the tool reads it; rt 3.11), against palette **9 % / 15 %** (FeedbackDelay +options); Brad passed the S3 figure on 2026-09-28. No lean patch. RAM 155 584 B at the default 800 ms line (48 kHz); `max_time_ms` spends less | Boss DD-2 (1983), light touch: its control law and dry/wet discipline, Mix 0 a wire, Time bends the repeats in pitch at a rate Glide sets instead of clicking, every static Time on a whole frame. Repeat Tone and Repeat Cut put the pedal's 7 kHz and 40 Hz corners in the loop as knobs that default out; patch 5 is the pedal's own corners. `tail_samples` is finite with Repeat Cut out; Feedback is handed as set, and with Repeat Tone in the bound counts one landing lap at a stall-window centre (audiodsp v0.6.3rc1, #157). Both out stops are exactly 0, and either filter back in after silence plays nothing (at v0.6.2 Tone's played 26 443 LSB and Cut's 20 858). The node lands 18 of the 128 Time positions one float32 step off the whole frame at 44.1 kHz and 18 at 22.05 kHz (patch 4 at 44.1 kHz: a click's first repeat reads 19 922 and 78), none at 48 kHz. **Digests:** six of seven patches identical on the P4, the S3 and the desktop; patch 5 identical board to board, and different from the desktop through single-precision Python floats in two derived settings (audiocomponents#75): `cut_hz` 0.035 % high (39.44766 Hz against 39.43366) and `damping_hz` one float32 step. **What the default surrenders:** no band limit and no compander, no HOLD, and Glide 0's instant knob clicks. | | `SlapbackDelay` | audiodsp (`audioecho`) | 6 | 0 samples | **P4 at most 7.4 %** (0.396 ms corrected; 6.4 % as the tool reads it; rt 5.82) **/ S3 14.6-14.7 %** at the default and patches 0-4 and **15.2 % at patch 5** (0.813 ms corrected, the one patch with Tone in; 11.6 % as the tool reads it; rt 3.08), against palette **9 % / 15 %** (FeedbackDelay +options); Brad passed patch 5 on 2026-09-28 ("It passes"). No lean patch. RAM about 50 KB (a fixed 251 ms line) | Sam Phillips' two-Ampex-350 slap at Sun Studio: one mono repeat at 135 ms, handed as a whole frame at every rate, with Saturation, Tone and Wow as the tape's colours and Repeats defaulting to 0. Level 0 is a wire; Time walks rather than clicks. Tone out is byte-identical to no filter whatever came before, and a Wow move glides over 20 ms (both since audiodsp v0.6.3rc1). The node lands 21 of the 128 Time positions one float32 step off the whole frame at 44.1 kHz and 20 at 22.05 kHz, none at 48 kHz and no shipped patch. **Digests:** all seven patches identical on the P4 and the S3, and different from the desktop through single-precision Python floats in a derived setting (audiocomponents#75): `wow_depth_ms` 2.1 x 10^-5 to 6.8 x 10^-5 ms high, under 0.0001 cent (patch 4's Feedback also one float32 step). **What the default surrenders:** Tone out, so the repeat is as bright as the dry, and Wow at 1 cent makes a 15 kHz repeat breathe down to -5.11 dB at 48 kHz. | - -| Class | Notes | -|---|---| -| `Reverb` | presets `room` `chamber` `hall` `plate` `spring` (spring adds pre-flutter) | -| `ConvolutionReverb` | a real impulse response, measured or synthesized; **patches** | -| `TapeDelay` | in-loop low-pass, soft-clip and per-sample wow; **patches** | -| `AnalogDelay` | BBD: band-limited both ends, `age` over the lot; **patches** | -| `PingPongDelay` | true cross-feed - repeats alternate sides; **patches** | -| `MultiTapDelay` | `(position, level)` tap patterns | - -`MultiTapDelay` is clean and runs on `audiodelays`, whose feedback path is -the echo times a decay. The other three are named after something that -happens *inside* that path - a filter taking a little more off each pass, a -soft-clip rounding it, a cross-feed sending it to the other speaker - so they -run on `audioecho.FeedbackDelay`, which is where audiodsp puts those, as do -the two adopted delays above. A delay's `max_time_ms` sizes its line and +| `TapeDelay` | audiodsp (`audioecho`) | 11 | 0 samples | **P4 13.4-14.1 %** (0.714-0.751 ms, rt 3.85-3.95) **/ S3 10.0-12.8 %** (0.531-0.683 ms, rt 2.51-2.70) at every shipped patch, audiodsp v0.6.3; the lean patch 8 is the cheapest on both. RAM 250 064 B | The RE-201's motor (`"varispeed"`, the default: a Time move bends the pitch for exactly the new time) and the EP-3's sliding head (`"sliding-head"`: the pitch bends only while the head moves, at a rate Glide sets) as characters. Each repeat darkens by a playback loss law that follows Spacing and the tape speed; Wow and Flutter drive one wow table; Record Level is the loop's squash. Mix 0 is a wire. | +| `AnalogDelay` | audiodsp (`audioecho`) | 8 | 0 samples | **P4 7.0-7.5 %** (0.374-0.399 ms, rt 5.19-5.31) **/ S3 14.0-14.5 %** (0.746-0.774 ms, rt 3.06-3.12), audiodsp v0.6.3. RAM 118 896 B | A bucket brigade: the Boss DM-2's one 4096-stage line (`"single-line"`) and the Deluxe Memory Man's two in series (`"double-line"`). Time is the line's clock, so the repeats darken as Time grows and a Time move bends their pitch instead of clicking; every Time lands on a whole frame. No sample-and-hold, no compander, so short Times are brighter than either pedal. | +| `PingPongDelay` | audiodsp (`audioecho`) | 9 | 0 samples | **P4 11.9-12.2 %** (0.636-0.649 ms, rt 4.20-4.22) **/ S3 16.4-16.6 %** (0.872-0.887 ms, rt 2.87-2.90), audiodsp v0.6.3; every digest equals the desktop's. RAM 194 112 B | Repeats that alternate between the speakers, on one node whose cross-feed and input pan Spread moves from two plain delays to the full bounce. The dry is each channel's own signal and the mono sum is an ordinary feedback delay exactly; Repeat Tone and Repeat Cut put a low-pass and a high-pass in the loop, defaulting out. | +| `MultiTapDelay` | audiodsp (`audioecho`, `audiodelays`) | 9 | 0 samples | **P4 36.0-72.5 %** (1.922-3.865 ms, rt 1.19-2.09) **/ S3 57.8-78.1 %** at patches 0-3, 5 and 6 (rt 1.01-1.34), audiodsp v0.6.3. **Patch 4 misses on the S3**: 5.567 ms, 104.4 % of a block, rt 0.80, so it needs a P4-class board, and patch 3 may; the docstring says so. RAM 661 376 B | The RE-201's multi-head modes, with the Echorec as a second reference: heads on a grid of whole multiples of one base time, Pattern the RE-202's twelve head sets, laps round a loop low-pass that darkens once per lap. Repeat Tone's out stop swaps to a lighter graph (patch 1, the one to stack on an S3). Mix 0 is a wire. | +| `Reverb` | audiodsp (`audioverb`) | 13 | 0 samples | **P4 19.0-23.4 %** (1.012-1.249 ms, rt 2.84-3.23) **/ S3 33.7-36.3 %** (1.799-1.935 ms, rt 1.84-1.93), audiodsp v0.6.3. RAM 49 120-149 904 B by patch | Dattorro's plate network on one `audioverb.Tank`, cut four ways by Character: an EMT 140 plate, dense at once, and a room, a chamber and a hall that build. Decay is T60 at 500 Hz within 12 % from 2 s to 10 s at the reference patches; Mix 0 is a byte-exact wire while the tank rings. The spring character is parked. **Digests:** patch 7's differs from the single-precision desktop build by nine one-LSB samples of 65536 (audiodsp#183). | +| `ConvolutionReverb` | audiodsp (`audioconvolve`) | 6 | 256 frames with an impulse loaded, 0 without | **P4 41.6-42.2 %** (2.217-2.249 ms, rt 1.85-1.88) **/ S3 71.2-73.1 %** (3.796-3.900 ms, rt 1.07-1.09) at the default 0.08 s, audiodsp v0.6.3. RAM 142 544 B | A short room synthesized from Decay, Damping, Predelay, Diffusion and one of 64 Room seeds, or your own impulse (int16 frames or a 16-bit WAV) with only Mix live. `seconds` is the allocation, from 0.06 s to the node's 512-partition ceiling. | + +The old classes (`Reverb`'s `preset`, `TapeDelay`'s `wow`, `AnalogDelay`'s +`age`, `MultiTapDelay`'s `(position, level)` taps) stay in `delay.py` and +`reverb.py` until the rebuilds come home, and `ShimmerHall` and `AirSpace` +still build on them (see [Effect racks](#effect-racks---rackpy)). A delay's `max_time_ms` sizes its line and cannot change afterwards; at 48 kHz a second of stereo line is 192 KB, so ask for what will be used. -The two reverbs are not the same kind of thing. `Reverb` is `audiofreeverb`: -a fixed network of delay lines that costs the same on a Cortex-M0 as on a -workstation, and that sounds like a plausible room. `ConvolutionReverb` is +The two reverbs are not the same kind of thing. `Reverb` is `audioverb`: a +fixed network of delay lines whose cost does not depend on how long the room +rings, and that sounds like a plausible room. `ConvolutionReverb` is `audioconvolve`: it applies an actual impulse response, so it sounds like a *particular* room - and one second of stereo impulse is about 1.5 MB and ~150 MFLOPS, which is a desktop or an offline render. Reach for `Reverb` @@ -262,7 +262,7 @@ micropython-vst3's soundtrack builds its custom racks: rack = audioeffects.create("Rack", source, 48000, chain=( ("Compressor", {"threshold_db": -24.0, "ratio": 3.0}), ("TapeDelay", {"time_ms": 340.0, "mix": 0.25}), - ("Reverb", {"preset": "hall", "mix": 0.3}), + ("Reverb", {"character": "hall", "mix": 0.3}), )) ``` @@ -275,7 +275,10 @@ unbounded), and its `reset()` clears every child's DSP history without reapplying the children's own patches over the options the rack built them with. `ShimmerHall` and `AirSpace` are the two racks the vst3 soundtrack shares between pieces, ported whole: fixed topologies whose -macros move their children's controls. +macros move their children's controls. They name the classes they were +ported against, `delay.TapeDelay` and `reverb.Reverb`, directly: a chain +entry may be a class in place of a `NAME`. Moving them onto the rebuilds +adopted on 2026-09-29 would re-voice them, and is not part of the adoption. ## Deliberately absent diff --git a/lib/audioeffects/_core.py b/lib/audioeffects/_core.py index 401e85d..a171a8b 100644 --- a/lib/audioeffects/_core.py +++ b/lib/audioeffects/_core.py @@ -10,7 +10,7 @@ comp = audioeffects.create("Compressor", source, 48000, threshold_db=-20, ratio=3) verb = audioeffects.create("Reverb", comp.output, 48000, - preset="hall", mix=0.3) + character="hall", mix=0.3) audio_out.play(verb.output) The underlying audiodsp nodes are kept as attributes so applications can bind diff --git a/lib/audioeffects/rack.py b/lib/audioeffects/rack.py index efcba67..3e1d5bc 100644 --- a/lib/audioeffects/rack.py +++ b/lib/audioeffects/rack.py @@ -10,7 +10,7 @@ rack = audioeffects.create("Rack", source, 48000, chain=( ("Compressor", {"threshold_db": -24.0, "ratio": 3.0}), ("TapeDelay", {"time_ms": 340.0, "mix": 0.25}), - ("Reverb", {"preset": "hall", "mix": 0.3}), + ("Reverb", {"character": "hall", "mix": 0.3}), )) audio_out.play(rack.output) @@ -19,6 +19,13 @@ fixed topologies with a macro surface over the children, so they carry patches the way any patchable effect here does. Racks may contain and be 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. """ VENDOR = "PyDevices" @@ -27,10 +34,15 @@ from ._component import macro_position as _macro_position 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 .reverb import Reverb as _OldReverb def _child(name, tail, options): - """One chain entry built around `tail` at the configured rate.""" + """One chain entry built around `tail` at the configured rate. `name` + is an effect NAME, or, for the soundtrack racks below, a class.""" + if not isinstance(name, str): + return name.create(tail, _core.sample_rate(), **options) import audioeffects return audioeffects.create(name, tail, _core.sample_rate(), **options) @@ -175,9 +187,9 @@ def __init__(self, source, shimmer=0.55, echo=0.36, space=0.42, # skipped for starting at zero could never come back. Rack.__init__(self, source, chain=( ("Octaver", {"down": 0.0, "up": 1.0}), - ("TapeDelay", {"time_ms": 420.0, "feedback": 0.5, "wow": 0.3, - "drive": 0.2}), - ("Reverb", {"preset": "hall"}), + (_OldTapeDelay, {"time_ms": 420.0, "feedback": 0.5, + "wow": 0.3, "drive": 0.2}), + (_OldReverb, {"preset": "hall"}), )) self.octave, self.tape, self.hall = self.effects self._init_macros((shimmer, echo, space, tone_hz), patch) @@ -215,8 +227,8 @@ def __init__(self, source, space=0.3, echo=0.25, frequency=4200.0, patch=None): Rack.__init__(self, source, chain=( ("LowPass", {"frequency": 4200.0}), - ("TapeDelay", {"time_ms": 375.0, "feedback": 0.4}), - ("Reverb", {"preset": "hall"}), + (_OldTapeDelay, {"time_ms": 375.0, "feedback": 0.4}), + (_OldReverb, {"preset": "hall"}), )) self.tone, self.tape, self.hall = self.effects self._init_macros((space, echo, frequency), patch) diff --git a/lib/audioeffects/rebuilt/__init__.py b/lib/audioeffects/rebuilt/__init__.py index 5fffad7..51df1d5 100644 --- a/lib/audioeffects/rebuilt/__init__.py +++ b/lib/audioeffects/rebuilt/__init__.py @@ -104,9 +104,26 @@ def _present(): #: `SlapbackDelay`, `CombFilter` and the FeedbackDelay classes Phase 5 is #: still building, and coming home moves it. That is a separate step, after #: those classes land. +#: +#: Phase 5's last six, adopted 2026-09-29: `TapeDelay`, `AnalogDelay`, +#: `PingPongDelay`, `MultiTapDelay`, `Reverb` and `ConvolutionReverb`. The +#: board session at audiodsp v0.6.3 met the cost bar (80 % of a 5.333 ms +#: stereo block) and real time at every cell on the ESP32-P4, and on the +#: ESP32-S3 at every cell but MultiTapDelay's patch 4 (5.567 ms), which its +#: docstring discloses on Brad's ruling. Each board digest equals the +#: desktop's, or a desktop MicroPython's built with single-precision floats +#: (audiocomponents#75); Reverb's patch 7 differs from that build by nine +#: one-LSB samples of 65536 (audiodsp#183). They stay under this directory +#: with the first two, for the same reason. ADOPTED = ( "DigitalDelay", "SlapbackDelay", + "TapeDelay", + "AnalogDelay", + "PingPongDelay", + "MultiTapDelay", + "Reverb", + "ConvolutionReverb", ) diff --git a/tests/test_cpython_effects_analogdelay.py b/tests/test_cpython_effects_analogdelay.py index 042b31f..3ce1471 100644 --- a/tests/test_cpython_effects_analogdelay.py +++ b/tests/test_cpython_effects_analogdelay.py @@ -21,8 +21,8 @@ class built as a wire. T1 is unmeasured and T2b, T4 and T5 are disconfirmed full spans, the stereo renders the rows name, the three interpreters and the M5 table live in the evidence pack, not in this file. -The rebuild is parked (not in `rebuilt.ADOPTED`), so the class is reached by -`rebuilt.module_class("AnalogDelay")`. +The class is reached by `rebuilt.module_class("AnalogDelay")`, which is also what +`audioeffects.AnalogDelay` serves since its adoption on 2026-09-29. The dossier's laws are written out here independently of the class: the corner is 0.2211 N / T, the Time map 20 * 30^position ms, the whole frame @@ -1349,11 +1349,17 @@ def test_macros_patches_tier_latency(self): effect.program_change(3) self.assertEqual(effect.patch_index, 3) - def test_parked_not_served(self): + def test_adopted_is_what_the_package_serves(self): + """Adopted on 2026-09-29, so `create()` serves this one. It was the + reverse assertion while the class was parked; revert + `rebuilt.ADOPTED` and this goes red.""" import audioeffects - self.assertNotIn("AnalogDelay", rebuilt.ADOPTED) - self.assertIn("AnalogDelay", rebuilt.parked()) - self.assertIsNot(audioeffects.AnalogDelay, AnalogDelay) + self.assertIn("AnalogDelay", rebuilt.ADOPTED) + self.assertNotIn("AnalogDelay", rebuilt.parked()) + self.assertIs(audioeffects.AnalogDelay, AnalogDelay) + served = audioeffects.create("AnalogDelay", silence_src(64), RATE) + self.assertIsInstance(served, AnalogDelay) + served.deinit() def test_patches_are_the_dossier_settings_on_the_grid(self): # Section 6's table, settings in the macros' own units, through diff --git a/tests/test_cpython_effects_convolutionreverb.py b/tests/test_cpython_effects_convolutionreverb.py index a7f59e3..999668a 100644 --- a/tests/test_cpython_effects_convolutionreverb.py +++ b/tests/test_cpython_effects_convolutionreverb.py @@ -998,10 +998,19 @@ def test_macros_patches_tier_capabilities(self): self.assertEqual(effect.patch_index, 3) effect.deinit() - def test_the_module_is_what_the_registry_builds(self): + def test_adopted_is_what_the_package_serves(self): + """Adopted on 2026-09-29, so `create()` serves this one. It was the + reverse assertion while the class was parked; revert + `rebuilt.ADOPTED` and this goes red.""" + import audioeffects from audioeffects import rebuilt as registry self.assertIs(registry.module_class(NAME), ConvolutionReverb) - self.assertIn(NAME, registry.parked()) + self.assertIn(NAME, registry.ADOPTED) + self.assertNotIn(NAME, registry.parked()) + self.assertIs(audioeffects.ConvolutionReverb, ConvolutionReverb) + served = audioeffects.create(NAME, switchable(), RATE) + self.assertIsInstance(served, ConvolutionReverb) + served.deinit() def test_patch_0_is_the_constructor_grid(self): effect = build() diff --git a/tests/test_cpython_effects_library.py b/tests/test_cpython_effects_library.py index 55807d7..f1aec81 100644 --- a/tests/test_cpython_effects_library.py +++ b/tests/test_cpython_effects_library.py @@ -36,6 +36,12 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "support")) from effects_measure import SAMPLE_RATE, build, peak, source # noqa: E402 +# `reverb.Reverb` is the old `_core.Effect` class, the one with `preset` and +# no macro surface, which `audioeffects.Reverb` served until the Phase 5 +# rebuild's adoption on 2026-09-29. The two tests that name it are about +# those two properties of that class. +from audioeffects import reverb # noqa: E402 + #: Every name the package exports as an effect. `ALL` is the catalogue the #: contract freezes - the provider names, computed by the package itself - #: so these tests read it rather than deriving the set from `dir()` again. @@ -82,7 +88,7 @@ def test_effects_chain_into_each_other(self): drive = audioeffects.Exciter(source(), tune=2500.0, harmonics=0.4) comp = audioeffects.Compressor(drive.output, threshold_db=-30.0, ratio=6.0, character="optical") - verb = audioeffects.Reverb(comp.output, preset="hall", mix=0.35) + verb = reverb.Reverb(comp.output, preset="hall", mix=0.35) self.assertGreater(peak(verb.output, 12), 0.001) def test_an_instrument_feeds_an_effect(self): @@ -156,7 +162,7 @@ def test_a_program_change_to_a_patch_that_is_not_there_is_ignored(self): self.assertEqual(effect._macros, before) def test_a_class_without_macros_declares_an_empty_macro_surface(self): - plain = audioeffects.Reverb(source()) + plain = reverb.Reverb(source()) self.assertEqual(plain.MACRO_LABELS, ()) self.assertEqual(plain.MACRO_MODES, {}) self.assertEqual(plain.PATCHES, {0: ("Default", ())}) diff --git a/tests/test_cpython_effects_multitapdelay.py b/tests/test_cpython_effects_multitapdelay.py index 1ccf950..49fbd6e 100644 --- a/tests/test_cpython_effects_multitapdelay.py +++ b/tests/test_cpython_effects_multitapdelay.py @@ -13,8 +13,8 @@ class built as a wire. The full spans, the three interpreters and the rates dossier, never taken from the class: the whole-frame landing, the lap clamp, S1's head sets, Tilt's line and the lap node's float32 hand-off. -The rebuild is parked (not in `rebuilt.ADOPTED`), so the class is reached by -`rebuilt.module_class("MultiTapDelay")`. +The class is reached by `rebuilt.module_class("MultiTapDelay")`, which is also what +`audioeffects.MultiTapDelay` serves since its adoption on 2026-09-29. Three plants are built for real here that Station A emulated: T4's per-head 6 kHz low-pass (a second tap node for head 2 behind an @@ -1293,9 +1293,17 @@ def test_macros_patches_tier_latency(self): effect.program_change(3) self.assertEqual(effect.patch_index, 3) - def test_parked_not_served(self): - self.assertNotIn("MultiTapDelay", rebuilt.ADOPTED) - self.assertIsNot(audioeffects.MultiTapDelay, MultiTapDelay) + def test_adopted_is_what_the_package_serves(self): + """Adopted on 2026-09-29, so `create()` serves this one. It was the + reverse assertion while the class was parked; revert + `rebuilt.ADOPTED` and this goes red.""" + self.assertIn("MultiTapDelay", rebuilt.ADOPTED) + self.assertNotIn("MultiTapDelay", rebuilt.parked()) + self.assertIs(audioeffects.MultiTapDelay, MultiTapDelay) + served = audioeffects.create( + "MultiTapDelay", Endless(silence(), RATE, 2), RATE) + self.assertIsInstance(served, MultiTapDelay) + served.deinit() def test_patch_table_is_the_dossier_on_the_grid(self): for index, (name, values) in enumerate(DOSSIER_PATCHES): diff --git a/tests/test_cpython_effects_pingpongdelay.py b/tests/test_cpython_effects_pingpongdelay.py index 759f99c..3a1db96 100644 --- a/tests/test_cpython_effects_pingpongdelay.py +++ b/tests/test_cpython_effects_pingpongdelay.py @@ -15,8 +15,8 @@ dossier, never taken from the class: the whole-frame landing, the Time span, the Spread law, the reference mono delay. -The rebuild is parked (not in `rebuilt.ADOPTED`), so the class is reached by -`rebuilt.module_class("PingPongDelay")`. +The class is reached by `rebuilt.module_class("PingPongDelay")`, which is also what +`audioeffects.PingPongDelay` serves since its adoption on 2026-09-29. """ import math @@ -902,11 +902,17 @@ def test_macros_patches_tier_latency(self): effect.program_change(3) self.assertEqual(effect.patch_index, 3) - def test_parked_not_served(self): + def test_adopted_is_what_the_package_serves(self): + """Adopted on 2026-09-29, so `create()` serves this one. It was the + reverse assertion while the class was parked; revert + `rebuilt.ADOPTED` and this goes red.""" import audioeffects - self.assertNotIn("PingPongDelay", rebuilt.ADOPTED) - self.assertIn("PingPongDelay", rebuilt.parked()) - self.assertIsNot(audioeffects.PingPongDelay, PingPongDelay) + self.assertIn("PingPongDelay", rebuilt.ADOPTED) + self.assertNotIn("PingPongDelay", rebuilt.parked()) + self.assertIs(audioeffects.PingPongDelay, PingPongDelay) + served = audioeffects.create("PingPongDelay", silence_src(64), RATE) + self.assertIsInstance(served, PingPongDelay) + served.deinit() def test_patches_are_the_dossier_settings_on_the_grid(self): for index, (name, values) in enumerate(DOSSIER_PATCHES): diff --git a/tests/test_cpython_effects_racks.py b/tests/test_cpython_effects_racks.py index b09ce91..b711b96 100644 --- a/tests/test_cpython_effects_racks.py +++ b/tests/test_cpython_effects_racks.py @@ -19,6 +19,12 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "support")) from effects_measure import SAMPLE_RATE, peak, source # noqa: E402 +# `reverb.Reverb` is the old `_core.Effect` class, the one with `preset`, +# which `audioeffects.Reverb` served until the Phase 5 rebuild's adoption on +# 2026-09-29. A chain entry may name a class in place of a NAME, which is +# how `ShimmerHall` and `AirSpace` keep it; these tests do the same. +from audioeffects import reverb # noqa: E402 + class RackTest(unittest.TestCase): """The rack kind: one component whose graph is several effects. @@ -31,7 +37,7 @@ class RackTest(unittest.TestCase): """ CHAIN = (("Overdrive", {"drive": 0.3, "mix": 0.4}), - ("Reverb", {"preset": "plate", "mix": 0.25})) + (reverb.Reverb, {"preset": "plate", "mix": 0.25})) def test_a_chain_spec_builds_children_in_order_and_renders(self): rack = audioeffects.create("Rack", source(), SAMPLE_RATE, @@ -67,7 +73,7 @@ def test_racks_nest(self): # "Racks may contain and be used by other racks" - both directions. inner = ("Rack", {"chain": (("Saturation", {"drive_db": 6.0}),)}) outer = audioeffects.Rack(source(), chain=( - inner, ("Reverb", {"preset": "room", "mix": 0.2}))) + inner, (reverb.Reverb, {"preset": "room", "mix": 0.2}))) self.assertEqual(type(outer.effects[0]).__name__, "Rack") self.assertGreater(peak(outer.output, 8), 0.001) diff --git a/tests/test_cpython_effects_reverb.py b/tests/test_cpython_effects_reverb.py index a35da72..e84a87e 100644 --- a/tests/test_cpython_effects_reverb.py +++ b/tests/test_cpython_effects_reverb.py @@ -695,6 +695,21 @@ def handed_cut(effect): # -- the surface ------------------------------------------------------------- class TheSurface(unittest.TestCase): + def test_adopted_is_what_the_package_serves(self): + """Adopted on 2026-09-29, so `create()` serves this one. It was the + reverse assertion while the class was parked; revert + `rebuilt.ADOPTED` and this goes red.""" + import audioeffects + self.assertIs(rebuilt.module_class("Reverb"), Reverb) + self.assertIn("Reverb", rebuilt.ADOPTED) + self.assertNotIn("Reverb", rebuilt.parked()) + self.assertIs(audioeffects.Reverb, Reverb) + served = audioeffects.create( + "Reverb", probes.ArraySource(interleave((), 2, 64), rate=RATE, + channels=2, block=64), RATE) + self.assertIsInstance(served, Reverb) + served.deinit() + def test_macros_patches_tier_latency(self): self.assertIs(rebuilt.module_class("Reverb"), Reverb) self.assertEqual(Reverb.MACRO_LABELS, ( diff --git a/tests/test_cpython_effects_tapedelay.py b/tests/test_cpython_effects_tapedelay.py index 04d2683..1497cc4 100644 --- a/tests/test_cpython_effects_tapedelay.py +++ b/tests/test_cpython_effects_tapedelay.py @@ -665,11 +665,19 @@ def test_macros_characters_tier_latency(self): with self.assertRaises(ValueError): TapeDelay(src_of(np.zeros(512)), character="reel") - def test_parked_not_served(self): + def test_adopted_is_what_the_package_serves(self): + """Adopted on 2026-09-29, so `create()` serves this one. It was the + reverse assertion while the class was parked; revert + `rebuilt.ADOPTED` and this goes red.""" + import audioeffects self.assertIs(rebuilt.module_class("TapeDelay"), TapeDelay) - self.assertNotIn("TapeDelay", rebuilt.ADOPTED) - self.assertIn("TapeDelay", rebuilt.parked()) - self.assertIsNone(rebuilt.load("TapeDelay")) + self.assertIn("TapeDelay", rebuilt.ADOPTED) + self.assertNotIn("TapeDelay", rebuilt.parked()) + self.assertIs(rebuilt.load("TapeDelay"), TapeDelay) + self.assertIs(audioeffects.TapeDelay, TapeDelay) + served = audioeffects.create("TapeDelay", src_of(np.zeros(64)), RATE) + self.assertIsInstance(served, TapeDelay) + served.deinit() def test_patches_are_the_dossier_settings_on_the_grid(self): # Section 6's table: Time, Fdbk, Mix, Glide, Wow, Flutter, Rec, diff --git a/tests/test_cpython_effects_time.py b/tests/test_cpython_effects_time.py index ad18246..45a4030 100644 --- a/tests/test_cpython_effects_time.py +++ b/tests/test_cpython_effects_time.py @@ -27,6 +27,13 @@ from effects_measure import (SAMPLE_RATE, burst, channels, loudest_in, peak, source, tone_gain_db) # noqa: E402 +# The tests below that name `delay.` or `reverb.` are written against the old +# `_core.Effect` classes those modules keep. `audioeffects.TapeDelay`, +# `AnalogDelay` and `ConvolutionReverb` serve the Phase 5 rebuilds since +# their adoption on 2026-09-29, which take different settings (no `wow`, +# `age` or `stereo`) and have no `clear()`. +from audioeffects import delay, reverb # noqa: E402 + class TimeTest(unittest.TestCase): def test_a_tape_delay_leaves_the_dry_path_alone(self): @@ -47,10 +54,10 @@ def test_a_tape_delay_darkens_each_repeat_more_than_the_last(self): # through it once per lap, so the third is darker than the first. # Measured as how much of each tone survives three laps. def survival(hz): - delay = audioeffects.TapeDelay(burst(hz), time_ms=100.0, - feedback=0.7, mix=1.0, wow=0.0, - tone_hz=3000.0, drive=0.0) - left, _ = channels(delay.output, 200) + line = delay.TapeDelay(burst(hz), time_ms=100.0, + feedback=0.7, mix=1.0, wow=0.0, + tone_hz=3000.0, drive=0.0) + left, _ = channels(line.output, 200) step = int(0.1 * SAMPLE_RATE) first = loudest_in(left, step, 2000) third = loudest_in(left, step * 3, 2000) @@ -80,26 +87,26 @@ def test_an_older_analog_delay_is_darker_and_narrower(self): # `age` is one knob over the loop's low-pass, its high-pass and its # drift. Only the first two are measurable as a level. def repeat_level(hz, age): - delay = audioeffects.AnalogDelay(burst(hz), time_ms=100.0, - feedback=0.6, mix=1.0, age=age, - drive=0.0) - left, _ = channels(delay.output, 200) + line = delay.AnalogDelay(burst(hz), time_ms=100.0, + feedback=0.6, mix=1.0, age=age, + drive=0.0) + left, _ = channels(line.output, 200) return loudest_in(left, int(0.2 * SAMPLE_RATE), 2000) self.assertLess(repeat_level(6000.0, 1.0), repeat_level(6000.0, 0.0)) self.assertLess(repeat_level(80.0, 1.0), repeat_level(80.0, 0.0)) def test_a_delay_line_can_be_emptied(self): - delay = audioeffects.TapeDelay(burst(1000.0), time_ms=100.0, - feedback=0.8, mix=1.0) - channels(delay.output, 40) - delay.clear() - self.assertEqual(peak(delay.output, 4), 0.0) + line = delay.TapeDelay(burst(1000.0), time_ms=100.0, + feedback=0.8, mix=1.0) + channels(line.output, 40) + line.clear() + self.assertEqual(peak(line.output, 4), 0.0) def test_a_synthesized_room_decays_at_the_time_it_was_asked_for(self): - verb = audioeffects.ConvolutionReverb(source(), seconds=0.5) + verb = reverb.ConvolutionReverb(source(), seconds=0.5) verb.set_macro(4, 127) # full wet, so only the tail is measured - low, high = audioeffects.ConvolutionReverb._MACRO_RANGES[0][:2] + low, high = reverb.ConvolutionReverb._MACRO_RANGES[0][:2] for knob in (127, 64, 0): verb.set_macro(0, knob) expected = 0.5 * (low + (high - low) * knob / 127.0) @@ -108,8 +115,8 @@ def test_a_synthesized_room_decays_at_the_time_it_was_asked_for(self): def test_a_synthesized_room_is_not_the_same_noise_on_both_sides(self): # A stereo impulse whose channels agreed would be a mono impulse, and # the whole reason to spend twice the memory is that they do not. - verb = audioeffects.ConvolutionReverb(source(), seconds=0.25, - stereo=True) + verb = reverb.ConvolutionReverb(source(), seconds=0.25, + stereo=True) verb.set_macro(4, 127) left, right = [], [] for _ in range(12): diff --git a/tests/test_rebuilt_registry.py b/tests/test_rebuilt_registry.py index 2e627e9..9678cf7 100644 --- a/tests/test_rebuilt_registry.py +++ b/tests/test_rebuilt_registry.py @@ -865,7 +865,11 @@ def test_phase2_names_have_left_adopted(self): # for Flanger and the phases to come. Phase 5's first two, # `DigitalDelay` and `SlapbackDelay`, were adopted on 2026-09-28 on # Brad's rulings, and are served from here until they come home. - self.assertEqual(rebuilt.ADOPTED, ("DigitalDelay", "SlapbackDelay")) + # Phase 5's last six followed on 2026-09-29, on the board session + # at audiodsp v0.6.3. + self.assertEqual(rebuilt.ADOPTED, ( + "DigitalDelay", "SlapbackDelay", "TapeDelay", "AnalogDelay", + "PingPongDelay", "MultiTapDelay", "Reverb", "ConvolutionReverb")) self.assertEqual(rebuilt.adopted(), rebuilt.ADOPTED) for name in rebuilt.ADOPTED: self.assertIs(rebuilt.load(name), rebuilt.module_class(name))