Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 32 additions & 23 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,54 +10,61 @@ 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
seven patches replace the old rack's Space, Echo and Tone; the loop
darkens each repeat, Top Cut darkens the send once, Feedback stretches the
chamber's tail, and a Time move bends the repeats' pitch. The chamber is
the rebuilt `Reverb`, and `reset()` renders exactly as a fresh instance.
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;
Delay holds the shifted voice back up to 112.5 ms; and with Feedback up
every repeat is shifted again, so a fifth climbs a fifth at a time.
`window_ms` (15 to 60, 30 by default) sets the splice window at
construction. The old `semitones=` option is gone: Pitch and Fine
replace it.
- **`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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 8 additions & 6 deletions lib/audioeffects/rack.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@
used by other racks: a `chain` entry may itself be a `("Rack", {...})`.

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

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


Expand Down Expand Up @@ -192,7 +194,7 @@ def __init__(self, source, shimmer=0.55, echo=0.36, space=0.42,
# macro moves the branch's mixer level, and a branch that was
# skipped for starting at zero could never come back.
Rack.__init__(self, source, chain=(
("Octaver", {"down": 0.0, "up": 1.0}),
(_OldOctaver, {"down": 0.0, "up": 1.0}),
(_OldTapeDelay, {"time_ms": 420.0, "feedback": 0.5,
"wow": 0.3, "drive": 0.2}),
(_OldReverb, {"preset": "hall"}),
Expand Down
12 changes: 12 additions & 0 deletions lib/audioeffects/rebuilt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -123,6 +128,13 @@ def _present():
"MultiTapDelay",
"Reverb",
"ConvolutionReverb",
"StereoWidener",
"Rack",
"Octaver",
"PitchShifter",
"Harmonizer",
"AirSpace",
"ShimmerHall",
)


Expand Down
24 changes: 12 additions & 12 deletions lib/audioeffects/rebuilt/shimmerhall.py
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down Expand Up @@ -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):
Expand All @@ -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.
Expand Down Expand Up @@ -279,20 +279,20 @@ 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):
# at 8 kHz the sixth pass folded back past -30 dB at 22.05 kHz.
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:
Expand Down
3 changes: 3 additions & 0 deletions tests/support/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions tests/test_cpython_effects_airspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
19 changes: 19 additions & 0 deletions tests/test_cpython_effects_harmonizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
19 changes: 19 additions & 0 deletions tests/test_cpython_effects_octaver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
13 changes: 7 additions & 6 deletions tests/test_cpython_effects_pitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,32 @@
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


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)

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__":
Expand Down
Loading
Loading