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..705127b 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: + self.abort() + 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,11 @@ 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, consider increasing qsize') self._temp_action_ptr[0] = action ret = self._action_q.write(self._temp_action_ptr) if ret != 1: @@ -149,6 +164,13 @@ class Mixer(_Base): except *callback* (a callback function implemented in C is used internally) and *dtype* (which is always ``'float32'``). + The additional keyword argument *qsize* (default: 16) sets the size + of the queues used to communicate with the callback, which is also + the maximum number of pending actions, i.e. actions that have been + started (or scheduled) but haven't finished yet. If an action is + started while *qsize* actions are pending, a `RuntimeError` is + raised. It must be a power of 2. + Uses default values from `sounddevice.default` (except *dtype*, which is always ``'float32'``). @@ -217,6 +239,8 @@ class Recorder(_Base): except *callback* (a callback function implemented in C is used internally) and *dtype* (which is always ``'float32'``). + The additional keyword argument *qsize* is described in `Mixer`. + Uses default values from `sounddevice.default` (except *dtype*, which is always ``'float32'``). @@ -282,6 +306,8 @@ class MixerAndRecorder(Mixer, Recorder): *callback* (a callback function implemented in C is used internally) and *dtype* (which is always ``'float32'``). + The additional keyword argument *qsize* is described in `Mixer`. + Uses default values from `sounddevice.default` (except *dtype*, which is always ``'float32'``).