Support for multiple sounds on the same stream

This commit is contained in:
Fabio Manganiello 2018-12-26 16:10:14 +01:00
parent c2544524e1
commit 3554968468
3 changed files with 81 additions and 43 deletions

View file

@ -78,9 +78,13 @@ class RedisBackend(Backend):
format(self.queue, self.redis_args)) format(self.queue, self.redis_args))
while not self.should_stop(): while not self.should_stop():
msg = self.get_message() try:
self.logger.info('Received message on the Redis backend: {}'.format(msg)) msg = self.get_message()
self.on_message(msg) self.logger.info('Received message on the Redis backend: {}'.
format(msg))
self.on_message(msg)
except Exception as e:
self.logger.exception(e)
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -13,8 +13,9 @@ class Sound(object):
STANDARD_A_FREQUENCY = 440.0 STANDARD_A_FREQUENCY = 440.0
STANDARD_A_MIDI_NOTE = 69 STANDARD_A_MIDI_NOTE = 69
_DEFAULT_BLOCKSIZE = 2048 _DEFAULT_BLOCKSIZE = 1024
_DEFAULT_BUFSIZE = 20 _DEFAULT_SYNTH_BUFSIZE = 2
_DEFAULT_FILE_BUFSIZE = 20
_DEFAULT_SAMPLERATE = 44100 _DEFAULT_SAMPLERATE = 44100
midi_note = None midi_note = None
@ -159,9 +160,11 @@ class Mix(object):
through an audio stream to an audio device through an audio stream to an audio device
""" """
_sounds = [] _sounds = None
def __init__(self, *sounds): def __init__(self, *sounds):
self._sounds = []
for sound in sounds: for sound in sounds:
self.add(sound) self.add(sound)
@ -235,4 +238,21 @@ class Mix(object):
return wave return wave
def duration(self):
"""
:returns: The duration of the mix in seconds as duration of its longest
sample, or None if the mixed sample have no duration set
"""
duration = 0
for sound in self._sounds:
if sound.duration is None:
return None
duration = max(duration, sound.duration)
return duration
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -12,7 +12,7 @@ import time
from enum import Enum from enum import Enum
from threading import Thread, Event, RLock from threading import Thread, Event, RLock
from .core import Sound from .core import Sound, Mix
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
@ -41,8 +41,7 @@ class SoundPlugin(Plugin):
def __init__(self, input_device=None, output_device=None, def __init__(self, input_device=None, output_device=None,
input_blocksize=Sound._DEFAULT_BLOCKSIZE, input_blocksize=Sound._DEFAULT_BLOCKSIZE,
output_blocksize=Sound._DEFAULT_BLOCKSIZE, output_blocksize=Sound._DEFAULT_BLOCKSIZE, *args, **kwargs):
playback_bufsize=Sound._DEFAULT_BUFSIZE, *args, **kwargs):
""" """
:param input_device: Index or name of the default input device. Use :method:`platypush.plugins.sound.query_devices` to get the available devices. Default: system default :param input_device: Index or name of the default input device. Use :method:`platypush.plugins.sound.query_devices` to get the available devices. Default: system default
:type input_device: int or str :type input_device: int or str
@ -55,9 +54,6 @@ class SoundPlugin(Plugin):
:param output_blocksize: Blocksize to be applied to the output device. Try to increase this value if you get output underflow errors while playing. Default: 2048 :param output_blocksize: Blocksize to be applied to the output device. Try to increase this value if you get output underflow errors while playing. Default: 2048
:type output_blocksize: int :type output_blocksize: int
:param playback_bufsize: Number of audio blocks that will be cached while playing (default: 20)
:type playback_bufsize: int
""" """
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -66,11 +62,11 @@ class SoundPlugin(Plugin):
self.output_device = output_device self.output_device = output_device
self.input_blocksize = input_blocksize self.input_blocksize = input_blocksize
self.output_blocksize = output_blocksize self.output_blocksize = output_blocksize
self.playback_bufsize = playback_bufsize
self.playback_state = {} self.playback_state = {}
self.playback_state_lock = RLock() self.playback_state_lock = RLock()
self.playback_paused_changed = {} self.playback_paused_changed = {}
self.stream_mixes = {}
self.recording_state = RecordingState.STOPPED self.recording_state = RecordingState.STOPPED
self.recording_state_lock = RLock() self.recording_state_lock = RLock()
self.recording_paused_changed = Event() self.recording_paused_changed = Event()
@ -173,7 +169,7 @@ class SoundPlugin(Plugin):
@action @action
def play(self, file=None, sound=None, device=None, blocksize=None, def play(self, file=None, sound=None, device=None, blocksize=None,
bufsize=Sound._DEFAULT_BUFSIZE, samplerate=None, channels=None, bufsize=None, samplerate=None, channels=None,
stream_index=None): stream_index=None):
""" """
Plays a sound file (support formats: wav, raw) or a synthetic sound. Plays a sound file (support formats: wav, raw) or a synthetic sound.
@ -201,7 +197,8 @@ class SoundPlugin(Plugin):
`output_blocksize` or 2048) `output_blocksize` or 2048)
:type blocksize: int :type blocksize: int
:param bufsize: Size of the audio buffer (default: 20) :param bufsize: Size of the audio buffer (default: 20 frames for audio
files, 2 frames for synth sounds)
:type bufsize: int :type bufsize: int
:param samplerate: Audio samplerate. Default: audio file samplerate if :param samplerate: Audio samplerate. Default: audio file samplerate if
@ -227,9 +224,17 @@ class SoundPlugin(Plugin):
if blocksize is None: if blocksize is None:
blocksize = self.output_blocksize blocksize = self.output_blocksize
if bufsize is None:
if file:
bufsize = Sound._DEFAULT_FILE_BUFSIZE
else:
bufsize = Sound._DEFAULT_SYNTH_BUFSIZE
q = queue.Queue(maxsize=bufsize) q = queue.Queue(maxsize=bufsize)
f = None f = None
t = 0. t = 0.
is_new_stream = stream_index is None or \
self.active_streams.get(stream_index) is None
if file: if file:
file = os.path.abspath(os.path.expanduser(file)) file = os.path.abspath(os.path.expanduser(file))
@ -239,21 +244,28 @@ class SoundPlugin(Plugin):
if device is None: if device is None:
device = self._get_default_device('output') device = self._get_default_device('output')
if file:
import soundfile as sf
f = sf.SoundFile(file)
if not samplerate:
samplerate = f.samplerate if f else Sound._DEFAULT_SAMPLERATE
if not channels:
channels = f.channels if f else 1
self.logger.info('Starting playback of {} to sound device [{}]'.
format(file or sound, device))
if is_new_stream:
stream_index = self._allocate_stream_index()
if sound:
mix = self.stream_mixes[stream_index]
mix.add(sound)
if not is_new_stream:
return # Let the existing callback handle the new mix
try: try:
if file:
import soundfile as sf
f = sf.SoundFile(file)
if not samplerate:
samplerate = f.samplerate if f else Sound._DEFAULT_SAMPLERATE
if not channels:
channels = f.channels if f else 1
self.logger.info('Starting playback of {} to sound device [{}]'.
format(file or sound, device))
if sound:
sound = Sound.build(sound)
# Audio queue pre-fill loop # Audio queue pre-fill loop
for _ in range(bufsize): for _ in range(bufsize):
if f: if f:
@ -261,24 +273,26 @@ class SoundPlugin(Plugin):
if not data: if not data:
break break
else: else:
duration = mix.duration()
blocktime = float(blocksize / samplerate) blocktime = float(blocksize / samplerate)
next_t = min(t+blocktime, sound.duration) \ next_t = min(t+blocktime, duration) \
if sound.duration is not None else t+blocktime if duration is not None else t+blocktime
data = sound.get_wave(t_start=t, t_end=next_t, data = mix.get_wave(t_start=t, t_end=next_t,
samplerate=samplerate) samplerate=samplerate)
t = next_t t = next_t
if sound.duration is not None and t >= sound.duration: if duration is not None and t >= duration:
break break
q.put_nowait(data) # Pre-fill the audio queue q.put_nowait(data) # Pre-fill the audio queue
if stream_index is None: stream = self.active_streams[stream_index]
completed_callback_event = self.completed_callback_events[stream_index]
if stream is None:
streamtype = sd.RawOutputStream if file else sd.OutputStream streamtype = sd.RawOutputStream if file else sd.OutputStream
stream_index = self._allocate_stream_index()
completed_callback_event = self.completed_callback_events[stream_index]
stream = streamtype(samplerate=samplerate, blocksize=blocksize, stream = streamtype(samplerate=samplerate, blocksize=blocksize,
device=device, channels=channels, device=device, channels=channels,
dtype='float32', dtype='float32',
@ -289,9 +303,6 @@ class SoundPlugin(Plugin):
finished_callback=completed_callback_event.set) finished_callback=completed_callback_event.set)
self._start_playback(stream_index=stream_index, stream=stream) self._start_playback(stream_index=stream_index, stream=stream)
else:
stream = self.active_streams[stream_index]
completed_callback_event = self.completed_callback_events[stream_index]
with stream: with stream:
# Timeout set until we expect all the buffered blocks to # Timeout set until we expect all the buffered blocks to
@ -308,15 +319,16 @@ class SoundPlugin(Plugin):
if not data: if not data:
break break
else: else:
duration = mix.duration()
blocktime = float(blocksize / samplerate) blocktime = float(blocksize / samplerate)
next_t = min(t+blocktime, sound.duration) \ next_t = min(t+blocktime, duration) \
if sound.duration is not None else t+blocktime if duration is not None else t+blocktime
data = sound.get_wave(t_start=t, t_end=next_t, data = mix.get_wave(t_start=t, t_end=next_t,
samplerate=samplerate) samplerate=samplerate)
t = next_t t = next_t
if sound.duration is not None and t >= sound.duration: if duration is not None and t >= duration:
break break
if self._get_playback_state(stream_index) == \ if self._get_playback_state(stream_index) == \
@ -586,6 +598,7 @@ class SoundPlugin(Plugin):
raise RuntimeError('No stream index available') raise RuntimeError('No stream index available')
self.active_streams[stream_index] = None self.active_streams[stream_index] = None
self.stream_mixes[stream_index] = Mix()
self.completed_callback_events[stream_index] = \ self.completed_callback_events[stream_index] = \
completed_callback_event if completed_callback_event else Event() completed_callback_event if completed_callback_event else Event()
@ -632,6 +645,7 @@ class SoundPlugin(Plugin):
event.wait() event.wait()
del self.completed_callback_events[i] del self.completed_callback_events[i]
del self.active_streams[i] del self.active_streams[i]
del self.stream_mixes[i]
self.logger.info('Playback stopped on streams [{}]'.format( self.logger.info('Playback stopped on streams [{}]'.format(
', '.join([str(stream) for stream in ', '.join([str(stream) for stream in