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:
parent
cfbbff19c1
commit
2d2db499be
2 changed files with 149 additions and 59 deletions
65
platypush/message/event/sound.py
Normal file
65
platypush/message/event/sound.py
Normal 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:
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue