diff --git a/CHANGELOG.md b/CHANGELOG.md index f84fab6..db62a61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,19 +10,22 @@ there, and are recorded in its changelog. ### Added -- **`ShimmerHall` (rebuilt, Phase 6):** a pitch shifter around a hall, in +- **`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 (the last, "Octave Bloom - lean", is Rise at Rise 0, for a board - short of memory) replace the old Shimmer / Echo / Space / Tone surface and its - constructor arguments. The hall is the rebuilt `Reverb`. The dry passes + 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 lives in `lib/audioeffects/rebuilt/shimmerhall.py`, beside - the old class in `rack.py`, until it is promoted. -- **`AirSpace` (rebuilt, Phase 6; not yet adopted):** a tape echo sent + 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 @@ -30,16 +33,18 @@ there, and are recorded in its changelog. 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. - The old `rack.AirSpace` stays what the library serves until adoption. -- **`Harmonizer` (rebuilt, Phase 6):** three pitch shifters at fixed + 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. -- **`PitchShifter` (rebuilt, Phase 6):** a fixed-interval splice shifter + 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; @@ -47,17 +52,19 @@ there, and are recorded in its changelog. 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. -- **`StereoWidener` (rebuilt, Phase 6):** two characters on a Width, Delay + 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. -- **`Rack` (rebuilt, Phase 6):** the serial chain on the contract's base, in + 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. `chain=` keeps its meaning and also takes a bare effect class. It + 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 @@ -67,13 +74,14 @@ there, and are recorded in its changelog. 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):** the analog octave-down, after the +- **`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. + 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 @@ -289,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 diff --git a/lib/audioeffects/rack.py b/lib/audioeffects/rack.py index 9730114..fce6c78 100644 --- a/lib/audioeffects/rack.py +++ b/lib/audioeffects/rack.py @@ -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" @@ -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 @@ -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"}), diff --git a/lib/audioeffects/rebuilt/__init__.py b/lib/audioeffects/rebuilt/__init__.py index d3e7616..ad2e4c0 100644 --- a/lib/audioeffects/rebuilt/__init__.py +++ b/lib/audioeffects/rebuilt/__init__.py @@ -114,6 +114,11 @@ def _present(): #: (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. +#: +#: Phase 6's seven, adopted 2026-10-13: `StereoWidener`, `Rack`, `Octaver`, +#: `PitchShifter`, `Harmonizer`, `AirSpace` and `ShimmerHall`. They stay +#: under this directory, and `pitch.py` and `rack.py` keep the old classes, +#: until they come home. ADOPTED = ( "DigitalDelay", "SlapbackDelay", @@ -123,6 +128,13 @@ def _present(): "MultiTapDelay", "Reverb", "ConvolutionReverb", + "StereoWidener", + "Rack", + "Octaver", + "PitchShifter", + "Harmonizer", + "AirSpace", + "ShimmerHall", ) diff --git a/lib/audioeffects/rebuilt/shimmerhall.py b/lib/audioeffects/rebuilt/shimmerhall.py index f2ef6bb..e858b5c 100644 --- a/lib/audioeffects/rebuilt/shimmerhall.py +++ b/lib/audioeffects/rebuilt/shimmerhall.py @@ -35,11 +35,11 @@ `tail_samples` bounds how long the output takes to reach exact zero once your input stops: 82 s at Rise 0.9 and Bloom 800 ms. `reset()` empties the hall, the line and the shifter, and restores patch 0. The patches: Octave -Bloom (the defaults), Short Stagger, Fifth Halo, Slow Rise, Rise Damped, -Falling Octaves, Octave Bloom - lean. On an ESP32-S3 the Bloom patches -(the defaults, Octave Bloom, Short Stagger and Fifth Halo) take longer than -a block and cannot keep up, so play a Rise patch there. Octave Bloom - lean -is Rise at Rise 0, for an S3: one octave, once, and the hall starts at once. +Rise (the defaults), Short Stagger, Fifth Halo, Slow Rise, Rise Damped, +Falling Octaves, Octave Bloom. Octave Rise is Octave Bloom in Rise at Rise +0: one octave, once, and the hall starts at once. On an ESP32-S3 the Bloom +patches (Octave Bloom, Short Stagger and Fifth Halo) take longer than a +block and cannot keep up; the defaults and every Rise patch keep up. **Limits** @@ -143,7 +143,7 @@ #: Each macro's patch-0 step; the constructor defaults are these steps' #: values, so a fresh instance is patch 0. -DEFAULT_STEPS = (0, 89, 85, 90, 96, 94, 64, 91, 32) +DEFAULT_STEPS = (127, 89, 0, 90, 96, 94, 64, 91, 32) def _default(index, span): @@ -160,7 +160,7 @@ def _default(index, span): SIZE = _default(6, (0.5, 1.5)) DAMPING_HZ = _default(7, (500.0, 16000.0, "log")) MIX = _default(8, (0.0, 2.0)) -DEFAULTS = (0.0, SHIMMER, RISE, BLOOM_MS, _default(4, (0.0, 5.0)), DECAY, +DEFAULTS = (1.0, SHIMMER, RISE, BLOOM_MS, _default(4, (0.0, 5.0)), DECAY, SIZE, DAMPING_HZ, MIX) #: The hall's macros the rack drives, by `Reverb`'s labels. @@ -279,7 +279,7 @@ class ShimmerHall(_component.Component): #: `_component.macro_of` of the card's settings. PATCHES = { - 0: ("Octave Bloom", DEFAULT_STEPS), + 0: ("Octave Rise", DEFAULT_STEPS), 1: ("Short Stagger", (0, 76, 85, 38, 96, 77, 38, 102, 25)), 2: ("Fifth Halo", (0, 102, 85, 103, 64, 108, 89, 84, 38)), # Damping step 86 (about 5.2 kHz), where the card had 102 (8 kHz): @@ -287,12 +287,12 @@ class ShimmerHall(_component.Component): 3: ("Slow Rise", (127, 89, 85, 103, 96, 108, 89, 86, 38)), 4: ("Rise Damped", (127, 102, 113, 87, 96, 94, 64, 51, 32)), 5: ("Falling Octaves", (127, 89, 85, 93, 0, 102, 64, 76, 32)), - # Patch 0 in Rise at Rise 0: the S3's patch nearest the defaults, - # since every Bloom patch runs over a block there. - 6: ("Octave Bloom - lean", (127, 89, 0, 90, 96, 94, 64, 91, 32)), + # Patch 0 in Bloom, at the card's Rise: every Bloom patch runs over + # a block on an ESP32-S3, so the defaults are its Rise twin. + 6: ("Octave Bloom", (0, 89, 85, 90, 96, 94, 64, 91, 32)), } - def _build(self, character="bloom", shimmer=SHIMMER, rise=RISE, + def _build(self, character="rise", shimmer=SHIMMER, rise=RISE, bloom_ms=BLOOM_MS, interval=12, decay=DECAY, size=SIZE, damping_hz=DAMPING_HZ, mix=MIX, patch=None): if character not in CHARACTERS: diff --git a/tests/support/lifecycle.py b/tests/support/lifecycle.py index e0aefe1..d4ca726 100644 --- a/tests/support/lifecycle.py +++ b/tests/support/lifecycle.py @@ -585,6 +585,8 @@ ("ShimmerHall", "E11-", "P4"): "moves that reach the hall, empty it, or move the shifter's interval or splice leave them off the control's for good (Reverb's E11 P4; N2)", ("ShimmerHall", "E4-m0=", "P5"): "family limit, disclosed (N1): a control that jumps makes the output step (Character)", ("ShimmerHall", "E4-m3=", "P5"): "family limit, disclosed (N1): a control that jumps makes the output step (Bloom)", + ("ShimmerHall", "E4-m6=", "P5"): "family limit, disclosed (N1): a control that jumps makes the output step (Size, which also empties the hall, as the docstring says); at the Rise defaults the hall sounds at once, so the jump lands on sound", + ("ShimmerHall", "E4-m8=", "P5"): "family limit, disclosed (N1): a control that jumps makes the output step (Mix); at the Rise defaults the hall sounds at once, so the jump lands on sound", ("ShimmerHall", "E5-", "P5"): "family limit, disclosed (N1): a control that jumps makes the output step (Mix)", ("ShimmerHall", "E6-", "P5"): "family limit, disclosed (N1): a control that jumps makes the output step (a patch change)", ("ShimmerHall", "E9-", "P5"): "family limit, disclosed (N1): a control that jumps makes the output step (Mix)", @@ -595,6 +597,7 @@ ("ShimmerHall", "E10-patch", "P4", "circuitpython"): _SHIMMER_CP, ("ShimmerHall", "E12-p1", "P4", "circuitpython"): _SHIMMER_CP, ("ShimmerHall", "E12-p2", "P4", "circuitpython"): _SHIMMER_CP, + ("ShimmerHall", "E12-p6", "P4", "circuitpython"): _SHIMMER_CP, ("ShimmerHall", "E12-mix64", "P4", "circuitpython"): _SHIMMER_CP, ("ShimmerHall", "E12-mix127", "P4", "circuitpython"): _SHIMMER_CP, # AirSpace: its chamber is the rebuilt Reverb, whose rows above say why diff --git a/tests/test_cpython_effects_airspace.py b/tests/test_cpython_effects_airspace.py index 030746c..9bb7791 100644 --- a/tests/test_cpython_effects_airspace.py +++ b/tests/test_cpython_effects_airspace.py @@ -687,6 +687,25 @@ def fresh(rate=48000, channels=2, patch=None, **options): return build(AirSpace, data, rate, channels, patch, **options) +class TheAdoption(unittest.TestCase): + def test_adopted_is_what_the_package_serves(self): + """Adopted on 2026-10-13, so `create()` serves this one. Revert + `rebuilt.ADOPTED` and this goes red.""" + import audiocore + import audioeffects + from array import array + self.assertIn("AirSpace", rebuilt.ADOPTED) + self.assertNotIn("AirSpace", rebuilt.parked()) + self.assertIs(audioeffects.AirSpace, AirSpace) + served = audioeffects.create( + "AirSpace", + audiocore.RawSample(array("h", [0, 0] * 64), sample_rate=48000, + channel_count=2), + 48000) + self.assertIsInstance(served, AirSpace) + served.deinit() + + class TheSurface(unittest.TestCase): def test_the_spans(self): diff --git a/tests/test_cpython_effects_harmonizer.py b/tests/test_cpython_effects_harmonizer.py index 2ace555..b3120cf 100644 --- a/tests/test_cpython_effects_harmonizer.py +++ b/tests/test_cpython_effects_harmonizer.py @@ -1117,6 +1117,25 @@ def test_the_bare_tap_is_red(self): # The surface, the wire, the tail +class TheAdoption(TestCase): + def test_adopted_is_what_the_package_serves(self): + """Adopted on 2026-10-13, so `create()` serves this one. Revert + `rebuilt.ADOPTED` and this goes red.""" + import audiocore + import audioeffects + from array import array + self.assertIn("Harmonizer", rebuilt.ADOPTED) + self.assertNotIn("Harmonizer", rebuilt.parked()) + self.assertIs(audioeffects.Harmonizer, Harmonizer) + served = audioeffects.create( + "Harmonizer", + audiocore.RawSample(array("h", [0, 0] * 64), sample_rate=48000, + channel_count=2), + 48000) + self.assertIsInstance(served, Harmonizer) + served.deinit() + + class TheSurface(TestCase): def _fresh(self, rate=48000, channels=2, **options): diff --git a/tests/test_cpython_effects_octaver.py b/tests/test_cpython_effects_octaver.py index c8ebbe6..e0a2094 100644 --- a/tests/test_cpython_effects_octaver.py +++ b/tests/test_cpython_effects_octaver.py @@ -957,6 +957,25 @@ def test_tone_1_at_twice_its_hertz_is_red(self): self.assertGreater(max(misses), 0.25, misses) +class TheAdoption(TestCase): + def test_adopted_is_what_the_package_serves(self): + """Adopted on 2026-10-13, so `create()` serves this one. Revert + `rebuilt.ADOPTED` and this goes red.""" + import audiocore + import audioeffects + from array import array + self.assertIn("Octaver", rebuilt.ADOPTED) + self.assertNotIn("Octaver", rebuilt.parked()) + self.assertIs(audioeffects.Octaver, Octaver) + served = audioeffects.create( + "Octaver", + audiocore.RawSample(array("h", [0, 0] * 64), sample_rate=48000, + channel_count=2), + 48000) + self.assertIsInstance(served, Octaver) + served.deinit() + + class TheSurface(TestCase): """The controls, the wire, latency, tail, reset and tempo.""" diff --git a/tests/test_cpython_effects_pitch.py b/tests/test_cpython_effects_pitch.py index 329b40b..bc270c5 100644 --- a/tests/test_cpython_effects_pitch.py +++ b/tests/test_cpython_effects_pitch.py @@ -18,7 +18,9 @@ import sys import unittest -import audioeffects +# `pitch.Octaver` is the old class, which `audioeffects.Octaver` served until +# the Phase 6 rebuild's adoption on 2026-10-13. +from audioeffects import pitch sys.path.insert(0, os.path.join(os.path.dirname(__file__), "support")) from effects_measure import peak, source # noqa: E402 @@ -26,11 +28,11 @@ class PitchTest(unittest.TestCase): def test_an_octaver_reaches_two_octaves_either_way(self): - deep = audioeffects.Octaver(source(), down=0.4, down2=0.4) + deep = pitch.Octaver(source(), down=0.4, down2=0.4) self.assertEqual(deep.down2.semitones, -24.0) self.assertIsNone(deep.up) self.assertGreater(peak(deep.output, 12), 0.001) - high = audioeffects.Octaver(source(), down=0.0, up=0.4, up2=0.4) + high = pitch.Octaver(source(), down=0.0, up=0.4, up2=0.4) self.assertEqual(high.up2.semitones, 24.0) self.assertIsNone(high.down) @@ -38,11 +40,10 @@ def test_an_octaver_builds_only_the_octaves_it_was_asked_for(self): # A Splitter fans out to four taps, so the dry signal plus three # octaves is the ceiling - and the default, one octave down, should # cost one shifter rather than four. - one = audioeffects.Octaver(source()) + one = pitch.Octaver(source()) self.assertEqual(len(one.mixer.voice), 2) with self.assertRaises(ValueError): - audioeffects.Octaver(source(), down=0.3, down2=0.3, up=0.3, - up2=0.3) + pitch.Octaver(source(), down=0.3, down2=0.3, up=0.3, up2=0.3) if __name__ == "__main__": diff --git a/tests/test_cpython_effects_pitchshifter.py b/tests/test_cpython_effects_pitchshifter.py index 597a7df..53b9b42 100644 --- a/tests/test_cpython_effects_pitchshifter.py +++ b/tests/test_cpython_effects_pitchshifter.py @@ -622,6 +622,25 @@ def run(e): # The tests +class TheAdoption(TestCase): + def test_adopted_is_what_the_package_serves(self): + """Adopted on 2026-10-13, so `create()` serves this one. Revert + `rebuilt.ADOPTED` and this goes red.""" + import audiocore + import audioeffects + from array import array + self.assertIn("PitchShifter", rebuilt.ADOPTED) + self.assertNotIn("PitchShifter", rebuilt.parked()) + self.assertIs(audioeffects.PitchShifter, PitchShifter) + served = audioeffects.create( + "PitchShifter", + audiocore.RawSample(array("h", [0, 0] * 64), sample_rate=48000, + channel_count=2), + 48000) + self.assertIsInstance(served, PitchShifter) + served.deinit() + + class TheSurface(TestCase): def test_macros_patches_tier_latency(self): self.assertEqual(PitchShifter.MACRO_LABELS, diff --git a/tests/test_cpython_effects_rack.py b/tests/test_cpython_effects_rack.py index f9de48f..0c3b938 100644 --- a/tests/test_cpython_effects_rack.py +++ b/tests/test_cpython_effects_rack.py @@ -1240,6 +1240,25 @@ def _native_binaries(): found[family] = path if path and os.path.isfile(path) else None return found, root +class TheAdoption(TestCase): + def test_adopted_is_what_the_package_serves(self): + """Adopted on 2026-10-13, so `create()` serves this one. Revert + `rebuilt.ADOPTED` and this goes red.""" + import audiocore + import audioeffects + from array import array + self.assertIn("Rack", rebuilt.ADOPTED) + self.assertNotIn("Rack", rebuilt.parked()) + self.assertIs(audioeffects.Rack, Rack) + served = audioeffects.create( + "Rack", + audiocore.RawSample(array("h", [0, 0] * 64), sample_rate=48000, + channel_count=2), + 48000) + self.assertIsInstance(served, Rack) + served.deinit() + + class TestEmptyRack(TestCase): """T1. With nothing in it, a rack is a patch cable.""" diff --git a/tests/test_cpython_effects_racks.py b/tests/test_cpython_effects_racks.py index b711b96..88baad8 100644 --- a/tests/test_cpython_effects_racks.py +++ b/tests/test_cpython_effects_racks.py @@ -13,7 +13,6 @@ import sys import unittest -import audioeffects from tools.validate_metadata import validate_component sys.path.insert(0, os.path.join(os.path.dirname(__file__), "support")) @@ -25,12 +24,18 @@ # how `ShimmerHall` and `AirSpace` keep it; these tests do the same. from audioeffects import reverb # noqa: E402 +# `rack.Rack`, `rack.ShimmerHall` and `rack.AirSpace` are the old classes, +# which `audioeffects.Rack` and the other two served until the Phase 6 +# rebuilds' adoption on 2026-10-13. These tests are about the old kind, so +# they name it from its module. +from audioeffects import rack as rack_module # noqa: E402 + class RackTest(unittest.TestCase): """The rack kind: one component whose graph is several effects. Ported from micropython-vst3's soundtrack racks; the mechanism is - `audioeffects.Rack` and the two shared presets are `ShimmerHall` and + `rack.Rack` and the two shared presets are `ShimmerHall` and `AirSpace`. A rack has exactly the effect shape, so everything the catalogue-wide tests assert already covers these three classes - what is here is the rack-specific behavior. @@ -40,8 +45,8 @@ class RackTest(unittest.TestCase): (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, - chain=self.CHAIN) + rack = rack_module.Rack.create(source(), SAMPLE_RATE, + chain=self.CHAIN) self.assertEqual([type(child).__name__ for child in rack.effects], ["Overdrive", "Reverb"]) # Each child is fed the previous one's output, and the rack's @@ -51,13 +56,13 @@ def test_a_chain_spec_builds_children_in_order_and_renders(self): self.assertGreater(peak(rack.output, 8), 0.001) def test_a_bare_name_is_a_valid_chain_entry(self): - rack = audioeffects.Rack(source(), chain=("Saturation", "Reverb")) + rack = rack_module.Rack(source(), chain=("Saturation", "Reverb")) self.assertEqual(len(rack.effects), 2) self.assertGreater(peak(rack.output, 8), 0.001) def test_an_empty_rack_is_a_wire(self): src = source() - rack = audioeffects.create("Rack", src, SAMPLE_RATE) + rack = rack_module.Rack.create(src, SAMPLE_RATE) self.assertIs(rack.output, src) self.assertEqual(rack.latency_samples, 0) self.assertEqual(rack.tail_samples, 0) @@ -65,20 +70,20 @@ def test_an_empty_rack_is_a_wire(self): def test_a_malformed_chain_entry_is_refused(self): with self.assertRaises(ValueError): - audioeffects.Rack(source(), chain=(42,)) + rack_module.Rack(source(), chain=(42,)) with self.assertRaises(ImportError): - audioeffects.Rack(source(), chain=("NoSuchEffect",)) + rack_module.Rack(source(), chain=("NoSuchEffect",)) 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 = (rack_module.Rack, {"chain": (("Saturation", {"drive_db": 6.0}),)}) + outer = rack_module.Rack(source(), chain=( inner, (reverb.Reverb, {"preset": "room", "mix": 0.2}))) self.assertEqual(type(outer.effects[0]).__name__, "Rack") self.assertGreater(peak(outer.output, 8), 0.001) def test_latency_and_tail_are_the_whole_graph(self): - rack = audioeffects.Rack(source(), chain=self.CHAIN) + rack = rack_module.Rack(source(), chain=self.CHAIN) self.assertEqual(rack.latency_samples, sum(child.latency_samples for child in rack.effects)) @@ -89,7 +94,7 @@ def test_latency_and_tail_are_the_whole_graph(self): def test_deinit_releases_the_children_but_not_the_source(self): src = source() - rack = audioeffects.Rack(src, chain=self.CHAIN) + rack = rack_module.Rack(src, chain=self.CHAIN) children = list(rack.effects) rack.deinit() rack.deinit() @@ -102,15 +107,14 @@ def test_deinit_releases_the_children_but_not_the_source(self): self.assertGreater(peak(src, 2), 0.001) def test_rack_metadata_is_valid_component_metadata(self): - from audioeffects import rack as module - for cls in (audioeffects.Rack, audioeffects.ShimmerHall, - audioeffects.AirSpace): + for cls in (rack_module.Rack, rack_module.ShimmerHall, + rack_module.AirSpace): validate_component(cls, kind="effect", expected_name=cls.__name__, - vendor_owner=module) + vendor_owner=rack_module) def test_a_preset_rack_macro_moves_its_child(self): - rack = audioeffects.ShimmerHall(source()) + rack = rack_module.ShimmerHall(source()) self.assertEqual(rack.patch_index, 0) rack.set_macro(0, 0) self.assertEqual(rack.octave.mixer.voice[1].level, 0.0) @@ -124,7 +128,7 @@ def test_a_preset_rack_macro_moves_its_child(self): def test_the_preset_racks_chain_audio_through_their_children(self): for name in ("ShimmerHall", "AirSpace"): - rack = getattr(audioeffects, name)(source()) + rack = getattr(rack_module, name)(source()) self.assertGreaterEqual(len(rack.effects), 3) self.assertGreater(peak(rack.output, 8), 0.001, name) diff --git a/tests/test_cpython_effects_shimmerhall.py b/tests/test_cpython_effects_shimmerhall.py index d244855..2ee7371 100644 --- a/tests/test_cpython_effects_shimmerhall.py +++ b/tests/test_cpython_effects_shimmerhall.py @@ -274,15 +274,16 @@ def _dry_source(self): #: The kit's plant table. A Rise plant is judged at a Rise patch, a Bloom -#: plant at the Bloom patch its fault shows plainest at. +#: plant at the Bloom patch its fault shows plainest at: the defaults are +#: Rise, so a Bloom plant always names its patch. KIT_PLANTS = { "T1": (PinnedStagger, 1), "T2": (AlwaysOctave, 2), "T3": (PinnedLoop, 3), "T4": (HeldFeedback, 4), - "T4b": RiseInBloom, + "T4b": (RiseInBloom, 6), "T5": (UndampedLoop, 4), - "T6": LateDry, + "T6": (LateDry, 6), } @@ -295,8 +296,10 @@ def _dry_source(self): def t1_onset(cls, rate, f0, offset, bloom): frames = law_frames(bloom + 400.0, rate) + offset - s1 = wet(cls, rate, card_channels(rate), f0, offset, frames, bloom_ms=bloom, shimmer=1.0) - s0 = wet(cls, rate, card_channels(rate), f0, offset, frames, bloom_ms=bloom, shimmer=0.0) + s1 = wet(cls, rate, card_channels(rate), f0, offset, frames, + character="bloom", bloom_ms=bloom, shimmer=1.0) + s0 = wet(cls, rate, card_channels(rate), f0, offset, frames, + character="bloom", bloom_ms=bloom, shimmer=0.0) hz = 2.0 * f0 size = hann_size(rate) window = hann(size) @@ -344,7 +347,7 @@ def t2_read(cls, rate, character, interval, seed): """The energy-weighted mean shift of d = (Shimmer 1) - (Shimmer 0), in semitones, over the 500 ms from Bloom.""" f0, offset = seed_set(seed) - # The card's patch-0 Bloom, which a default-built instance plays. + # The defaults' Bloom, Octave Bloom's too. bloom = 20.0 * 40.0 ** (90.0 / 127.0) start = law_frames(bloom, rate) + offset span = law_frames(500.0, rate) @@ -753,27 +756,32 @@ def run(e): return values, out +def _bloom(src, **options): + """Built in Bloom: the defaults are Rise.""" + return ShimmerHall(src, character="bloom", **options) + + GET_BUFFER_CASES = ( ("built at the defaults", ShimmerHall, {}), - ("built in Rise", lambda src: ShimmerHall(src, character="rise"), {}), - ("reset before the first pull", ShimmerHall, {0: lambda e: e.reset()}), - ("reset twice before the first pull", ShimmerHall, + ("built in Bloom", _bloom, {}), + ("reset before the first pull", _bloom, {0: lambda e: e.reset()}), + ("reset twice before the first pull", _bloom, {0: lambda e: (e.reset(), e.reset())}), - ("built at Mix 0, then patch 0", lambda src: ShimmerHall(src, mix=0.0), - {3: lambda e: e.program_change(0)}), - ("built at Mix 0, reset", lambda src: ShimmerHall(src, mix=0.0), + ("built at Mix 0, then Octave Bloom", + lambda src: _bloom(src, mix=0.0), {3: lambda e: e.program_change(6)}), + ("built at Mix 0, reset", lambda src: _bloom(src, mix=0.0), {3: lambda e: e.reset()}), - ("Mix 0 before the first block, back", ShimmerHall, + ("Mix 0 before the first block, back", _bloom, {0: lambda e: e.set_macro(MIX_I, 0), 4: lambda e: e.set_macro(MIX_I, 32)}), - ("Mix 0 after playing, reset twice", ShimmerHall, + ("Mix 0 after playing, reset twice", _bloom, {2: lambda e: e.set_macro(MIX_I, 0), 6: lambda e: (e.reset(), e.reset())}), - ("a Character move and a reset in one block", ShimmerHall, + ("a Character move and a reset in one block", _bloom, {2: lambda e: (e.program_change(4), e.reset())}), ("built at Mix 2, reset after playing", - lambda src: ShimmerHall(src, mix=2.0), {3: lambda e: e.reset()}), - ("Mix 0 after playing, back to 127", ShimmerHall, + lambda src: _bloom(src, mix=2.0), {3: lambda e: e.reset()}), + ("Mix 0 after playing, back to 127", _bloom, {2: lambda e: e.set_macro(MIX_I, 0), 6: lambda e: e.set_macro(MIX_I, 127)}), - ("Mix 127 before the first pull", ShimmerHall, + ("Mix 127 before the first pull", _bloom, {0: lambda e: e.set_macro(MIX_I, 127)}), ) @@ -786,6 +794,25 @@ def _port(rate=48000, channels=2): return kc.feed(array("h", bytes(4 * channels)), rate, channels).port +class TheAdoption(TestCase): + def test_adopted_is_what_the_package_serves(self): + """Adopted on 2026-10-13, so `create()` serves this one. Revert + `rebuilt.ADOPTED` and this goes red.""" + import audiocore + import audioeffects + from array import array + self.assertIn("ShimmerHall", rebuilt.ADOPTED) + self.assertNotIn("ShimmerHall", rebuilt.parked()) + self.assertIs(audioeffects.ShimmerHall, ShimmerHall) + served = audioeffects.create( + "ShimmerHall", + audiocore.RawSample(array("h", [0, 0] * 64), sample_rate=48000, + channel_count=2), + 48000) + self.assertIsInstance(served, ShimmerHall) + served.deinit() + + class TheSurface(TestCase): def test_macros_patches_tier_latency(self): self.assertEqual(ShimmerHall.MACRO_LABELS, @@ -798,9 +825,9 @@ def test_macros_patches_tier_latency(self): self.assertEqual(ShimmerHall.CAPABILITIES, ()) self.assertEqual(ShimmerHall.LATENCY_SAMPLES, 0) self.assertEqual([ShimmerHall.PATCHES[i][0] for i in range(7)], - ["Octave Bloom", "Short Stagger", "Fifth Halo", + ["Octave Rise", "Short Stagger", "Fifth Halo", "Slow Rise", "Rise Damped", "Falling Octaves", - "Octave Bloom - lean"]) + "Octave Bloom"]) self.assertEqual(sorted(ShimmerHall.PATCHES), list(range(7))) def test_the_spans(self): @@ -854,13 +881,16 @@ def test_the_constructor(self): e.deinit() def test_patches_are_the_card_settings_on_the_grid(self): + # Patch 0 is the card's Octave Bloom in Rise at Rise 0 (ruled + # 2026-10-13); the card's own Octave Bloom is patch 6. card = { - 0: ("bloom", 0.7, 0.6, 275, 12, 4.0, 1.0, 6000, 0.5), + 0: ("rise", 0.7, 0.0, 275, 12, 4.0, 1.0, 6000, 0.5), 1: ("bloom", 0.6, 0.6, 60, 12, 2.5, 0.8, 8000, 0.4), 2: ("bloom", 0.8, 0.6, 400, 7, 6.0, 1.2, 5000, 0.6), 3: ("rise", 0.7, 0.6, 400, 12, 6.0, 1.2, 8000, 0.6), 4: ("rise", 0.8, 0.8, 250, 12, 4.0, 1.0, 2000, 0.5), 5: ("rise", 0.7, 0.6, 300, -12, 5.0, 1.0, 4000, 0.5), + 6: ("bloom", 0.7, 0.6, 275, 12, 4.0, 1.0, 6000, 0.5), } zones = dict(zip(INTERVALS, (0, 32, 64, 96, 127))) for index, row in card.items(): @@ -886,7 +916,7 @@ def test_a_fresh_instance_reads_patch_0(self): ShimmerHall.PATCHES[0][1]) self.assertEqual(e.patch_index, 0) values = [e.macro(i) for i in range(9)] - want = [0.0, 0.7008, 0.6024, 273.1, 12.0, 4.0206, 1.00394, + want = [1.0, 0.7008, 0.0, 273.1, 12.0, 4.0206, 1.00394, 5990.5, 0.5039] for got, card in zip(values, want): self.assertAlmostEqual(got, card, delta=abs(card) * 1e-4) @@ -904,7 +934,7 @@ def test_patches_play_the_same_whatever_the_constructor_said(self): # half-cycle would play at the constructor's Mix. for options in ({}, dict(shimmer=0.37, rise=0.71, bloom_ms=33.3, interval=-5, decay=7.7, size=0.61, - damping_hz=777.0, character="rise")): + damping_hz=777.0, character="bloom")): feed = kc.feed(values, 48000, 2) e = ShimmerHall(feed.port, **options) e.program_change(patch) @@ -945,7 +975,8 @@ def test_mix_0_is_the_input(self): def test_no_latency(self): values = array("h", bytes(2 * 2 * 4096)) values[2000] = values[2001] = 12000 - out = render(ShimmerHall, values, 48000, 2, 4096, mix=1.0) + out = render(ShimmerHall, values, 48000, 2, 4096, mix=1.0, + character="bloom") self.assertEqual(out[2000], 12000) self.assertEqual(list(out[:2000]), [0] * 2000) @@ -1015,7 +1046,7 @@ def test_a_character_move_recuts_the_wet_chain(self): # After a Character move, the wet path renders as a fresh # instance's in that character, and the dry carries on. values = t6_material(48000, 2, 2) - for start, end in (("bloom", 3), ("rise", 0)): + for start, end in (("bloom", 3), ("rise", 6)): fresh_wet = render(ShimmerHall, values[2 * 9728:], 48000, 2, 19200, patch=end) feed = kc.feed(values, 48000, 2) @@ -1089,28 +1120,29 @@ def _same_as_fresh(self, build, moves, fresh_from, fresh_options, def test_a_reset_from_mix_2_plays_the_dry_from_frame_0(self): self._same_as_fresh( - lambda port: ShimmerHall(port, mix=2.0), + lambda port: _bloom(port, mix=2.0), {6: lambda e: e.reset()}, 6, {}, name="reset from Mix 2") def test_a_reset_before_the_first_pull_plays_the_dry_from_frame_0(self): self._same_as_fresh( - lambda port: ShimmerHall(port, mix=2.0), + lambda port: _bloom(port, mix=2.0), {0: lambda e: e.reset()}, 0, {}, name="reset before a pull") def test_a_return_from_mix_0_plays_the_dry_from_frame_0(self): self._same_as_fresh( - ShimmerHall, + _bloom, {2: lambda e: e.set_macro(MIX_I, 0), - 6: lambda e: e.set_macro(MIX_I, 127)}, 6, dict(mix=2.0), - name="Mix 0 then 127") + 6: lambda e: e.set_macro(MIX_I, 127)}, 6, + dict(mix=2.0, character="bloom"), name="Mix 0 then 127") def test_a_mix_move_before_the_first_pull_plays_the_dry_from_frame_0( self): self._same_as_fresh( - ShimmerHall, {0: lambda e: e.set_macro(MIX_I, 127)}, 0, - dict(mix=2.0), name="Mix 127 before the first pull") + _bloom, {0: lambda e: e.set_macro(MIX_I, 127)}, 0, + dict(mix=2.0, character="bloom"), + name="Mix 127 before the first pull") self._same_as_fresh( - ShimmerHall, {0: lambda e: e.program_change(1)}, 0, + _bloom, {0: lambda e: e.program_change(1)}, 0, dict(patch=1), name="patch 1 before the first pull") def test_a_reset_from_rise_plays_as_a_new_instance(self): @@ -1136,7 +1168,7 @@ def wrapped(): ("interval", lambda e: e.set_macro(INTERVAL_I, 0), 0), ("patch 2", lambda e: e.program_change(2), 1), ("mix 127", lambda e: e.set_macro(MIX_I, 127), 1)): - e = ShimmerHall(_port()) + e = _bloom(_port()) try: count = counted(e) move(e) @@ -1144,7 +1176,7 @@ def wrapped(): finally: e.deinit() self.assertEqual(count[0], rearms, name) - e = ShimmerHall(_port(), mix=2.0) + e = _bloom(_port(), mix=2.0) try: count = counted(e) e.output @@ -1179,7 +1211,8 @@ def test_a_rack_holding_it_resets_to_a_fresh_racks_render(self): values = array("h", [rng.next() % 20001 - 10000 for _ in range(frames * channels)]) other = array("h", [-v // 2 for v in values]) - for options in ({}, dict(mix=2.0), off_grid): + for options in ({}, dict(mix=2.0), dict(character="bloom"), + dict(character="bloom", mix=2.0), off_grid): chain = ((ShimmerHall, options),) fresh = rack_class.create( kc.feed(values, rate, channels).port, rate, chain=chain) @@ -1211,10 +1244,12 @@ def test_an_output_taken_before_mix_moves_plays_the_old_level_at_first( blocks = 14 values = self._dc(rate, channels, blocks) for name, options, move, fresh in ( - ("Mix 127", {}, lambda e: e.set_macro(MIX_I, 127), - dict(mix=2.0)), - ("patch 0 from Mix 2", dict(mix=2.0), - lambda e: e.program_change(0), {})): + ("Mix 127", dict(character="bloom"), + lambda e: e.set_macro(MIX_I, 127), + dict(mix=2.0, character="bloom")), + ("Octave Bloom from Mix 2", + dict(mix=2.0, character="bloom"), + lambda e: e.program_change(6), dict(patch=6))): e = ShimmerHall(kc.feed(values, rate, channels).port, **options) try: @@ -1377,7 +1412,7 @@ def test_the_dry_never_carries_the_shimmer(self): self.assertLess(loud, 32767, (rate, channels, patch)) def test_late_dry_is_red(self): - worst, _loud = t6_read(LateDry, 48000, 2, 0, 1) + worst, _loud = t6_read(LateDry, 48000, 2, 6, 1) self.assertGreater(worst, 1000) @@ -1466,32 +1501,35 @@ def lean_levels(rate, f0, offset, patch): return [db(x) - db(levels[0]) for x in levels] -class TheLeanPatch(TestCase): +class TheDefaults(TestCase): def test_the_bloom_patches_are_the_ones_named_for_the_s3(self): # The S3 cost is measured on the boards (the evidence's board # table: every Bloom patch over the bar, every Rise patch under); - # this pins the sentence's list to the patches in Bloom. + # this pins the sentence's list to the patches in Bloom, and the + # defaults to Rise. bloom = [name for name, steps in ShimmerHall.PATCHES.values() if steps[CHARACTER_I] < 64] - self.assertEqual(bloom, ["Octave Bloom", "Short Stagger", - "Fifth Halo"]) + self.assertEqual(bloom, ["Short Stagger", "Fifth Halo", + "Octave Bloom"]) e = ShimmerHall(_port()) try: - self.assertEqual(e.macro(CHARACTER_I), 0.0) # the defaults + self.assertEqual(e.macro(CHARACTER_I), 1.0) # the defaults finally: e.deinit() doc = " ".join(sys.modules[ShimmerHall.__module__].__doc__.split()) - self.assertIn("the Bloom patches (the defaults, Octave Bloom, Short" - " Stagger and Fifth Halo) take longer than a block", + self.assertIn("the Bloom patches (Octave Bloom, Short Stagger and" + " Fifth Halo) take longer than a block and cannot" + " keep up; the defaults and every Rise patch keep up.", doc) - def test_the_lean_patch_is_patch_0_in_rise_at_rise_0(self): - steps = list(ShimmerHall.PATCHES[0][1]) + def test_the_defaults_are_octave_bloom_in_rise_at_rise_0(self): + steps = list(ShimmerHall.PATCHES[6][1]) steps[CHARACTER_I] = 127 steps[RISE_I] = 0 - self.assertEqual(ShimmerHall.PATCHES[6][1], tuple(steps)) - self.assertEqual(ShimmerHall.PATCHES[6][0], "Octave Bloom - lean") - e = ShimmerHall(_port(), patch=6) + self.assertEqual(ShimmerHall.PATCHES[0][1], tuple(steps)) + self.assertEqual(ShimmerHall.PATCHES[0][0], "Octave Rise") + self.assertEqual(ShimmerHall.PATCHES[6][0], "Octave Bloom") + e = ShimmerHall(_port()) try: self.assertEqual(e.macro(CHARACTER_I), 1.0) self.assertEqual(e.macro(RISE_I), 0.0) @@ -1499,24 +1537,24 @@ def test_the_lean_patch_is_patch_0_in_rise_at_rise_0(self): finally: e.deinit() - def test_the_lean_patch_is_one_octave_once(self): + def test_the_defaults_are_one_octave_once(self): # Rise 0.9 climbs: the second and the third octave are in the - # shimmer within 30 dB of the first. At the lean patch only the + # shimmer within 30 dB of the first. At the defaults only the # first is there, and the others stand more than 40 dB under it. for rate, f0 in ((48000, 247.0), (22050, 220.0)): - lean = lean_levels(rate, f0, 13, 6) + lean = lean_levels(rate, f0, 13, 0) self.assertLess(max(lean[1:]), -40.0, (rate, lean)) climbing = lean_levels(48000, 247.0, 13, 3) self.assertGreater(climbing[1], -30.0, climbing) - def test_the_lean_hall_starts_at_once(self): - # Patch 0 holds the whole hall for Bloom (273 ms); the lean patch - # plays the dry into it at once. + def test_the_defaults_hall_starts_at_once(self): + # Octave Bloom holds the whole hall for Bloom (273 ms); the + # defaults play the dry into it at once. for rate in RATES: channels = card_channels(rate) frames = law_frames(150.0, rate) heard = [] - for patch in (0, 6): + for patch in (6, 0): out = wet(ShimmerHall, rate, channels, 247.0, 0, frames, **patch_options(ShimmerHall, patch, mix=2.0)) heard.append(max(abs(x) for x in out)) @@ -1603,18 +1641,19 @@ def test_the_lean_hall_starts_at_once(self): "the first 256 frames, 5 ms at 48 kHz, play at the old level.", ("test_an_output_taken_before_mix_moves_plays_the_old_level_at_first", "test_only_a_move_that_changes_a_voice_level_re_arms")), - ("The patches: Octave Bloom (the defaults), Short Stagger, Fifth Halo, " - "Slow Rise, Rise Damped, Falling Octaves, Octave Bloom - lean.", - "test_macros_patches_tier_latency"), - ("On an ESP32-S3 the Bloom patches (the defaults, Octave Bloom, Short " - "Stagger and Fifth Halo) take longer than a block and cannot keep up, " - "so play a Rise patch there.", + ("The patches: Octave Rise (the defaults), Short Stagger, Fifth Halo, " + "Slow Rise, Rise Damped, Falling Octaves, Octave Bloom.", + ("test_macros_patches_tier_latency", + "test_a_fresh_instance_reads_patch_0")), + ("Octave Rise is Octave Bloom in Rise at Rise 0: one octave, once, and " + "the hall starts at once.", + ("test_the_defaults_are_octave_bloom_in_rise_at_rise_0", + "test_the_defaults_are_one_octave_once", + "test_the_defaults_hall_starts_at_once")), + ("On an ESP32-S3 the Bloom patches (Octave Bloom, Short Stagger and " + "Fifth Halo) take longer than a block and cannot keep up; the defaults " + "and every Rise patch keep up.", "test_the_bloom_patches_are_the_ones_named_for_the_s3"), - ("Octave Bloom - lean is Rise at Rise 0, for an S3: one octave, once, " - "and the hall starts at once.", - ("test_the_lean_patch_is_patch_0_in_rise_at_rise_0", - "test_the_lean_patch_is_one_octave_once", - "test_the_lean_hall_starts_at_once")), ("Interval is a fixed shift in semitones, with no key awareness.", "test_interval_has_five_zones"), ("In Rise, on some held notes the shimmer lands beside the octave " @@ -1725,11 +1764,11 @@ def figures(which=("T1", "T2", "T3", "T4", "T5", "T6", "L")): worst = max(worst, w) loud = max(loud, lo) runs += 1 - plant = t6_read(LateDry, 48000, 2, 0, 1)[0] + plant = t6_read(LateDry, 48000, 2, 6, 1)[0] print("%s|T6|runs %d|worst %d LSB|loudest %d|plant %d LSB" % ( name, runs, worst, loud, plant)) if "F" in which: - for patch in (3, 4, 6): + for patch in (3, 4, 0): options = fold_shipped(patch) for rate in RATES: worst = -999.0 @@ -1740,7 +1779,7 @@ def figures(which=("T1", "T2", "T3", "T4", "T5", "T6", "L")): for level in rel: worst = max(worst, level) # Exact silence where the pass never comes (Rise damped to - # nothing, or the lean patch's loop). + # nothing, or the defaults' loop at Rise 0). print("%s|F|patch %d %s|%d|worst fold line %s over %d " "notes|bar %.0f" % ( name, patch, ShimmerHall.PATCHES[patch][0], rate, @@ -1779,7 +1818,7 @@ def figures(which=("T1", "T2", "T3", "T4", "T5", "T6", "L")): for c in range(channels): values[1000 * channels + c] = 12000 out = render(ShimmerHall, values, rate, channels, 4096, - mix=1.0) + mix=1.0, character="bloom") first = None for n in range(4096): if out[n * channels]: diff --git a/tests/test_cpython_effects_stereowidener.py b/tests/test_cpython_effects_stereowidener.py index bb0d56c..cf1ffe7 100644 --- a/tests/test_cpython_effects_stereowidener.py +++ b/tests/test_cpython_effects_stereowidener.py @@ -513,6 +513,25 @@ def _same_after(a, b): return sum(1 for i in range(len(a)) if a[i] != b[i]) +class TheAdoption(TestCase): + def test_adopted_is_what_the_package_serves(self): + """Adopted on 2026-10-13, so `create()` serves this one. Revert + `rebuilt.ADOPTED` and this goes red.""" + import audiocore + import audioeffects + from array import array + self.assertIn("StereoWidener", rebuilt.ADOPTED) + self.assertNotIn("StereoWidener", rebuilt.parked()) + self.assertIs(audioeffects.StereoWidener, StereoWidener) + served = audioeffects.create( + "StereoWidener", + audiocore.RawSample(array("h", [0, 0] * 64), sample_rate=48000, + channel_count=2), + 48000) + self.assertIsInstance(served, StereoWidener) + served.deinit() + + class TheSurface(TestCase): def test_macros_patches_tier_latency(self): self.assertEqual(StereoWidener.MACRO_LABELS, diff --git a/tests/test_rebuilt_registry.py b/tests/test_rebuilt_registry.py index 77d5920..2d50394 100644 --- a/tests/test_rebuilt_registry.py +++ b/tests/test_rebuilt_registry.py @@ -866,10 +866,12 @@ def test_phase2_names_have_left_adopted(self): # `DigitalDelay` and `SlapbackDelay`, were adopted on 2026-09-28 on # rulings of that day, and are served from here until they come home. # Phase 5's last six followed on 2026-09-29, on the board session - # at audiodsp v0.6.3. + # at audiodsp v0.6.3. Phase 6's seven were adopted on 2026-10-13. self.assertEqual(rebuilt.ADOPTED, ( "DigitalDelay", "SlapbackDelay", "TapeDelay", "AnalogDelay", - "PingPongDelay", "MultiTapDelay", "Reverb", "ConvolutionReverb")) + "PingPongDelay", "MultiTapDelay", "Reverb", "ConvolutionReverb", + "StereoWidener", "Rack", "Octaver", "PitchShifter", + "Harmonizer", "AirSpace", "ShimmerHall")) self.assertEqual(rebuilt.adopted(), rebuilt.ADOPTED) for name in rebuilt.ADOPTED: self.assertIs(rebuilt.load(name), rebuilt.module_class(name))