2018-12-15 01:18:45 +01:00
"""
. . moduleauthor : : Fabio Manganiello < blacklight86 @gmail.com >
"""
2018-12-22 01:47:52 +01:00
import json
import math
2018-12-15 01:18:45 +01:00
import os
import queue
import tempfile
import time
from enum import Enum
from threading import Thread , Event , RLock
from platypush . plugins import Plugin , action
class PlaybackState ( Enum ) :
STOPPED = ' STOPPED ' ,
PLAYING = ' PLAYING ' ,
PAUSED = ' PAUSED '
class RecordingState ( Enum ) :
STOPPED = ' STOPPED ' ,
RECORDING = ' RECORDING ' ,
PAUSED = ' PAUSED '
2018-12-22 01:47:52 +01:00
class Sound ( object ) :
"""
Class model a synthetic sound that can be played through the audio device
"""
STANDARD_A_FREQUENCY = 440.0
STANDARD_A_MIDI_NOTE = 69
_DEFAULT_BLOCKSIZE = 2048
_DEFAULT_BUFSIZE = 20
_DEFAULT_SAMPLERATE = 44100
midi_note = None
frequency = None
gain = 1.0
duration = None
def __init__ ( self , midi_note = midi_note , frequency = None , gain = gain ,
duration = duration , A_frequency = STANDARD_A_FREQUENCY ) :
"""
You can construct a sound either from a MIDI note or a base frequency
: param midi_note : MIDI note code , see
https : / / newt . phys . unsw . edu . au / jw / graphics / notes . GIF
: type midi_note : int
: param frequency : Sound base frequency in Hz
: type frequency : float
: param gain : Note gain / volume between 0.0 and 1.0 ( default : 1.0 )
: type gain : float
: param duration : Note duration in seconds . Default : keep until
release / pause / stop
: type duration : float
: param A_frequency : Reference A4 frequency ( default : 440 Hz )
: type A_frequency : float
"""
if midi_note and frequency :
raise RuntimeError ( ' Please specify either a MIDI note or a base ' +
' frequency ' )
if midi_note :
self . midi_note = midi_note
self . frequency = self . note_to_freq ( midi_note = midi_note ,
A_frequency = A_frequency )
elif frequency :
self . frequency = frequency
self . midi_note = self . freq_to_note ( frequency = frequency ,
A_frequency = A_frequency )
else :
raise RuntimeError ( ' Please specify either a MIDI note or a base ' +
' frequency ' )
self . gain = gain
self . duration = duration
@classmethod
def note_to_freq ( cls , midi_note , A_frequency = STANDARD_A_FREQUENCY ) :
"""
Converts a MIDI note to its frequency in Hz
: param midi_note : MIDI note to convert
: type midi_note : int
: param A_frequency : Reference A4 frequency ( default : 440 Hz )
: type A_frequency : float
"""
return ( 2.0 * * ( ( midi_note - cls . STANDARD_A_MIDI_NOTE ) / 12.0 ) ) \
* A_frequency
@classmethod
def freq_to_note ( cls , frequency , A_frequency = STANDARD_A_FREQUENCY ) :
"""
Converts a frequency in Hz to its closest MIDI note
: param frequency : Frequency in Hz
: type midi_note : float
: param A_frequency : Reference A4 frequency ( default : 440 Hz )
: type A_frequency : float
"""
# TODO return also the offset in % between the provided frequency
# and the standard MIDI note frequency
return int ( 12.0 * math . log ( frequency / A_frequency , 2 )
+ cls . STANDARD_A_MIDI_NOTE )
def get_wave ( self , t_start = 0. , t_end = 0. , samplerate = _DEFAULT_SAMPLERATE ) :
"""
Get the wave binary data associated to this sound
: param t_start : Start offset for the sine wave in seconds . Default : 0
: type t_start : float
: param t_end : End offset for the sine wave in seconds . Default : 0
: type t_end : float
: param samplerate : Audio sample rate . Default : 44100 Hz
: type samplerate : int
: returns : A numpy . ndarray [ n , 1 ] with the raw float values
"""
import numpy as np
x = np . linspace ( t_start , t_end , int ( ( t_end - t_start ) * samplerate ) )
x = x . reshape ( len ( x ) , 1 )
return self . gain * np . sin ( 2 * np . pi * self . frequency * x )
def __str__ ( self ) :
return json . dumps ( {
' midi_note ' : midi_note ,
' frequency ' : frequency ,
' gain ' : gain ,
' duration ' : duration ,
} )
2018-12-23 23:29:52 +01:00
@classmethod
def build ( cls , * args , * * kwargs ) :
"""
Construct a sound object either from a JSON representation or a
key - value representation
"""
if args :
if isinstance ( args [ 0 ] , cls ) :
return args [ 0 ]
if isinstance ( args [ 0 ] , str ) :
kwargs = json . loads ( args [ 0 ] )
elif isinstance ( args [ 0 ] , dict ) :
kwargs = args [ 0 ]
if kwargs :
return Sound ( * * kwargs )
raise RuntimeError ( ' Usage: {} ' . format ( __doc__ ) )
2018-12-22 01:47:52 +01:00
2018-12-15 01:18:45 +01:00
class SoundPlugin ( Plugin ) :
"""
Plugin to interact with a sound device .
Requires :
* * * sounddevice * * ( ` ` pip install sounddevice ` ` )
* * * soundfile * * ( ` ` pip install soundfile ` ` )
* * * numpy * * ( ` ` pip install numpy ` ` )
"""
2018-12-16 17:11:05 +01:00
def __init__ ( self , input_device = None , output_device = None ,
2018-12-22 01:47:52 +01:00
input_blocksize = Sound . _DEFAULT_BLOCKSIZE ,
output_blocksize = Sound . _DEFAULT_BLOCKSIZE ,
playback_bufsize = Sound . _DEFAULT_BUFSIZE , * args , * * kwargs ) :
2018-12-15 01:18:45 +01:00
"""
: 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
: 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
2018-12-16 17:11:05 +01:00
: 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
: 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
: type output_blocksize : int
: param playback_bufsize : Number of audio blocks that will be cached while playing ( default : 20 )
: type playback_bufsize : int
2018-12-15 01:18:45 +01:00
"""
super ( ) . __init__ ( * args , * * kwargs )
2018-12-21 16:52:47 +01:00
self . input_device = input_device
self . output_device = output_device
2018-12-16 17:11:05 +01:00
self . input_blocksize = input_blocksize
self . output_blocksize = output_blocksize
self . playback_bufsize = playback_bufsize
2018-12-15 01:18:45 +01:00
self . playback_state = PlaybackState . STOPPED
self . playback_state_lock = RLock ( )
self . playback_paused_changed = Event ( )
self . recording_state = RecordingState . STOPPED
self . recording_state_lock = RLock ( )
self . recording_paused_changed = Event ( )
2018-12-23 23:29:52 +01:00
self . active_players = { }
self . completed_callback_events = { }
2018-12-15 01:18:45 +01:00
2018-12-21 16:52:47 +01:00
def _get_default_device ( self , category ) :
"""
Query the default audio devices .
: param category : Device category to query . Can be either input or output
: type category : str
"""
import sounddevice as sd
return sd . query_hostapis ( ) [ 0 ] . get ( ' default_ ' + category . lower ( ) + ' _device ' )
2018-12-15 01:18:45 +01:00
@action
def query_devices ( self , category = None ) :
"""
Query the available devices
: param category : Device category to query . Can be either input or output . Default : None ( query all devices )
: type category : str
: returns : A dictionary representing the available devices . Example : :
[
{
" name " : " pulse " ,
" hostapi " : 0 ,
" max_input_channels " : 32 ,
" max_output_channels " : 32 ,
" default_low_input_latency " : 0.008684807256235827 ,
" default_low_output_latency " : 0.008684807256235827 ,
" default_high_input_latency " : 0.034807256235827665 ,
" default_high_output_latency " : 0.034807256235827665 ,
" default_samplerate " : 44100
} ,
{
" name " : " default " ,
" hostapi " : 0 ,
" max_input_channels " : 32 ,
" max_output_channels " : 32 ,
" default_low_input_latency " : 0.008684807256235827 ,
" default_low_output_latency " : 0.008684807256235827 ,
" default_high_input_latency " : 0.034807256235827665 ,
" default_high_output_latency " : 0.034807256235827665 ,
" default_samplerate " : 44100
}
]
"""
2018-12-16 17:29:45 +01:00
import sounddevice as sd
2018-12-15 01:18:45 +01:00
devs = sd . query_devices ( )
if category == ' input ' :
devs = [ d for d in devs if d . get ( ' max_input_channels ' ) > 0 ]
elif category == ' output ' :
devs = [ d for d in devs if d . get ( ' max_output_channels ' ) > 0 ]
return devs
2018-12-23 23:29:52 +01:00
def _play_audio_callback ( self , q , blocksize , streamtype ) :
import sounddevice as sd
is_raw_stream = streamtype == sd . RawOutputStream
def audio_callback ( outdata , frames , time , status ) :
if self . _get_playback_state ( ) == PlaybackState . STOPPED :
raise sd . CallbackAbort
while self . _get_playback_state ( ) == PlaybackState . PAUSED :
self . playback_paused_changed . wait ( )
assert frames == blocksize
if status . output_underflow :
self . logger . warning ( ' Output underflow: increase blocksize? ' )
outdata = ( b ' \x00 ' if is_raw_stream else 0. ) * len ( outdata )
return
assert not status
try :
data = q . get_nowait ( )
except queue . Empty :
self . logger . warning ( ' Buffer is empty: increase buffersize? ' )
raise sd . CallbackAbort
if len ( data ) < len ( outdata ) :
outdata [ : len ( data ) ] = data
outdata [ len ( data ) : ] = ( b ' \x00 ' if is_raw_stream else 0. ) * \
( len ( outdata ) - len ( data ) )
else :
outdata [ : ] = data
return audio_callback
2018-12-15 01:18:45 +01:00
@action
2018-12-23 23:29:52 +01:00
def play ( self , file = None , sound = None , device = None , blocksize = None ,
2018-12-22 01:47:52 +01:00
bufsize = Sound . _DEFAULT_BUFSIZE , samplerate = None , channels = None ) :
2018-12-15 01:18:45 +01:00
"""
2018-12-22 01:47:52 +01:00
Plays a sound file ( support formats : wav , raw ) or a synthetic sound .
2018-12-15 01:18:45 +01:00
2018-12-22 01:47:52 +01:00
: param file : Sound file path . Specify this if you want to play a file
2018-12-15 01:18:45 +01:00
: type file : str
2018-12-23 23:29:52 +01:00
: param sound : Sound to play . Specify this if you want to play
synthetic sounds . You can also create polyphonic sounds by just
calling play multple times .
: type sound : Sound . You can initialize it either from a list
2018-12-22 01:47:52 +01:00
of ` Sound ` objects or from its JSON representation , e . g . :
2018-12-23 23:29:52 +01:00
{
" midi_note " : 69 , # 440 Hz A
" gain " : 1.0 , # Maximum volume
" duration " : 1.0 # 1 second or until release/pause/stop
}
2018-12-22 01:47:52 +01:00
: param device : Output device ( default : default configured device or
system default audio output if not configured )
2018-12-15 01:18:45 +01:00
: type device : int or str
2018-12-22 01:47:52 +01:00
: param blocksize : Audio block size ( default : configured
` output_blocksize ` or 2048 )
2018-12-15 01:18:45 +01:00
: type blocksize : int
: param bufsize : Size of the audio buffer ( default : 20 )
: type bufsize : int
2018-12-22 01:47:52 +01:00
: param samplerate : Audio samplerate . Default : audio file samplerate if
in file mode , 44100 Hz if in synth mode
: type samplerate : int
: param channels : Number of audio channels . Default : number of channels
in the audio file in file mode , 1 if in synth mode
: type channels : int
2018-12-15 01:18:45 +01:00
"""
2018-12-23 23:29:52 +01:00
if not file and not sound :
2018-12-22 01:47:52 +01:00
raise RuntimeError ( ' Please specify either a file to play or a ' +
' list of sound objects ' )
2018-12-16 17:29:45 +01:00
import sounddevice as sd
2018-12-16 17:11:05 +01:00
if blocksize is None :
blocksize = self . output_blocksize
2018-12-15 01:18:45 +01:00
self . playback_paused_changed . clear ( )
2018-12-23 23:29:52 +01:00
stream_index = None
2018-12-22 01:47:52 +01:00
q = queue . Queue ( maxsize = bufsize )
f = None
t = 0.
if file :
file = os . path . abspath ( os . path . expanduser ( file ) )
2018-12-15 01:18:45 +01:00
2018-12-16 16:24:45 +01:00
if device is None :
2018-12-15 01:18:45 +01:00
device = self . output_device
2018-12-21 16:52:47 +01:00
if device is None :
device = self . _get_default_device ( ' output ' )
2018-12-15 01:18:45 +01:00
try :
2018-12-22 01:47:52 +01:00
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
2018-12-15 01:18:45 +01:00
2018-12-23 23:29:52 +01:00
self . logger . info ( ' Starting playback of {} to device [ {} ] ' .
format ( file or sound , device ) )
2018-12-22 01:47:52 +01:00
2018-12-23 23:29:52 +01:00
if sound :
sound = Sound . build ( sound )
2018-12-22 01:47:52 +01:00
# Audio queue pre-fill loop
for _ in range ( bufsize ) :
if f :
2018-12-15 01:18:45 +01:00
data = f . buffer_read ( blocksize , dtype = ' float32 ' )
if not data :
break
2018-12-22 01:47:52 +01:00
else :
blocktime = float ( blocksize / samplerate )
next_t = min ( t + blocktime , sound . duration ) \
if sound . duration is not None else t + blocktime
data = sound . get_wave ( t_start = t , t_end = next_t ,
samplerate = samplerate )
t = next_t
if sound . duration is not None and t > = sound . duration :
break
2018-12-15 01:18:45 +01:00
2018-12-22 01:47:52 +01:00
while self . _get_playback_state ( ) == PlaybackState . PAUSED :
self . playback_paused_changed . wait ( )
2018-12-15 01:18:45 +01:00
2018-12-22 01:47:52 +01:00
q . put_nowait ( data ) # Pre-fill the audio queue
2018-12-15 01:18:45 +01:00
2018-12-22 01:47:52 +01:00
streamtype = sd . RawOutputStream if file else sd . OutputStream
2018-12-23 23:43:12 +01:00
completed_callback_event = Event ( )
2018-12-22 01:47:52 +01:00
stream = streamtype ( samplerate = samplerate , blocksize = blocksize ,
device = device , channels = channels ,
2018-12-23 23:29:52 +01:00
dtype = ' float32 ' ,
callback = self . _play_audio_callback (
q = q , blocksize = blocksize ,
streamtype = streamtype ) ,
2018-12-22 01:47:52 +01:00
finished_callback = completed_callback_event . set )
2018-12-15 01:18:45 +01:00
2018-12-23 23:29:52 +01:00
stream_index = self . start_playback ( stream , completed_callback_event )
2018-12-22 01:47:52 +01:00
with stream :
# Timeout set until we expect all the buffered blocks to
# be consumed
timeout = blocksize * bufsize / samplerate
2018-12-15 01:18:45 +01:00
2018-12-22 01:47:52 +01:00
while True :
while self . _get_playback_state ( ) == PlaybackState . PAUSED :
self . playback_paused_changed . wait ( )
if f :
2018-12-15 01:18:45 +01:00
data = f . buffer_read ( blocksize , dtype = ' float32 ' )
2018-12-22 01:47:52 +01:00
if not data :
break
else :
blocktime = float ( blocksize / samplerate )
next_t = min ( t + blocktime , sound . duration ) \
if sound . duration is not None else t + blocktime
data = sound . get_wave ( t_start = t , t_end = next_t ,
samplerate = samplerate )
t = next_t
if sound . duration is not None and t > = sound . duration :
break
2018-12-15 01:18:45 +01:00
2018-12-22 01:47:52 +01:00
if self . _get_playback_state ( ) == PlaybackState . STOPPED :
raise sd . CallbackAbort
2018-12-15 01:18:45 +01:00
2018-12-22 01:47:52 +01:00
try :
q . put ( data , timeout = timeout )
except queue . Full as e :
if self . _get_playback_state ( ) != PlaybackState . PAUSED :
raise e
2018-12-15 01:18:45 +01:00
2018-12-22 01:47:52 +01:00
completed_callback_event . wait ( )
2018-12-15 01:18:45 +01:00
except queue . Full as e :
self . logger . warning ( ' Playback timeout: audio callback failed? ' )
finally :
2018-12-22 01:47:52 +01:00
if f and not f . closed :
f . close ( )
f = None
2018-12-23 23:29:52 +01:00
self . stop_playback ( stream_index )
2018-12-15 01:18:45 +01:00
@action
def record ( self , file = None , duration = None , device = None , sample_rate = None ,
2018-12-16 17:11:05 +01:00
blocksize = None , latency = 0 , channels = 1 , subtype = ' PCM_24 ' ) :
2018-12-15 01:18:45 +01:00
"""
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 )
: type file : str
: param duration : Recording duration in seconds ( default : record until stop event )
: type duration : float
: param device : Input device ( default : default configured device or system default audio input if not configured )
: type device : int or str
: param sample_rate : Recording sample rate ( default : device default rate )
: type sample_rate : int
2018-12-16 17:11:05 +01:00
: param blocksize : Audio block size ( default : configured ` input_blocksize ` or 2048 )
2018-12-16 16:58:33 +01:00
: type blocksize : int
2018-12-15 01:18:45 +01:00
: param latency : Device latency in seconds ( default : 0 )
: type latency : float
: param channels : Number of channels ( default : 1 )
: type channels : int
: param subtype : Recording subtype - see ` soundfile docs < https : / / pysoundfile . readthedocs . io / en / 0.9 .0 / #soundfile.available_subtypes>`_ for a list of the available subtypes (default: PCM_24)
: type subtype : str
"""
2018-12-16 17:29:45 +01:00
import sounddevice as sd
2018-12-15 01:18:45 +01:00
self . recording_paused_changed . clear ( )
if file :
file = os . path . abspath ( os . path . expanduser ( file ) )
else :
file = tempfile . mktemp ( prefix = ' platypush_recording_ ' , suffix = ' .wav ' ,
dir = ' ' )
if os . path . isfile ( file ) :
2018-12-22 01:47:52 +01:00
self . logger . info ( ' Removing existing audio file {} ' . format ( file ) )
2018-12-15 01:18:45 +01:00
os . unlink ( file )
2018-12-16 16:24:45 +01:00
if device is None :
2018-12-15 01:18:45 +01:00
device = self . input_device
2018-12-21 16:52:47 +01:00
if device is None :
device = self . _get_default_device ( ' input ' )
2018-12-15 01:18:45 +01:00
if sample_rate is None :
dev_info = sd . query_devices ( device , ' input ' )
sample_rate = int ( dev_info [ ' default_samplerate ' ] )
2018-12-16 17:11:05 +01:00
if blocksize is None :
2018-12-16 17:23:26 +01:00
blocksize = self . input_blocksize
2018-12-16 17:11:05 +01:00
2018-12-15 01:18:45 +01:00
q = queue . Queue ( )
def audio_callback ( indata , frames , time , status ) :
while self . _get_recording_state ( ) == RecordingState . PAUSED :
self . recording_paused_changed . wait ( )
if status :
self . logger . warning ( ' Recording callback status: {} ' . format (
str ( status ) ) )
q . put ( indata . copy ( ) )
try :
import soundfile as sf
import numpy
with sf . SoundFile ( file , mode = ' x ' , samplerate = sample_rate ,
channels = channels , subtype = subtype ) as f :
with sd . InputStream ( samplerate = sample_rate , device = device ,
channels = channels , callback = audio_callback ,
2018-12-16 16:58:33 +01:00
latency = latency , blocksize = blocksize ) :
2018-12-15 01:18:45 +01:00
self . start_recording ( )
self . logger . info ( ' Started recording from device [ {} ] to [ {} ] ' .
format ( device , file ) )
recording_started_time = time . time ( )
while self . _get_recording_state ( ) != RecordingState . STOPPED \
and ( duration is None or
time . time ( ) - recording_started_time < duration ) :
while self . _get_recording_state ( ) == RecordingState . PAUSED :
self . recording_paused_changed . wait ( )
2018-12-16 16:58:33 +01:00
get_args = {
' block ' : True ,
' timeout ' : max ( 0 , duration - ( time . time ( ) -
recording_started_time ) )
} if duration is not None else { }
2018-12-15 01:18:45 +01:00
2018-12-16 16:58:33 +01:00
data = q . get ( * * get_args )
f . write ( data )
2018-12-15 01:18:45 +01:00
f . flush ( )
except queue . Empty as e :
self . logger . warning ( ' Recording timeout: audio callback failed? ' )
finally :
self . stop_recording ( )
2018-12-16 16:15:45 +01:00
@action
def recordplay ( self , duration = None , input_device = None , output_device = None ,
2018-12-16 17:11:05 +01:00
sample_rate = None , blocksize = None , latency = 0 , channels = 1 ,
dtype = None ) :
2018-12-16 16:15:45 +01:00
"""
Records audio and plays it on an output sound device ( audio pass - through )
: param duration : Recording duration in seconds ( default : record until stop event )
: type duration : float
: param input_device : Input device ( default : default configured device or system default audio input if not configured )
: type input_device : int or str
: param output_device : Output device ( default : default configured device or system default audio output if not configured )
: type output_device : int or str
: param sample_rate : Recording sample rate ( default : device default rate )
: type sample_rate : int
2018-12-16 17:23:26 +01:00
: param blocksize : Audio block size ( default : configured ` output_blocksize ` or 2048 )
2018-12-16 16:15:45 +01:00
: type blocksize : int
: param latency : Device latency in seconds ( default : 0 )
: type latency : float
: param channels : Number of channels ( default : 1 )
: type channels : int
: param dtype : Data type for the recording - see ` soundfile docs < https : / / python - sounddevice . readthedocs . io / en / 0.3 .12 / _modules / sounddevice . html #rec>`_ for available types (default: input device default)
: type dtype : str
"""
2018-12-16 17:29:45 +01:00
import sounddevice as sd
2018-12-16 16:15:45 +01:00
self . playback_paused_changed . clear ( )
self . recording_paused_changed . clear ( )
2018-12-16 16:24:45 +01:00
if input_device is None :
2018-12-16 16:15:45 +01:00
input_device = self . input_device
2018-12-21 16:52:47 +01:00
if input_device is None :
input_device = self . _get_default_device ( ' input ' )
2018-12-16 16:15:45 +01:00
2018-12-16 16:24:45 +01:00
if output_device is None :
2018-12-16 16:15:45 +01:00
output_device = self . output_device
2018-12-21 16:52:47 +01:00
if output_device is None :
output_device = self . _get_default_device ( ' output ' )
2018-12-16 16:15:45 +01:00
if sample_rate is None :
dev_info = sd . query_devices ( input_device , ' input ' )
sample_rate = int ( dev_info [ ' default_samplerate ' ] )
2018-12-16 17:11:05 +01:00
if blocksize is None :
2018-12-16 17:23:26 +01:00
blocksize = self . output_blocksize
2018-12-16 17:11:05 +01:00
2018-12-16 16:15:45 +01:00
def audio_callback ( indata , outdata , frames , time , status ) :
while self . _get_recording_state ( ) == RecordingState . PAUSED :
self . recording_paused_changed . wait ( )
if status :
self . logger . warning ( ' Recording callback status: {} ' . format (
str ( status ) ) )
outdata [ : ] = indata
2018-12-23 23:29:52 +01:00
stream_index = None
2018-12-16 16:15:45 +01:00
try :
import soundfile as sf
import numpy
2018-12-23 23:29:52 +01:00
stream = sd . Stream ( samplerate = sample_rate , channels = channels ,
blocksize = blocksize , latency = latency ,
device = ( input_device , output_device ) ,
dtype = dtype , callback = audio_callback )
self . start_recording ( )
stream_index = self . start_playback ( stream )
2018-12-16 16:15:45 +01:00
2018-12-23 23:29:52 +01:00
self . logger . info ( ' Started recording pass-through from device ' +
' [ {} ] to device [ {} ] ' .
format ( input_device , output_device ) )
2018-12-16 16:15:45 +01:00
2018-12-23 23:29:52 +01:00
recording_started_time = time . time ( )
2018-12-16 16:15:45 +01:00
2018-12-23 23:29:52 +01:00
while self . _get_recording_state ( ) != RecordingState . STOPPED \
and ( duration is None or
time . time ( ) - recording_started_time < duration ) :
while self . _get_recording_state ( ) == RecordingState . PAUSED :
self . recording_paused_changed . wait ( )
2018-12-16 16:15:45 +01:00
2018-12-23 23:29:52 +01:00
time . sleep ( 0.1 )
2018-12-16 16:15:45 +01:00
except queue . Empty as e :
self . logger . warning ( ' Recording timeout: audio callback failed? ' )
finally :
2018-12-23 23:29:52 +01:00
self . stop_playback ( stream_index )
2018-12-16 16:15:45 +01:00
self . stop_recording ( )
2018-12-23 23:29:52 +01:00
@action
def get_active_players ( self ) :
"""
: returns : A list of active players
"""
return {
i : {
attr : getattr ( stream , attr )
for attr in [ ' active ' , ' closed ' , ' stopped ' , ' blocksize ' ,
' channels ' , ' cpu_load ' , ' device ' , ' dtype ' ,
' latency ' , ' samplerate ' , ' samplesize ' ]
if hasattr ( stream , attr )
} for i , stream in self . active_players . items ( )
}
def start_playback ( self , stream , completed_callback_event = None ) :
stream_index = None
2018-12-15 01:18:45 +01:00
with self . playback_state_lock :
self . playback_state = PlaybackState . PLAYING
2018-12-23 23:29:52 +01:00
stream_index = len ( self . active_players )
self . active_players [ stream_index ] = stream
self . completed_callback_events [ stream_index ] = \
completed_callback_event if completed_callback_event else Event ( )
self . logger . info ( ' Playback started on stream index {} ' .
format ( stream_index ) )
return stream_index
2018-12-15 01:18:45 +01:00
@action
2018-12-23 23:29:52 +01:00
def stop_playback ( self , * streams ) :
2018-12-15 01:18:45 +01:00
with self . playback_state_lock :
2018-12-23 23:29:52 +01:00
if not streams :
streams = self . active_players . keys ( )
updated_n_players = len ( self . active_players )
completed_callback_events = { }
for i in streams :
if i is None or not ( i in self . active_players ) :
continue
stream = self . active_players [ i ]
updated_n_players - = 1
if self . completed_callback_events [ i ] :
completed_callback_events [ i ] = self . completed_callback_events [ i ]
if not updated_n_players :
self . playback_state = PlaybackState . STOPPED
for i , event in completed_callback_events . items ( ) :
event . wait ( )
del self . completed_callback_events [ i ]
del self . active_players [ i ]
2018-12-15 01:18:45 +01:00
self . logger . info ( ' Playback stopped ' )
@action
def pause_playback ( self ) :
with self . playback_state_lock :
if self . playback_state == PlaybackState . PAUSED :
self . playback_state = PlaybackState . PLAYING
elif self . playback_state == PlaybackState . PLAYING :
self . playback_state = PlaybackState . PAUSED
else :
return
self . playback_paused_changed . set ( )
2018-12-23 23:29:52 +01:00
self . logger . info ( ' Playback ' + ( ' paused ' if self . playback_state ==
PlaybackState . PAUSED else ' playing ' ) )
2018-12-15 01:18:45 +01:00
def start_recording ( self ) :
with self . recording_state_lock :
self . recording_state = RecordingState . RECORDING
@action
def stop_recording ( self ) :
with self . recording_state_lock :
self . recording_state = RecordingState . STOPPED
self . logger . info ( ' Recording stopped ' )
@action
def pause_recording ( self ) :
with self . recording_state_lock :
if self . recording_state == RecordingState . PAUSED :
self . recording_state = RecordingState . RECORDING
elif self . recording_state == RecordingState . RECORDING :
self . recording_state = RecordingState . PAUSED
else :
return
self . logger . info ( ' Recording paused state toggled ' )
self . recording_paused_changed . set ( )
def _get_playback_state ( self ) :
with self . playback_state_lock :
return self . playback_state
def _get_recording_state ( self ) :
with self . recording_state_lock :
return self . recording_state
@action
def get_state ( self ) :
return {
' playback_state ' : self . _get_playback_state ( ) . name ,
' recording_state ' : self . _get_recording_state ( ) . name ,
}
# vim:sw=4:ts=4:et: