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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ there, and are recorded in its changelog.
and sits 3 dB lower (-3.97 dB), so a worst-phase tone driven into it
still reads -0.96 dB TP. The docstring now says which patches an S3
cannot play.
- **`Octaver` patch 6, *Both octaves - lean*:** Both octaves for an
ESP32-S3, where patch 1 cannot keep up. It has the same balance of the
note and the two octaves, 2 dB louder (Direct at 1, both octaves at
0.87); turn the level after it down 2 dB and you hear Both octaves. The
docstring says so.
- **`MultibandCompressor`'s docstring says to build it with `bands=2` on an
ESP32-S3**, where no three-band patch keeps up (107 % of a block; two
bands is 71 %). No patch can do this instead: every macro position builds
Expand Down
10 changes: 10 additions & 0 deletions lib/audioeffects/rebuilt/octaver.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ class Octaver(_component.Component):
A control that jumps makes the output step: move it in small steps
from the host if you need it smooth.

**On an ESP32-S3, Both octaves can't keep up and the sound breaks up;
play Both octaves - lean instead.** It has the same balance of your
note and the two octaves, 2 dB louder: turn the level after it down
2 dB and you hear Both octaves.

**Wire, latency, tail.** With both octaves at 0 and Direct at 1 the
output is the input, byte for byte, however the host's buffers are
cut.
Expand Down Expand Up @@ -215,13 +220,18 @@ class Octaver(_component.Component):
#: `_component.macro_of` of the card's settings: 0.7 is 89, 0.8 is 102,
#: 0.6 is 76; Tracking 90/50/200 Hz is 45/12/89; Tone 1 324/250/700 Hz
#: is 50/39/82; Tone 2 70.3 Hz is 24; Sensitivity -80 dBFS is 25.
#: Patch 6 is patch 1 with Direct at 1 and both octaves at 111 (0.874),
#: the same balance 1.9 dB up. A mixer voice at exactly 1 passes its
#: samples through with no multiply, which is the 0.07 ms an ESP32-S3
#: lacks for patch 1, where all three levels are under 1.
PATCHES = {
0: ("Octave down under the dry", (127, 89, 0, 45, 50, 24, 25)),
1: ("Both octaves", (102, 89, 89, 45, 50, 24, 25)),
2: ("Octaves alone, no dry", (0, 127, 76, 45, 50, 24, 25)),
3: ("Bass guitar", (127, 102, 0, 12, 39, 24, 25)),
4: ("High on the neck", (127, 89, 0, 89, 82, 24, 25)),
5: ("Two octaves down alone", (0, 0, 127, 45, 50, 24, 25)),
6: ("Both octaves - lean", (127, 111, 111, 45, 50, 24, 25)),
}

