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
16 changes: 11 additions & 5 deletions lib/audioinstruments/_rack/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,9 @@ def _pull(self):
def fill(self):
"""Top the ring up with rendered audio; returns the buffers written.

It fills the room the ring has when you call it and returns, even when
the patch renders slower than real time and the ring has drained again
by then; the audio then underruns rather than the call never ending.
Call it from a timer on a build whose output is a ring (MicroPython).
On CPython the pull renders and this does nothing. With
``output="graph"`` it is one turn of the graph's Python side (see the
Expand All @@ -1022,14 +1025,17 @@ def fill(self):
if ring is None:
return 0
raw, views = self._slots[0]
written = 0
need = len(raw)
while ring.space() >= need:
# Fill the room there is now, once. A patch that renders slower than
# real time never catches a ring that drains while it renders, so
# looping until the ring is full would never return.
blocks = ring.space() // len(raw)
left = blocks
while left > 0:
with self._lock:
self._render(raw, views)
ring.write(raw)
written += 1
return written
left -= 1
return blocks


def graph_available():
Expand Down
42 changes: 42 additions & 0 deletions tests/test_fill_bounded.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# SPDX-FileCopyrightText: 2026 Brad Barnett
# SPDX-License-Identifier: MIT
"""fill() returns when the patch renders slower than the ring drains."""

import audioinstruments


class _DrainingRing:
"""A ring whose reader keeps up with every write, so it never fills."""

def __init__(self, room_blocks, block_bytes):
self.room = room_blocks * block_bytes
self.writes = 0

def space(self):
if self.writes > 1000:
raise AssertionError("fill() is still writing after 1000 blocks")
return self.room

def write(self, buf):
self.writes += 1


def test_fill_fills_the_room_once_and_returns():
inst = audioinstruments.create("electric_piano", 48000, output="pull")
inst.note_on(60, 100)
ring = _DrainingRing(5, len(inst._slots[0][0]))
inst._ring = ring
assert inst.fill() == 5
assert ring.writes == 5
# The room is still there: the next call fills it again, it does not
# carry on from the last one.
assert inst.fill() == 5
assert ring.writes == 10


def test_fill_with_no_room_renders_nothing():
inst = audioinstruments.create("electric_piano", 48000, output="pull")
ring = _DrainingRing(0, len(inst._slots[0][0]))
inst._ring = ring
assert inst.fill() == 0
assert ring.writes == 0
Loading