Running recording in a separate thread so it doesn't block the receiving backend and added playback/recording start/pause/stop events

This commit is contained in:
Fabio Manganiello 2019-02-15 19:23:01 +01:00
parent cfbbff19c1
commit 2d2db499be
2 changed files with 149 additions and 59 deletions

View file

@ -0,0 +1,65 @@
from platypush.message.event import Event
class SoundEvent(Event):
""" Base class for sound events """
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class SoundPlaybackStartedEvent(SoundEvent):
"""
Event triggered when a new sound playback starts
"""
def __init__(self, filename=None, *args, **kwargs):
super().__init__(*args, filename=filename, **kwargs)
class SoundPlaybackStoppedEvent(SoundEvent):
"""
Event triggered when the sound playback stops
"""
def __init__(self, filename=None, *args, **kwargs):
super().__init__(*args, filename=filename, **kwargs)
class SoundPlaybackPausedEvent(SoundEvent):
"""
Event triggered when the sound playback pauses
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class SoundRecordingStartedEvent(SoundEvent):
"""
Event triggered when a new recording starts
"""
def __init__(self, filename=None, *args, **kwargs):
super().__init__(*args, filename=filename, **kwargs)
class SoundRecordingStoppedEvent(SoundEvent):
"""
Event triggered when a sound recording stops
"""
def __init__(self, filename=None, *args, **kwargs):
super().__init__(*args, filename=filename, **kwargs)
class SoundRecordingPausedEvent(SoundEvent):
"""
Event triggered when a sound recording pauses
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# vim:sw=4:ts=4:et:

View file

