From 7125d164b87bc22973f3f578085d8933b709aec4 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 20 Oct 2026 10:57:08 -0400 Subject: [PATCH] Fix minor bugs with queue and teardown --- .gitignore | 1 + portaudio | 2 +- src/rtmixer.c | 5 ++++- src/rtmixer.py | 16 +++++++++++++++- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 3c1a62c..679bf0f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ __pycache__ .eggs /dist/ /wheelhouse/ +.DS_Store \ No newline at end of file diff --git a/portaudio b/portaudio index 147dd72..7c45fed 160000 --- a/portaudio +++ b/portaudio @@ -1 +1 @@ -Subproject commit 147dd722548358763a8b649b3e4b41dfffbcfbb6 +Subproject commit 7c45fed9322bd86b5dd0da4547b15d9ac3b9b0ac diff --git a/src/rtmixer.c b/src/rtmixer.c index 9ac3133..6506220 100644 --- a/src/rtmixer.c +++ b/src/rtmixer.c @@ -128,7 +128,10 @@ int callback(const void* input, void* output, frame_t frameCount struct state* state = userData; CALLBACK_ASSERT(state); - memset(output, 0, sizeof(float) * state->output_channels * frameCount); + if (output) // NULL for input-only streams + { + memset(output, 0, sizeof(float) * state->output_channels * frameCount); + } get_stats(frameCount, statusFlags, &(state->stats)); diff --git a/src/rtmixer.py b/src/rtmixer.py index 0cdde95..16cb190 100644 --- a/src/rtmixer.py +++ b/src/rtmixer.py @@ -41,6 +41,15 @@ def __init__(self, kind, qsize=16, **kwargs): self._actions = {} self._temp_action_ptr = _ffi.new('struct action**') + def __del__(self): + # The C callback uses memory owned by this object, so the stream + # must be closed before that memory is freed. + try: + if not self.closed: + self.close() + except Exception: + pass # e.g. __init__() failed or interpreter shutdown + @property def actions(self): """The set of active "actions".""" @@ -76,7 +85,8 @@ def cancel(self, action, time=0, allow_belated=True): requested_time=time, action=action, )) - self._enqueue(cancel_action) + # The callback dereferences *action*, so it must be kept alive: + self._enqueue(cancel_action, keep_alive=action) return cancel_action def fetch_and_reset_stats(self, time=0, allow_belated=True): @@ -126,6 +136,10 @@ def _check_channels(self, channels, kind): def _enqueue(self, action, keep_alive=None): self._drain_result_q() + # Each action ends up in the result queue exactly once, so limiting + # the number of pending actions guarantees that it never overflows: + if len(self._actions) >= len(self._result_q): + raise RuntimeError('Too many pending actions') self._temp_action_ptr[0] = action ret = self._action_q.write(self._temp_action_ptr) if ret != 1: