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.
This commit is contained in:
2026-09-13 23:36:27 +01:00
parent 6135ca193e
commit 14a05e8694

View File

@@ -107,6 +107,9 @@ class Playback(Event):
self._current_frame = 0 self._current_frame = 0
self._fadecurve = None self._fadecurve = None
self._fadeframe = None self._fadeframe = None
self._nextdata = None
self._nextfs = None
self._delay = False
self._stream = sounddevice.OutputStream( self._stream = sounddevice.OutputStream(
samplerate=self._fs, device=self._device, channels=self._channels, samplerate=self._fs, device=self._device, channels=self._channels,
@@ -116,17 +119,44 @@ class Playback(Event):
def callback(self, outdata, frames, time, status): def callback(self, outdata, frames, time, status):
if status: if status:
print(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) chunksize = min(len(self._data) - self._current_frame, frames)
if self._fadecurve is not None: if self._fadecurve is not None:
chunksize = min(chunksize, len(self._fadecurve) - self._fadeframe) 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: 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 self._fadeframe += chunksize
if chunksize < frames:
outdata[chunksize:, self._mapping] = 0
raise sounddevice.CallbackStop()
self._current_frame += chunksize 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): def finished_callback(self):
self._data = None self._data = None
@@ -141,10 +171,21 @@ class Playback(Event):
self._stream.close(True) self._stream.close(True)
self._stream = None 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': if message == 'fadeout':
self._fadecurve = np.logspace(0, -db/10, fadetime * self._fs) self._fadecurve = np.logspace(0, -db/10, fadetime * self._fs)
self._fadeframe = 0 self._fadeframe = 0
elif message == 'replaceplayback':
self.replace(media, delay)
elif message == 'endloop':
self._loop = False
class FadeOut(Event): class FadeOut(Event):
def __init__(self, duration=10, db=30, *args, **kwargs): 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.notify('fadeout', fadetime=self._duration, db=self._db)
self._eventrunner.finished(self) 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): class SetTempo(Event):
def __init__(self, tempo=None, beats=None, *args, **kwargs): def __init__(self, tempo=None, beats=None, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)