Support for stream names
This commit is contained in:
parent
fae45d8ca3
commit
f9f43964a2
2 changed files with 111 additions and 25 deletions
|
@ -39,20 +39,30 @@ class SoundPlugin(Plugin):
|
||||||
* **numpy** (``pip install numpy``)
|
* **numpy** (``pip install numpy``)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_STREAM_NAME_PREFIX = 'platypush-stream-'
|
||||||
|
|
||||||
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, *args, **kwargs):
|
output_blocksize=Sound._DEFAULT_BLOCKSIZE, *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
|
||||||
|
|
||||||
:param output_device: Index or name of the default output device. Use :method:`platypush.plugins.sound.query_devices` to get the available devices. Default: system default
|
:param output_device: Index or name of the default output device.
|
||||||
|
Use :method:`platypush.plugins.sound.query_devices` to get the
|
||||||
|
available devices. Default: system default
|
||||||
:type output_device: int or str
|
:type output_device: int or str
|
||||||
|
|
||||||
:param input_blocksize: Blocksize to be applied to the input device. Try to increase this value if you get input overflow errors while recording. Default: 2048
|
:param input_blocksize: Blocksize to be applied to the input device.
|
||||||
|
Try to increase this value if you get input overflow errors while
|
||||||
|
recording. Default: 1024
|
||||||
:type input_blocksize: int
|
:type input_blocksize: int
|
||||||
|
|
||||||
: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: 1024
|
||||||
:type output_blocksize: int
|
:type output_blocksize: int
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -71,6 +81,8 @@ class SoundPlugin(Plugin):
|
||||||
self.recording_state_lock = RLock()
|
self.recording_state_lock = RLock()
|
||||||
self.recording_paused_changed = Event()
|
self.recording_paused_changed = Event()
|
||||||
self.active_streams = {}
|
self.active_streams = {}
|
||||||
|
self.stream_name_to_index = {}
|
||||||
|
self.stream_index_to_name = {}
|
||||||
self.completed_callback_events = {}
|
self.completed_callback_events = {}
|
||||||
|
|
||||||
def _get_default_device(self, category):
|
def _get_default_device(self, category):
|
||||||
|
@ -82,7 +94,8 @@ class SoundPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sounddevice as sd
|
import sounddevice as sd
|
||||||
return sd.query_hostapis()[0].get('default_' + category.lower() + '_device')
|
return sd.query_hostapis()[0].get('default_' + category.lower() +
|
||||||
|
'_device')
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def query_devices(self, category=None):
|
def query_devices(self, category=None):
|
||||||
|
@ -143,19 +156,24 @@ class SoundPlugin(Plugin):
|
||||||
while self._get_playback_state(stream_index) == PlaybackState.PAUSED:
|
while self._get_playback_state(stream_index) == PlaybackState.PAUSED:
|
||||||
self.playback_paused_changed[stream_index].wait()
|
self.playback_paused_changed[stream_index].wait()
|
||||||
|
|
||||||
assert frames == blocksize
|
if frames != blocksize:
|
||||||
|
self.logger.warning('Received {} frames, expected blocksize is {}'.
|
||||||
|
format(frames, blocksize))
|
||||||
|
return
|
||||||
|
|
||||||
if status.output_underflow:
|
if status.output_underflow:
|
||||||
self.logger.warning('Output underflow: increase blocksize?')
|
self.logger.warning('Output underflow: increase blocksize?')
|
||||||
outdata = (b'\x00' if is_raw_stream else 0.) * len(outdata)
|
outdata = (b'\x00' if is_raw_stream else 0.) * len(outdata)
|
||||||
return
|
return
|
||||||
|
|
||||||
assert not status
|
if status:
|
||||||
|
self.logger.warning('Audio callback failed: {}'.format(status))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = q.get_nowait()
|
data = q.get_nowait()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
self.logger.warning('Buffer is empty: increase buffersize?')
|
self.logger.warning('Buffer is empty: increase buffersize?')
|
||||||
raise sd.CallbackAbort
|
raise sd.CallbackStop
|
||||||
|
|
||||||
if len(data) < len(outdata):
|
if len(data) < len(outdata):
|
||||||
outdata[:len(data)] = data
|
outdata[:len(data)] = data
|
||||||
|
@ -169,7 +187,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=None, samplerate=None, channels=None,
|
bufsize=None, samplerate=None, channels=None, stream_name=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.
|
||||||
|
@ -213,6 +231,14 @@ class SoundPlugin(Plugin):
|
||||||
index (you can get them through
|
index (you can get them through
|
||||||
:method:`platypush.plugins.sound.query_streams`). Default:
|
:method:`platypush.plugins.sound.query_streams`). Default:
|
||||||
creates a new audio stream through PortAudio.
|
creates a new audio stream through PortAudio.
|
||||||
|
:type stream_index: int
|
||||||
|
|
||||||
|
:param stream_name: Name of the stream to play to. If set, the sound
|
||||||
|
will be played to the specified stream name, or a stream with that
|
||||||
|
name will be created. If not set, and ``stream_index`` is not set
|
||||||
|
either, then a new stream will be created on the next available
|
||||||
|
index and named ``platypush-stream-<index>``.
|
||||||
|
:type stream_name: str
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not file and not sound:
|
if not file and not sound:
|
||||||
|
@ -233,8 +259,6 @@ class SoundPlugin(Plugin):
|
||||||
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))
|
||||||
|
@ -252,19 +276,22 @@ class SoundPlugin(Plugin):
|
||||||
if not channels:
|
if not channels:
|
||||||
channels = f.channels if f else 1
|
channels = f.channels if f else 1
|
||||||
|
|
||||||
if is_new_stream:
|
with self.playback_state_lock:
|
||||||
stream_index = self._allocate_stream_index()
|
stream_index, is_new_stream = self._get_or_allocate_stream_index(
|
||||||
|
stream_index=stream_index, stream_name=stream_name)
|
||||||
self.logger.info(('Starting playback of {} to sound device [{}] ' +
|
|
||||||
'on stream [{}]').format(
|
|
||||||
file or sound, device, stream_index))
|
|
||||||
|
|
||||||
if sound:
|
if sound:
|
||||||
mix = self.stream_mixes[stream_index]
|
mix = self.stream_mixes[stream_index]
|
||||||
mix.add(sound)
|
mix.add(sound)
|
||||||
|
|
||||||
|
self.logger.info(('Starting playback of {} to sound device [{}] ' +
|
||||||
|
'on stream [{}]').format(
|
||||||
|
file or sound, device, stream_index))
|
||||||
|
|
||||||
if not is_new_stream:
|
if not is_new_stream:
|
||||||
return # Let the existing callback handle the new mix
|
return # Let the existing callback handle the new mix
|
||||||
|
# TODO Potential support also for mixed streams with
|
||||||
|
# multiple sound files and synth sounds?
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Audio queue pre-fill loop
|
# Audio queue pre-fill loop
|
||||||
|
@ -582,6 +609,7 @@ class SoundPlugin(Plugin):
|
||||||
|
|
||||||
for i, stream in streams.items():
|
for i, stream in streams.items():
|
||||||
stream['playback_state'] = self.playback_state[i].name
|
stream['playback_state'] = self.playback_state[i].name
|
||||||
|
stream['name'] = self.stream_index_to_name.get(i)
|
||||||
if i in self.stream_mixes:
|
if i in self.stream_mixes:
|
||||||
stream['mix'] = { j: sound for j, sound in
|
stream['mix'] = { j: sound for j, sound in
|
||||||
enumerate(list(self.stream_mixes[i])) }
|
enumerate(list(self.stream_mixes[i])) }
|
||||||
|
@ -589,7 +617,32 @@ class SoundPlugin(Plugin):
|
||||||
return streams
|
return streams
|
||||||
|
|
||||||
|
|
||||||
def _allocate_stream_index(self, completed_callback_event=None):
|
def _get_or_allocate_stream_index(self, stream_index=None, stream_name=None,
|
||||||
|
completed_callback_event=None):
|
||||||
|
stream = None
|
||||||
|
|
||||||
|
with self.playback_state_lock:
|
||||||
|
if stream_index is None:
|
||||||
|
if stream_name is not None:
|
||||||
|
stream_index = self.stream_name_to_index.get(stream_name)
|
||||||
|
else:
|
||||||
|
if stream_name is not None:
|
||||||
|
raise RuntimeError('Redundant specification of both ' +
|
||||||
|
'stream_name and stream_index')
|
||||||
|
|
||||||
|
if stream_index is not None:
|
||||||
|
stream = self.active_streams.get(stream_index)
|
||||||
|
|
||||||
|
if not stream:
|
||||||
|
return (self._allocate_stream_index(stream_name=stream_name,
|
||||||
|
completed_callback_event=
|
||||||
|
completed_callback_event),
|
||||||
|
True)
|
||||||
|
|
||||||
|
return (stream_index, False)
|
||||||
|
|
||||||
|
def _allocate_stream_index(self, stream_name=None,
|
||||||
|
completed_callback_event=None):
|
||||||
stream_index = None
|
stream_index = None
|
||||||
|
|
||||||
with self.playback_state_lock:
|
with self.playback_state_lock:
|
||||||
|
@ -601,8 +654,13 @@ class SoundPlugin(Plugin):
|
||||||
if stream_index is None:
|
if stream_index is None:
|
||||||
raise RuntimeError('No stream index available')
|
raise RuntimeError('No stream index available')
|
||||||
|
|
||||||
|
if stream_name is None:
|
||||||
|
stream_name = self._STREAM_NAME_PREFIX + str(stream_index)
|
||||||
|
|
||||||
self.active_streams[stream_index] = None
|
self.active_streams[stream_index] = None
|
||||||
self.stream_mixes[stream_index] = Mix()
|
self.stream_mixes[stream_index] = Mix()
|
||||||
|
self.stream_index_to_name[stream_index] = stream_name
|
||||||
|
self.stream_name_to_index[stream_name] = stream_index
|
||||||
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()
|
||||||
|
|
||||||
|
@ -626,8 +684,8 @@ class SoundPlugin(Plugin):
|
||||||
@action
|
@action
|
||||||
def stop_playback(self, streams=None):
|
def stop_playback(self, streams=None):
|
||||||
"""
|
"""
|
||||||
:param streams: Streams to stop by index (default: all)
|
:param streams: Streams to stop by index or name (default: all)
|
||||||
:type streams: list[int]
|
:type streams: list[int] or list[str]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with self.playback_state_lock:
|
with self.playback_state_lock:
|
||||||
|
@ -637,16 +695,22 @@ class SoundPlugin(Plugin):
|
||||||
completed_callback_events = {}
|
completed_callback_events = {}
|
||||||
|
|
||||||
for i in streams:
|
for i in streams:
|
||||||
if i is None or not (i in self.active_streams):
|
stream = self.active_streams.get(i)
|
||||||
|
if not stream:
|
||||||
|
i = self.stream_name_to_index.get(i)
|
||||||
|
stream = self.active_streams.get(i)
|
||||||
|
if not stream:
|
||||||
|
self.logger.info('No such stream index or name: {}'.
|
||||||
|
format(i))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
stream = self.active_streams[i]
|
|
||||||
if self.completed_callback_events[i]:
|
if self.completed_callback_events[i]:
|
||||||
completed_callback_events[i] = self.completed_callback_events[i]
|
completed_callback_events[i] = self.completed_callback_events[i]
|
||||||
self.playback_state[i] = PlaybackState.STOPPED
|
self.playback_state[i] = PlaybackState.STOPPED
|
||||||
|
|
||||||
for i, event in completed_callback_events.items():
|
for i, event in completed_callback_events.items():
|
||||||
event.wait()
|
event.wait()
|
||||||
|
|
||||||
if i in self.completed_callback_events:
|
if i in self.completed_callback_events:
|
||||||
del self.completed_callback_events[i]
|
del self.completed_callback_events[i]
|
||||||
if i in self.active_streams:
|
if i in self.active_streams:
|
||||||
|
@ -654,6 +718,12 @@ class SoundPlugin(Plugin):
|
||||||
if i in self.stream_mixes:
|
if i in self.stream_mixes:
|
||||||
del self.stream_mixes[i]
|
del self.stream_mixes[i]
|
||||||
|
|
||||||
|
if i in self.stream_index_to_name:
|
||||||
|
name = self.stream_index_to_name[i]
|
||||||
|
del self.stream_index_to_name[i]
|
||||||
|
if name in self.stream_name_to_index:
|
||||||
|
del self.stream_name_to_index[name]
|
||||||
|
|
||||||
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
|
||||||
completed_callback_events.keys()])))
|
completed_callback_events.keys()])))
|
||||||
|
@ -671,7 +741,13 @@ class SoundPlugin(Plugin):
|
||||||
return
|
return
|
||||||
|
|
||||||
for i in streams:
|
for i in streams:
|
||||||
if i is None or not (i in self.active_streams):
|
stream = self.active_streams.get(i)
|
||||||
|
if not stream:
|
||||||
|
i = self.stream_name_to_index.get(i)
|
||||||
|
stream = self.active_streams.get(i)
|
||||||
|
if not stream:
|
||||||
|
self.logger.info('No such stream index or name: {}'.
|
||||||
|
format(i))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
stream = self.active_streams[i]
|
stream = self.active_streams[i]
|
||||||
|
@ -711,7 +787,7 @@ class SoundPlugin(Plugin):
|
||||||
self.recording_paused_changed.set()
|
self.recording_paused_changed.set()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def release(self, stream_index=None,
|
def release(self, stream_index=None, stream_name=None,
|
||||||
sound_index=None, midi_note=None, frequency=None):
|
sound_index=None, midi_note=None, frequency=None):
|
||||||
"""
|
"""
|
||||||
Remove a sound from an active stream, either by sound index (use
|
Remove a sound from an active stream, either by sound index (use
|
||||||
|
@ -723,6 +799,10 @@ class SoundPlugin(Plugin):
|
||||||
active streams)
|
active streams)
|
||||||
:type stream_index: int
|
:type stream_index: int
|
||||||
|
|
||||||
|
:param stream_name: Stream name (default: sound removed from all the
|
||||||
|
active streams)
|
||||||
|
:type stream_index: str
|
||||||
|
|
||||||
:param sound_index: Sound index
|
:param sound_index: Sound index
|
||||||
:type sound_index: int
|
:type sound_index: int
|
||||||
|
|
||||||
|
@ -733,6 +813,12 @@ class SoundPlugin(Plugin):
|
||||||
:type frequency: float
|
:type frequency: float
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if stream_name:
|
||||||
|
if stream_index:
|
||||||
|
raise RuntimeError('stream_index and stream name are ' +
|
||||||
|
'mutually exclusive')
|
||||||
|
stream_index = self.stream_name_to_index.get(stream_name)
|
||||||
|
|
||||||
mixes = {
|
mixes = {
|
||||||
i: mix for i, mix in self.stream_mixes.items()
|
i: mix for i, mix in self.stream_mixes.items()
|
||||||
} if stream_index is None else {
|
} if stream_index is None else {
|
||||||
|
|
|
@ -302,7 +302,7 @@ class Mix(object):
|
||||||
else:
|
else:
|
||||||
wave += sound_wave
|
wave += sound_wave
|
||||||
|
|
||||||
if normalize_range:
|
if normalize_range and len(wave):
|
||||||
scale_factor = (normalize_range[1]-normalize_range[0]) / \
|
scale_factor = (normalize_range[1]-normalize_range[0]) / \
|
||||||
(wave.max()-wave.min())
|
(wave.max()-wave.min())
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue