From 14a05e8694c8d977caad73dbf48e90f0af35b32d Mon Sep 17 00:00:00 2001 From: Gavan Fantom Date: Sun, 13 Sep 2026 23:36:27 +0100 Subject: [PATCH] Implement looping, PlaybackNext, and EndLoop PlaybackNext replaces the current playback object with new media. Optionally, it waits until the end of the media. In this case, it only waits until the end of the current loop. EndLoop stops the playback at the end of the current loop. Note that PlaybackNext and EndLoop have no effect if there is no media currently playing. --- events.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/events.py b/events.py index 84bf2e2..7488faf 100644 --- a/events.py +++ b/events.py @@ -107,6 +107,9 @@ class Playback(Event): self._current_frame = 0 self._fadecurve = None self._fadeframe = None + self._nextdata = None + self._nextfs = None + self._delay = False self._stream = sounddevice.OutputStream( samplerate=self._fs, device=self._device, channels=self._channels, @@ -116,17 +119,44 @@ class Playback(Event): def callback(self, outdata, frames, time, status): if status: print(status) + if self._nextdata is not None and not self._delay: + self.callback_startnew() + offset = 0 + while frames > 0: + chunksize = self.callback_fillchunk(outdata, offset, frames) + offset += chunksize + frames -= chunksize + if frames > 0: + if self._nextdata is not None: + self.callback_startnew() + elif self._loop: + self._current_frame = 0 + else: + outdata[offset:, self._mapping] = 0 + raise sounddevice.CallbackStop() + self._current_frame += frames + frames = 0 + + def callback_fillchunk(self, outdata, offset, frames): chunksize = min(len(self._data) - self._current_frame, frames) if self._fadecurve is not None: chunksize = min(chunksize, len(self._fadecurve) - self._fadeframe) - outdata[:chunksize, self._mapping] = self._data[self._current_frame:self._current_frame + chunksize] * self._gain + outdata[offset:offset + chunksize, self._mapping] = self._data[self._current_frame:self._current_frame + chunksize] * self._gain if self._fadecurve is not None: - outdata[:chunksize, self._mapping] *= self._fadecurve[self._fadeframe:self._fadeframe + chunksize, np.newaxis] + outdata[offset:offset + chunksize, self._mapping] *= self._fadecurve[self._fadeframe:self._fadeframe + chunksize, np.newaxis] self._fadeframe += chunksize - if chunksize < frames: - outdata[chunksize:, self._mapping] = 0 - raise sounddevice.CallbackStop() self._current_frame += chunksize + return chunksize + + def callback_startnew(self): + self._data = self._nextdata + self._fs = self._nextfs + self._loop = self._nextloop + self._current_frame = 0 + self._nextdata = None + self._nextfs = None + self._fadecurve = None + self._fadeframe = None def finished_callback(self): self._data = None @@ -141,10 +171,21 @@ class Playback(Event): self._stream.close(True) self._stream = None - def notify(self, message, fadetime=10, db=30, *args, **kwargs): + def replace(self, media, delay=False, loop=False): + self._nextdata, self._nextfs = soundfile.read(media) + self._delay = delay + self._nextloop = loop + # We really should check that the media is the sampe shape as the + # existing playback. But for now, let's trust the user. *cough* + + def notify(self, message, fadetime=10, db=30, media=None, delay=False, *args, **kwargs): if message == 'fadeout': self._fadecurve = np.logspace(0, -db/10, fadetime * self._fs) self._fadeframe = 0 + elif message == 'replaceplayback': + self.replace(media, delay) + elif message == 'endloop': + self._loop = False class FadeOut(Event): def __init__(self, duration=10, db=30, *args, **kwargs): @@ -160,6 +201,34 @@ class FadeOut(Event): self._eventrunner.notify('fadeout', fadetime=self._duration, db=self._db) self._eventrunner.finished(self) +class EndLoop(Event): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __repr__(self): + return '{}(at={})'.format(type(self).__name__, self._at) + + def start(self): + print("Ending loop") + self._eventrunner.notify('endloop') + self._eventrunner.finished(self) + + +class PlaybackNext(Event): + def __init__(self, media, delay=False, loop=False, *args, **kwargs): + super().__init__(*args, **kwargs) + self._media = media + self._delay = delay + self._loop = loop + + def __repr__(self): + return '{}(at={}, media={}, delay={}, loop={})'.format(type(self).__name__, self._at, self._media, self._delay, self._loop) + + def start(self): + print("Replacing playback") + self._eventrunner.notify('replaceplayback', media=self._media, delay=self._delay, loop=self._loop) + self._eventrunner.finished(self) + class SetTempo(Event): def __init__(self, tempo=None, beats=None, *args, **kwargs): super().__init__(*args, **kwargs)