From daf9bfa2971cd7d48aff90aecbbcc2319d49a875 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:11:06 -0500 Subject: [PATCH 1/3] ShimmerHall: the defaults become Octave Rise, Octave Bloom in Rise at Rise 0, which an ESP32-S3 keeps up with; Octave Bloom moves to patch 6 Patch 0 is now Rise at Rise 0 (steps 127, 89, 0, 90, 96, 94, 64, 91, 32), and the old defaults are patch 6, Octave Bloom. The S3 sentence names the Bloom patches as they now are. Tests that measured Bloom at the defaults ask for it by name, so every trait is still held where it was. --- CHANGELOG.md | 8 +- lib/audioeffects/rebuilt/shimmerhall.py | 24 +-- tests/test_cpython_effects_shimmerhall.py | 170 ++++++++++++---------- 3 files changed, 112 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7d19b7..de55fb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,9 +16,11 @@ there, and are recorded in its changelog. 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. 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/test_cpython_effects_shimmerhall.py b/tests/test_cpython_effects_shimmerhall.py index d244855..dfe6925 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)}), ) @@ -798,9 +806,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 +862,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 +897,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 +915,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 +956,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 +1027,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 +1101,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 +1149,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 +1157,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 +1192,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 +1225,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 +1393,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 +1482,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 +1518,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 +1622,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 +1745,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 +1760,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 +1799,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]: From 72a68a9489596eb42c397f28ea4e16ada3800565 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:31:25 -0500 Subject: [PATCH 2/3] StereoWidener, Rack, Octaver, PitchShifter, Harmonizer, AirSpace and ShimmerHall are adopted: Phase 6's seven ADOPTED names all seven, so audioeffects. and create() serve the rebuilds. They stay under rebuilt/, and pitch.py and rack.py keep the old classes until the rebuilds are promoted. The old ShimmerHall built its octave branch by NAME with the old Octaver's down= and up=, which the rebuild refuses, so it now names pitch.Octaver directly, as it already does for the tape delay and the reverb. Its render and AirSpace's old rack are unchanged when built from rack.py. Tests: each class gains an "adopted is what the package serves" test (create() returns the rebuild), and the registry names all fifteen adopted classes; with ADOPTED cut back to the first eight, all of them go red. The tests written against the old classes by their served name now name them from their own module: test_cpython_effects_pitch (the shifter-based Octaver's octave reach and shifter count) and test_cpython_effects_racks (the old Rack, ShimmerHall and AirSpace). Their assertions are unchanged. The CHANGELOG entries for the seven say they are adopted (2026-10-13), and the note on the soundtrack racks now includes the octaver. Gates, this tree: validate_api 55 instruments / 45 effects; metadata valid; flake8 clean; contract and the seven classes' scoped tests, the kit and the registry OK; the library smoke 45 classes, 318 patches, 0 failures on CPython, MicroPython and CircuitPython. The CPython digest census (burst plus two seconds, every served class at the defaults and every patch) moves only the six served names whose rebuilds differ from the old classes, onto the rebuilds' own digests; Rack's default chain is a wire either way, and every other class is byte-identical. --- CHANGELOG.md | 47 ++++++++++++--------- lib/audioeffects/rack.py | 14 +++--- lib/audioeffects/rebuilt/__init__.py | 12 ++++++ tests/test_cpython_effects_airspace.py | 19 +++++++++ tests/test_cpython_effects_harmonizer.py | 19 +++++++++ tests/test_cpython_effects_octaver.py | 19 +++++++++ tests/test_cpython_effects_pitch.py | 13 +++--- tests/test_cpython_effects_pitchshifter.py | 19 +++++++++ tests/test_cpython_effects_rack.py | 19 +++++++++ tests/test_cpython_effects_racks.py | 40 ++++++++++-------- tests/test_cpython_effects_shimmerhall.py | 19 +++++++++ tests/test_cpython_effects_stereowidener.py | 19 +++++++++ tests/test_rebuilt_registry.py | 6 ++- 13 files changed, 213 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f84fab6..6481d6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ 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 @@ -20,9 +20,10 @@ there, and are recorded in its changelog. short of memory) replace the old Shimmer / Echo / Space / Tone surface and its constructor arguments. 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 +31,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 +50,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 +72,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 +295,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/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..af3650d 100644 --- a/tests/test_cpython_effects_shimmerhall.py +++ b/tests/test_cpython_effects_shimmerhall.py @@ -786,6 +786,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, 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)) From d529f3bb80742ee679efeb9cac1cbd64c33625c0 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:28:51 -0500 Subject: [PATCH 3/3] Lifecycle: ShimmerHall's rows for the Rise defaults At the Rise defaults the hall sounds at once, so a Size or Mix jump early in the render lands on sound and steps the output: the disclosed family limit (a control that jumps makes the output step; moving Size empties the hall). On CircuitPython, Octave Bloom loaded before the first pull now re-arms Bloom's shifter from the defaults, and CircuitPython's own PitchShift keeps its positions through a clear, as for the other Bloom patches. --- tests/support/lifecycle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/support/lifecycle.py b/tests/support/lifecycle.py index 80b339d..5390a2e 100644 --- a/tests/support/lifecycle.py +++ b/tests/support/lifecycle.py @@ -361,6 +361,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)", @@ -371,6 +373,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, }