def _build(self, direct=1.0, octave1=0.7, octave2=0.0, tracking_hz=90.0,
Expand Down
21 changes: 14 additions & 7 deletions tests/support/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
#: A scope narrows a row; give each kind at most once, in either order:
#:
#: - a tuple of patches, the configurations the row covers (None for the
#: constructor defaults): ("Octaver", "E5-", "P1", (1, 2, 5));
#: constructor defaults): ("Octaver", "E5-", "P1", (1, 2, 5, 6));
#: - a string naming the interpreters it holds on, `sys.implementation.name`,
#: comma-separated: ("PitchShifter", "E10-reset", "P4", "circuitpython").
#:
Expand Down Expand Up @@ -383,6 +383,9 @@
("Octaver", "E6-p5", "P1"):
"the event changes to patch 5, where Octave 1 at 0 is a level move,"
" not the wire, so the output is not the source by design",
("Octaver", "E6-p6", "P1"):
"the event changes to patch 6, where Octave 1 at 0 is a level move,"
" not the wire, so the output is not the source by design",
("Octaver", "E12-p1", "P1"):
"the move before the first pull is to patch 1, where Octave 1 at 0"
" is a level move, not the wire, so the output is not the source by"
Expand All @@ -395,11 +398,15 @@
"the move before the first pull is to patch 5, where Octave 1 at 0"
" is a level move, not the wire, so the output is not the source by"
" design",
("Octaver", "E12-p6", "P1"):
"the move before the first pull is to patch 6, where Octave 1 at 0"
" is a level move, not the wire, so the output is not the source by"
" design",
("Octaver", "E11-mix+patch", "P1"):
"the event ends on patch 5, where Octave 1 at 0 is a level move,"
"the event ends on patch 6, where Octave 1 at 0 is a level move,"
" not the wire, so the output is not the source by design",
("Octaver", "E11-3moves", "P1"):
"the event ends on patch 5, where Octave 1 at 0 is a level move,"
"the event ends on patch 6, where Octave 1 at 0 is a level move,"
" not the wire, so the output is not the source by design",
("Octaver", "E5-", "P5"):
"family limit, disclosed (audiocomponents#117): a control that jumps"
Expand Down Expand Up @@ -688,12 +695,12 @@
for _event in CONTROL_EVENTS:
DECLARED[(_name, _event, "P5")] = _why

#: At patches 1, 2 and 5 Octave 1 at 0 is a level move, not the wire, so P1
#: (Mix 0 is the source) does not apply there, on any event.
#: At patches 1, 2, 5 and 6 Octave 1 at 0 is a level move, not the wire, so
#: P1 (Mix 0 is the source) does not apply there, on any event.
for _event in ("E1-", "E2-", "E4-", "E5-", "E6-", "E8-", "E9-", "E10-",
"E11-", "E12-"):
DECLARED[("Octaver", _event, "P1", (1, 2, 5))] = (
"at patches 1, 2 and 5 Octave 2 is up or Direct is under 1, so"
DECLARED[("Octaver", _event, "P1", (1, 2, 5, 6))] = (
"at patches 1, 2, 5 and 6 Octave 2 is up or Direct is under 1, so"
" Octave 1 at 0 is a level move, not the wire, and the output is"
" not the source by design")

Expand Down
37 changes: 37 additions & 0 deletions tests/test_cpython_effects_octaver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1218,6 +1218,37 @@ def test_a_patch_builds_as_its_values(self):
render(Octaver, values, rate, 1,
options=self._values_of(patch)))

def test_the_lean_patch_is_both_octaves_2_db_louder(self):
# Patch 6 is patch 1 with Direct at 1, so the mixer passes the dry
# through with no multiply, and the octaves raised to keep patch
# 1's balance. Turned down by patch 1's Direct level it renders
# patch 1, to the octave levels' rounding on the 128-step grid.
name, grid = Octaver.PATCHES[6]
self.assertEqual(name, "Both octaves - lean")
self.assertEqual(Octaver.PATCHES[1][0], "Both octaves")
one = Octaver.PATCHES[1][1]
self.assertEqual(grid[DIRECT], 127)
self.assertEqual(grid[OCT1], grid[OCT2])
self.assertEqual(grid[TRACKING:], one[TRACKING:])
down = one[DIRECT] / 127.0
self.assertAlmostEqual(20.0 * math.log10(down), -1.90, places=2)
self.assertLess(abs(grid[OCT1] * down - one[OCT1]), 0.2)
for rate in RATES:
for channels in (1, 2):
with self.subTest(rate=rate, channels=channels):
values = self._material(rate, channels)
both = render(Octaver, values, rate, channels,
options={"patch": 1})
lean = render(Octaver, values, rate, channels,
options={"patch": 6})
peak = max(abs(v) for v in both)
self.assertGreater(peak, 1000)
self.assertLess(max(lean), 32767)
self.assertGreater(min(lean), -32768)
worst = max(abs(lean[i] * down - both[i])
for i in range(len(both)))
self.assertLessEqual(worst, 0.003 * peak + 3.0)

def test_a_patch_before_the_first_pull_renders_as_fresh(self):
# Arming plays only silence, so the all-pass has heard nothing yet:
# a Tracking move before the first pull lands at once, not walked
Expand Down Expand Up @@ -1410,6 +1441,12 @@ def test_the_peak_gain(self):
("A control that jumps makes the output step: move it in small steps "
"from the host if you need it smooth.",
"DECLARED:Octaver,E4-,P5"),
("On an ESP32-S3, Both octaves can't keep up and the sound breaks up; "
"play Both octaves - lean instead.",
"TheSurface.test_the_lean_patch_is_both_octaves_2_db_louder"),
("It has the same balance of your note and the two octaves, 2 dB "
"louder: turn the level after it down 2 dB and you hear Both octaves.",
"TheSurface.test_the_lean_patch_is_both_octaves_2_db_louder"),
("With both octaves at 0 and Direct at 1 the output is the input, byte "
"for byte, however the host's buffers are cut.",
("TheSurface.test_the_wire_is_the_input",
Expand Down
Loading