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
11 changes: 8 additions & 3 deletions lib/audioinstruments/_rack/dsp/drums.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions tests/mpy/test_rhythm909.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading