From 99bf5823d5066ea5aec6e64b3f5cb776dc239c94 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Sat, 31 Oct 2026 09:58:03 -0500 Subject: [PATCH] rhythm909: clamp the crusher's hold to the output rate so it builds below 32 kHz --- lib/audioinstruments/_rack/dsp/drums.py | 11 ++++++++--- tests/mpy/test_rhythm909.py | 13 +++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/audioinstruments/_rack/dsp/drums.py b/lib/audioinstruments/_rack/dsp/drums.py index 6f176b6..12be440 100644 --- a/lib/audioinstruments/_rack/dsp/drums.py +++ b/lib/audioinstruments/_rack/dsp/drums.py @@ -1162,10 +1162,15 @@ def _build_hold(self): elif audioshaper is not None: self.bridge = NodeBridge(block_size, self.sample_rate) self.sample_hold = audioshaper.SampleHold( - self.bridge.source, self.sample_rate, int(round(self.rate)) + self.bridge.source, self.sample_rate, self._hold_rate() ) self.bridge.chain(self.sample_hold, direct=False) + def _hold_rate(self): + # The hold only drops frames, so it cannot run faster than the output: + # callers that ask for 32 kHz on a 24 kHz output get a hold of 24 kHz. + return min(int(round(self.rate)), self.sample_rate) + def _hold_clear(self): # Armed so that the first frames refresh, the way the node's clear() leaves it. self._acc = self.sample_rate - 1 @@ -1177,7 +1182,7 @@ def _hold(self, src, dst): Every array is made once in _build_hold, so a block allocates nothing. """ num = self.sample_rate - den = int(round(self.rate)) + den = self._hold_rate() n = len(src) count = self._count[:n] np.multiply(self._ahead[:n], den, out=count) @@ -1212,7 +1217,7 @@ def set_rate(self, rate_hz): self.rate = r self._update_anti_alias() if self.sample_hold is not None: - self.sample_hold.set(self.sample_rate, int(round(self.rate))) + self.sample_hold.set(self.sample_rate, self._hold_rate()) def _update_anti_alias(self): cutoff = min(self.rate * 0.45, self.sample_rate * 0.45) diff --git a/tests/mpy/test_rhythm909.py b/tests/mpy/test_rhythm909.py index 91fa8d7..911c296 100644 --- a/tests/mpy/test_rhythm909.py +++ b/tests/mpy/test_rhythm909.py @@ -164,3 +164,16 @@ def test_crusher_follows_its_input_and_settles_to_zero(): for _ in range(6): crusher.process(src, dst) assert max(abs(dst[i]) for i in range(128)) == 0.0 + + +def test_builds_and_sounds_below_32_khz(): + """The crusher's 32 kHz hold cannot exceed the output rate: 22.05 and 24 kHz must build.""" + for rate in (22050, 24000): + inst = adapter.create(rate, voice="rhythm909", engine="rack") + for pad in VOICE_PADS: + inst.note_on(pad, 100) + top = peak(pulled(inst, 15)) + assert top > 300, "Pad %d at %d Hz is too quiet: %d" % (pad, rate, top) + inst.all_notes_off() + pulled(inst, 20) + inst.deinit()