@ -13,6 +13,13 @@ from enum import Enum
from threading import Thread, Event, RLock from threading import Thread, Event, RLock
from .core import Sound, Mix from .core import Sound, Mix
from platypush.context import get_bus
from platypush.message.event.sound import SoundPlaybackStartedEvent, \
SoundPlaybackPausedEvent, SoundPlaybackStoppedEvent, \
SoundRecordingStartedEvent, SoundRecordingPausedEvent, \
SoundRecordingStoppedEvent
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
@ -32,6 +39,15 @@ class SoundPlugin(Plugin):
""" """
Plugin to interact with a sound device. Plugin to interact with a sound device.
Triggers:
* :class:`platypush.message.event.sound.SoundPlaybackStartedEvent` on playback start
* :class:`platypush.message.event.sound.SoundPlaybackStoppedEvent` on playback stop
* :class:`platypush.message.event.sound.SoundPlaybackPausedEvent` on playback pause
* :class:`platypush.message.event.sound.SoundRecordingStartedEvent` on recording start
* :class:`platypush.message.event.sound.SoundRecordingStoppedEvent` on recording stop
* :class:`platypush.message.event.sound.SoundRecordingPausedEvent` on recording pause
Requires: Requires:
* **sounddevice** (``pip install sounddevice``) * **sounddevice** (``pip install sounddevice``)
@ -384,13 +400,13 @@ class SoundPlugin(Plugin):
@action @action
def record(self, file=None, duration=None, device=None, sample_rate=None, def record(self, outfile=None, duration=None, device=None, sample_rate=None,
blocksize=None, latency=0, channels=1, subtype='PCM_24'): blocksize=None, latency=0, channels=1, subtype='PCM_24'):
""" """
Records audio to a sound file (support formats: wav, raw) Records audio to a sound file (support formats: wav, raw)
:param file: Sound file (default: the method will create a temporary file with the recording) :param outfile: Sound file (default: the method will create a temporary file with the recording)
:type file: str :type outfile: str
:param duration: Recording duration in seconds (default: record until stop event) :param duration: Recording duration in seconds (default: record until stop event)
:type duration: float :type duration: float
@ -414,81 +430,90 @@ class SoundPlugin(Plugin):
:type subtype: str :type subtype: str
""" """
import sounddevice as sd def recording_thread(outfile, duration, device, sample_rate, blocksize,
latency, channels, subtype):
import sounddevice as sd
self.recording_paused_changed.clear() self.recording_paused_changed.clear()
if file: if outfile:
file = os.path.abspath(os.path.expanduser(file)) outfile = os.path.abspath(os.path.expanduser(outfile))
else: else:
file = tempfile.mktemp(prefix='platypush_recording_', suffix='.wav', outfile = tempfile.NamedTemporaryFile(
dir='') prefix='recording_', suffix='.wav', delete=False,
dir=tempfile.gettempdir()).name
if os.path.isfile(file): if os.path.isfile(outfile):
self.logger.info('Removing existing audio file {}'.format(file)) self.logger.info('Removing existing audio file {}'.format(outfile))
os.unlink(file) os.unlink(outfile)
if device is None: if device is None:
device = self.input_device device = self.input_device
if device is None: if device is None:
device = self._get_default_device('input') device = self._get_default_device('input')
if sample_rate is None: if sample_rate is None:
dev_info = sd.query_devices(device, 'input') dev_info = sd.query_devices(device, 'input')
sample_rate = int(dev_info['default_samplerate']) sample_rate = int(dev_info['default_samplerate'])
if blocksize is None: if blocksize is None:
blocksize = self.input_blocksize blocksize = self.input_blocksize
q = queue.Queue() q = queue.Queue()
def audio_callback(indata, frames, time, status): def audio_callback(indata, frames, time, status):
while self._get_recording_state() == RecordingState.PAUSED: while self._get_recording_state() == RecordingState.PAUSED:
self.recording_paused_changed.wait() self.recording_paused_changed.wait()
if status: if status:
self.logger.warning('Recording callback status: {}'.format( self.logger.warning('Recording callback status: {}'.format(
str(status))) str(status)))
q.put(indata.copy()) q.put(indata.copy())
try: try:
import soundfile as sf import soundfile as sf
import numpy import numpy
with sf.SoundFile(file, mode='x', samplerate=sample_rate, with sf.SoundFile(outfile, mode='x', samplerate=sample_rate,
channels=channels, subtype=subtype) as f: channels=channels, subtype=subtype) as f:
with sd.InputStream(samplerate=sample_rate, device=device, with sd.InputStream(samplerate=sample_rate, device=device,
channels=channels, callback=audio_callback, channels=channels, callback=audio_callback,
latency=latency, blocksize=blocksize): latency=latency, blocksize=blocksize):
self.start_recording() self.start_recording()
self.logger.info('Started recording from device [{}] to [{}]'. get_bus().post(SoundRecordingStartedEvent(filename=outfile))
format(device, file)) self.logger.info('Started recording from device [{}] to [{}]'.
format(device, outfile))
recording_started_time = time.time() recording_started_time = time.time()
while self._get_recording_state() != RecordingState.STOPPED \ while self._get_recording_state() != RecordingState.STOPPED \
and (duration is None or and (duration is None or
time.time() - recording_started_time < duration): time.time() - recording_started_time < duration):
while self._get_recording_state() == RecordingState.PAUSED: while self._get_recording_state() == RecordingState.PAUSED:
self.recording_paused_changed.wait() self.recording_paused_changed.wait()
get_args = { get_args = {
'block': True, 'block': True,
'timeout': max(0, duration - (time.time() - 'timeout': max(0, duration - (time.time() -
recording_started_time)) recording_started_time))
} if duration is not None else {} } if duration is not None else {}
data = q.get(**get_args) data = q.get(**get_args)
f.write(data) f.write(data)
f.flush() f.flush()
except queue.Empty as e: except queue.Empty as e:
self.logger.warning('Recording timeout: audio callback failed?') self.logger.warning('Recording timeout: audio callback failed?')
finally: finally:
self.stop_recording() self.stop_recording()
get_bus().post(SoundRecordingStoppedEvent(filename=outfile))
Thread(target=recording_thread, args=(
outfile, duration, device, sample_rate, blocksize, latency, channels,
subtype)).start()
@action @action