Skip to content

Commit 56e8848

Browse files
committed
BandPass and GraphicEQ: what audiobiquad's transposed direct form II fixed
audioif#64 moved audiobiquad's float sections to transposed direct form II. Three tests written to stay red on the old kernel's defects went green, and each was right to say so. BandPass T1 held the -0.50 dB miss at the knob stops, and said it would be the test to announce audioif#64. Re-running the low-corner probe at the new floor: 55 of 56 cells inside the 0.05 dB bar, stops included, and one miss, -0.091 dB at f0 31.5 Hz with Q 32, now pinned by its own test. The class docstring and the README row say so. GraphicEQ's retired plant is green on every band now, not six of ten. Its live plant, a patch jump on stale state, no longer slams from 5 to 1; from 2 to 1 it still reaches 2.72x steady against the check's 2x bar, and 1.32x through the class, so the plant moves there and is held to that bar. The CHANGELOG records all of #66.
1 parent 46bbae1 commit 56e8848

5 files changed

Lines changed: 76 additions & 44 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,24 @@ there, and are recorded in its changelog.
8181
nearest tom for every tom number a kit does not have. Each machine now
8282
sounds exactly the notes in its `NOTE_MAP`, and nothing else.
8383

84+
### Fixed
85+
86+
- **The effects catch up with the audioif floor at cebb7ca** (#66). Moving
87+
`AUDIOIF_PIN` there for `acoustickit` brought five deliberate audioif
88+
changes, and 27 tests went red. `AutoPan`'s Centre was mirrored, because
89+
CircuitPython 10.3.0 reversed synthio's panning sign; its notes are paired
90+
the other way now. A fresh mixer voice or synthio note now starts at level 0
91+
and waits for a zero crossing, so the first block of `AutoPan`, `DeEsser`
92+
and `MultibandCompressor` came out silent and then clicked in; each opens
93+
its gates on one block of silence before it takes its source
94+
(`_component.open_level_gates`). `Saturation`'s shelves moved to
95+
`audiobiquad`, because `synthio.Biquad` is CircuitPython's Q15 arithmetic
96+
again (audioif#77) and tape had lost its head bump. `BandPass` T1 now
97+
holds in 55 of 56 cells, both stops included (audioif#64). The rest were
98+
tests holding audioif's old behaviour: released nodes raise `ValueError`,
99+
`Splitter` releases, `Dynamics.reset()` clears its key filters, and there
100+
are 55 instruments.
101+
84102
### Removed
85103

86104
- **`Rotary` is removed.** The Leslie-style rotating-speaker effect is no longer in `audioeffects`; nothing replaces it yet.

lib/audioeffects/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ marginal share of one block on each board at construction defaults.
125125
| `DynamicEQ` | audioif (`audiobiquad`, `audiodynamics`, `audioroute`) | 8 | 0 samples | 14.5 % P4 / 26.0 % S3 of a block at patch 0 against the palette-derived 17 % / 29 % (listed remainder; `Splitter` taps=3 is a named gap). T2/T5 miss above ~5 ms attack; T4/T6 out-of-band miss below Q 2 | none - the exactly complementary split: one bell that does nothing until the sound *in that band* crosses a threshold, and a measured **wire** when it is idle |
126126
| `LowPass` | audioif (`audiobiquad`) | 5 | 0 samples | 5.6 % P4 / 9.6 % S3 of a block, **over** its 1.5 % / 5 % budget, and no `" - lean"` patch is possible: every section runs whatever its `mix` is, so no macro position is cheaper than any other | none - the two-pole analog prototype `H(s) = 1/(s² + 2Rs + 1)`: one knob slides the curve, one decides how loud the corner stands, −3 dB at Resonance 0.707 and +24 dB at 16. **Exact except at the bottom of the Frequency knob**: at f₀ 20 Hz with Q 16 at 24 dB/oct the corner stands 0.44 dB low and the curve stops being one shape below 25 Hz (float32 coefficients near z = 1), and "rings for Q periods" is a 12 dB/oct statement below about 0.17·F_s - at 24 dB/oct the ring runs 1.36× the Resonance number at Q 2 up to 1.71× at Q 16. The roll-off, −12.03 dB/oct on the warped axis with unity DC, holds everywhere |
127127
| `HighPass` | audioif (`audiobiquad`) | 5 | 0 samples | 5.7 % P4 / 9.6 % S3 of a block, **over** its 1.5 % / 5 % budget, and no patch on this surface is cheaper - patch 0 is already one live section and two wires | none - RBJ's two-pole low-cut, with an exact transmission zero at DC: a held offset decays to zero rather than to the 71 LSB the ported node parks on at a 10 Hz corner. Two bounds, measured: the two numbers on the panel are exact only above about 40 Hz at 48 kHz - at a 10 Hz corner the peak is 0.6 dB shy of `Resonance` (Q 8, 24 dB/oct) and the curve is 0.21 dB off its own shape, both the node's `float` coefficients rather than the cut - and at 24 dB/oct the corner **rings 1.85x longer** than the knob says, which is the price of reading the same gain at both slopes. The cut, the 12/24 dB/oct skirt and the exact zero at DC hold everywhere |
128-
| `BandPass` | audioif (`audiobiquad`) | 4 | 0 samples | 3.8 % P4 / 6.2 % S3 of a block, inside its 4 % / 13 % budget, and the same at every patch | none - the two-pole resonant band-pass in RBJ's constant 0 dB peak-gain form, so `Width` moves the skirts without moving the peak. **Above 100 Hz.** Below it, at Q >= 4, the peak loses up to 0.50 dB and the loss changes sign with the level: the kernel's float32 recursion, not the form (audioif#64). The +-6 dB/oct and -3 dB figures are Q 0.707 statements below about 2 kHz - above that the bilinear warp moves them, and this class tracks the warped prototype to 0.009 dB |
128+
| `BandPass` | audioif (`audiobiquad`) | 4 | 0 samples | 3.8 % P4 / 6.2 % S3 of a block, inside its 4 % / 13 % budget, and the same at every patch | none - the two-pole resonant band-pass in RBJ's constant 0 dB peak-gain form, so `Width` moves the skirts without moving the peak. The peak holds within 0.05 dB in 55 of 56 measured cells, both knob stops included; the one miss is -0.09 dB at f0 31.5 Hz with Q 32 (audioif#64 fixed the rest). The +-6 dB/oct and -3 dB figures are Q 0.707 statements below about 2 kHz - above that the bilinear warp moves them, and this class tracks the warped prototype to 0.009 dB |
129129
| `Notch` | audioif (`audiobiquad`) | 5 | 0 samples | 5.6 % P4 / 9.6 % S3 of a block, **over** its 1.5 % / 5 % budget; a `" - lean"` patch is still owed and none was invented | none (the Twin-T was weighed and dropped on scope) - a band-stop whose `Width` is a bandwidth and not a depth, with a Harmonics toggle for mains hum. A `float` coefficient set cannot put the zeros exactly on the unit circle, so at 60 Hz it is a hum *reducer*, not an eliminator |
130130
| `LadderFilter` | audioif (`audioladder`) | 7 | 0 samples | 16.2 % P4 / 26.7 % S3 of a block at patch 4 against the palette-derived 17 % / 28 %; lean patch 6 is 8.6 % / 14.7 %. T5 and T1's stopband slope stay disconfirmed | the Moog transistor ladder - four one-pole stages round one global feedback loop with an odd saturator **inside** it, so the passband sinks as `Resonance` rises. That droop is the circuit |
131131
| `CombFilter` | audioif (`audioecho`, `audiobiquad`) | 6 | 0 samples | 7.4 % P4 / 12.2 % S3 of a block at patch 0 against the palette-derived 8 % / 13 %. Above Feedback 0.5 the parked ring's period is the nearest whole number of samples to F_s/Frequency, not the fractional delay the comb was asked for: +17.4 cents at 1760 Hz / Feedback 0.8 (27 samples at 48 kHz) and at most a half-sample — about 70 cents — near 4 kHz. The first-repeat tap still lands within 0.01 cents. Below Feedback 0.5, and at half-sample tunings, the tail reaches exact zero. T2/T5 miss at fractional tunings | none - the naked textbook feedback comb `y(n) = x(n) + g·y(n−M)`: a delay short enough to be a pitch, fed back, so noise grows resonances on that note's harmonic series |

lib/audioeffects/bandpass.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,17 @@
5252
belong to RBJ's prototype, which this class tracks to nine thousandths of a
5353
decibel wherever it was checked.
5454
55-
**The build's one - the 0 dB peak is not held at a low centre with a narrow
56-
width.** T1 says the gain at f0 is 0.00 dB +- 0.05 at every width. Measured
57-
2026-09-07 over the whole `Frequency` x `Width` grid at four probe levels, it
58-
holds **at every width for f0 >= 100 Hz, and at every centre for Q <= 2**, and
59-
it does not hold in eleven of fifty-six cells below that - all of them f0
60-
<= 63 Hz with Q >= 4, worst **-0.50 dB at f0 20 Hz with Q 32**, which is the
61-
`Frequency` knob's bottom stop against the `Width` knob's top. Two knob turns
62-
from patch 5 `Sub Window`. What it sounds like: a sub-bass resonance up to
63-
half a decibel quieter than the same knob setting an octave higher, and the
64-
error changes sign with the signal level (+0.09 dB at -3 dBFS, -0.48 at -12,
65-
+0.44 at -20), so it is not a trim anyone can dial out.
55+
**The build's one - the 0 dB peak is not held at one low, narrow cell.** T1
56+
says the gain at f0 is 0.00 dB +- 0.05 at every width. Measured 2026-09-17 at
57+
the audioif cebb7ca floor over the whole `Frequency` x `Width` grid at four
58+
probe levels, it holds in fifty-five of fifty-six cells, both knob stops
59+
included, and misses in one: **-0.091 dB at f0 31.5 Hz with Q 32**. What it
60+
sounds like: nothing anyone will hear - a tenth of a decibel on the narrowest
61+
sub-bass band the knobs reach.
62+
63+
It used to be eleven cells, all f0 <= 63 Hz with Q >= 4, worst -0.50 dB at
64+
the `Frequency` knob's bottom stop against the `Width` knob's top, with an
65+
error that changed sign with the signal level. The history of why follows.
6666
6767
The cause is not this class and not the prototype. RBJ's closed form at that
6868
cell is `+0.00000 dB`, and `audiobiquad`'s own five coefficients, read off the
@@ -73,8 +73,9 @@
7373
coefficients cancel to seven parts in a million, so the increment single
7474
precision has to carry is 2e-5 of the numbers being differenced. Filed as
7575
audioif#64 with the fix (a transposed direct form II costs nothing at run
76-
time); this class is parked on it as audiocomponents#39. The measurement, the
77-
map and the four-way decomposition are
76+
time), and the fix landed; the one cell left is what it did not reach
77+
(audiocomponents#66). The measurement, the map and the four-way
78+
decomposition are
7879
`workspace docs/effects-internal/probes/phase2_probes/bandpass_lowcorner.py`.
7980
8081
**The prototype's four**, each measured against RBJ's closed form at the
@@ -130,10 +131,10 @@ class never reads `self._transport()`.
130131

131132

132133
class BandPass(_component.Component):
133-
"""A resonant band-pass: 0 dB at the centre at every width above 100 Hz,
134-
exact zeros at DC and Nyquist, +-6 dB/octave skirts (+-12 with `Slope`
135-
on). Below 100 Hz at Q >= 4 the peak loses up to half a decibel - the
136-
kernel's float32 recursion, audioif#64, module docstring."""
134+
"""A resonant band-pass: 0 dB at the centre at every width, exact zeros
135+
at DC and Nyquist, +-6 dB/octave skirts (+-12 with `Slope` on). The one
136+
exception is a tenth of a decibel at f0 31.5 Hz with Q 32 - module
137+
docstring."""
137138

138139
NAME = 'BandPass'
139140
DISPLAY_NAME = 'Band Pass'

tests/test_cpython_effects_bandpass.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -559,12 +559,14 @@ class TierTwo(unittest.TestCase):
559559

560560
# -- T1: 0 dB peak, at every Q ------------------------------------
561561
#
562-
# DISCONFIRMED below 100 Hz once Q >= 4, and the cause is the kernel's
563-
# float32 recursion, not this class (audioif#64; audiocomponents#39; the
564-
# decomposition and the map are workspace docs/effects-internal/probes/phase2_probes/bandpass_lowcorner.py).
565-
# The four tests below are the bound: where the row holds, where it does
566-
# not, that the fault fires through the class, and that the measurement
567-
# can fail at all.
562+
# DISCONFIRMED at one cell. Until audioif#64 the kernel's float32
563+
# direct-form I recursion missed the bar in 11 of 56 cells below 100 Hz
564+
# at Q >= 4, worst -0.50 dB at the macro stops. Its transposed direct
565+
# form II, at the cebb7ca floor, leaves 1 of 56: -0.091 dB at f0 31.5 Hz,
566+
# Q 32 (workspace docs/effects-internal/probes/phase2_probes/
567+
# bandpass_lowcorner.py, `map`; audiocomponents#66). The tests below are
568+
# the bound: where the row holds, the one cell where it does not, that
569+
# the fault fires through the class, and that the measurement can fail.
568570

569571
#: Where T1 holds, measured rather than assumed: every width the surface
570572
#: reaches at f0 >= 100 Hz, and every centre it reaches at Q <= 2.
@@ -597,14 +599,11 @@ def test_t1_holds_with_the_second_section_switched_in(self):
597599
measured = tone_gain_db(1000.0, 1000.0, q, sections=2)
598600
self.assertLess(abs(measured), 0.05)
599601

600-
def test_t1_is_disconfirmed_at_the_two_macro_stops(self):
601-
"""The disconfirmation, kept as a test so it cannot come back green
602-
without anyone noticing - and so that the day audioif#64 lands, this
603-
is the test that says so.
604-
605-
The sweep is the kit's own driver over the spans T1 quantifies over,
606-
in the class's macro units, and it is run at the stops first because
607-
that is where the worst cell is.
602+
def test_t1_holds_at_the_two_macro_stops(self):
603+
"""This test used to hold the disconfirmation at the stops - worst
604+
-0.50 dB at Frequency 0 against Width 127 - and said it would be the
605+
one to announce audioif#64. It did: at the cebb7ca floor the kit's
606+
sweep over the spans T1 quantifies over passes, stops included.
608607
"""
609608
effect = build(frequency=1000.0, q=0.707)
610609

@@ -621,11 +620,15 @@ def measure_at(settings):
621620
measure_at, bar=0.05, unit="dB", name="T1")
622621
finally:
623622
effect.deinit()
624-
self.assertFalse(swept["passed"], swept["values"])
625-
self.assertTrue(swept["values"]["at_a_stop"])
626-
self.assertEqual(swept["values"]["at_units"],
627-
{"Frequency": 20.0, "Width": 32.0})
628-
self.assertLess(swept["values"]["worst"], -0.4)
623+
self.assertTrue(swept["passed"], swept["values"])
624+
self.assertLess(abs(swept["values"]["worst"]), 0.05)
625+
626+
def test_t1_is_still_disconfirmed_at_31_5_hz_and_q_32(self):
627+
"""The one cell of 56 the transposed kernel leaves outside the bar,
628+
kept as a test so it cannot come back green unnoticed. Settled for
629+
twenty seconds it still reads -0.08 dB, so it is not the probe's
630+
settle rule; the pack's map reads -0.091 at -20 dBFS."""
631+
self.assertLess(tone_gain_db(31.5, 31.5, 32.0), -0.05)
629632

630633
def test_planted_fault_t1_a_numerator_that_is_not_the_constant_zero_db(
631634
self):

tests/test_cpython_effects_graphiceq.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -334,10 +334,14 @@ def test_planted_fault_the_detent_branch_deleted_fires_on_every_band(self):
334334
"band %d: the detent-branch fault did not change the render, "
335335
"so T4 proves nothing at that band" % band)
336336

337-
def test_the_fault_it_replaced_is_green_on_six_of_the_ten_bands(self):
338-
# Kept as the record of why it was replaced, not as evidence. This
339-
# is the auditor's own reproduction: RED at bands 0-2, green from 4
340-
# up, at the pack's own probe level.
337+
def test_the_fault_it_replaced_is_green_on_every_band(self):
338+
# Kept as the record of why it was replaced, not as evidence. The
339+
# auditor's reproduction read RED at bands 0-2 and green from 4 up,
340+
# at the pack's own probe level. Since audiobiquad's transposed
341+
# direct form II (audioif#64, at the cebb7ca floor) a flat section
342+
# forced to mix 1 is bit-transparent at all ten centres, so the old
343+
# plant could not fail anywhere - which is the case for replacing it,
344+
# made complete.
341345
data = noise(24000)
342346
red = []
343347
for band in range(BANDS):
@@ -354,7 +358,7 @@ def test_the_fault_it_replaced_is_green_on_six_of_the_ten_bands(self):
354358
effect.deinit()
355359
if faulted.digest != control.digest:
356360
red.append(band)
357-
self.assertEqual(red, [0, 1, 2],
361+
self.assertEqual(red, [],
358362
"the retired fault's reach moved: red at %r" % red)
359363

360364
def test_the_fault_is_not_one_the_surface_can_dial(self):
@@ -549,8 +553,14 @@ def test_a_patch_change_does_not_overshoot_its_own_steady_level(self):
549553
"patch %d -> %d clips" % (before, after))
550554

551555
def test_planted_fault_the_jump_taken_on_the_old_curves_state(self):
552-
steady, peak = self._transition(5, 1, clear=False)
553-
self.assertGreater(peak, steady * 4,
556+
# Held to the check's own 2x bar, on a pair that check reads. On the
557+
# direct-form I sections this was 5 -> 1 at more than 4x; since
558+
# audiobiquad moved to transposed direct form II (audioif#64) a jump
559+
# on stale state rings far less, and 2 -> 1 is the pair that still
560+
# crosses the bar: 12032 against 4424 (2.72x) with the fault planted,
561+
# 1.32x through the class, measured at the cebb7ca floor.
562+
steady, peak = self._transition(2, 1, clear=False)
563+
self.assertGreater(peak, steady * 2,
554564
"the base program_change no longer slams, so the "
555565
"check above proves nothing (peak %d, steady %d)"
556566
% (peak, steady))

0 commit comments

Comments
 (0)