platypush/platypush/plugins/sound/_manager/_main.py

292 lines
10 KiB
Python
Raw Normal View History

from logging import getLogger
import os
import stat
from threading import Event
from time import time
from typing import Iterable, List, Optional, Union
from .._model import AudioDevice, DeviceType, StreamType
from .._streams import AudioPlayer, AudioRecorder, AudioThread
from ._device import DeviceManager
from ._stream import StreamManager
class AudioManager:
"""
The audio manager is responsible for managing multiple audio controllers and
their access to audio resources.
It main purpose is to act as a proxy/facade between the high-level audio
plugin and the audio functionalities (allocating streams, managing the state
of the player and recorder processes, etc.).
"""
_default_signal_timeout = 2
def __init__(
self,
should_stop: Event,
input_blocksize: int,
output_blocksize: int,
input_device: Optional[DeviceType] = None,
output_device: Optional[DeviceType] = None,
queue_size: Optional[int] = None,
):
"""
:param should_stop: Event to synchronize the audio manager stop.
:param input_blocksize: Block size for the input stream.
:param output_blocksize: Block size for the output stream.
:param input_device: Default device to use for the input stream.
:param output_device: Default device to use for the output stream.
:param queue_size: Maximum size of the audio queues.
"""
self._should_stop = should_stop
self._device_manager = DeviceManager(
input_device=input_device, output_device=output_device
)
self._stream_manager = StreamManager(device_manager=self._device_manager)
self.logger = getLogger(__name__)
self.input_blocksize = input_blocksize
self.output_blocksize = output_blocksize
self.queue_size = queue_size
def create_player(
self,
device: DeviceType,
channels: int,
volume: float,
infile: Optional[str] = None,
sound: Optional[Union[dict, Iterable[dict]]] = None,
duration: Optional[float] = None,
sample_rate: Optional[int] = None,
dtype: str = 'int16',
blocksize: Optional[int] = None,
latency: Union[float, str] = 'high',
stream_name: Optional[str] = None,
) -> AudioPlayer:
"""
Create an audio player thread.
:param device: Audio device to use.
:param channels: Number of output channels.
:param volume: Output volume, between 0 and 100.
:param infile: File or URL to play.
:param sound: Alternatively to a file/URL, you can play synthetic
sounds.
:param duration: Duration of the stream in seconds.
:param sample_rate: Sample rate of the stream.
:param dtype: Data type of the stream.
:param blocksize: Block size of the stream.
:param latency: Latency of the stream.
:param stream_name: Name of the stream.
"""
dev = self._device_manager.get_device(device, type=StreamType.OUTPUT)
player = AudioPlayer.build(
device=device,
infile=infile,
sound=sound,
duration=duration,
volume=volume,
sample_rate=sample_rate or dev.default_samplerate,
dtype=dtype,
blocksize=blocksize or self.output_blocksize,
latency=latency,
channels=channels,
queue_size=self.queue_size,
should_stop=self._should_stop,
)
self._stream_manager.register(
player, dev, StreamType.OUTPUT, stream_name=stream_name
)
return player
def create_recorder(
self,
device: DeviceType,
output_device: Optional[DeviceType] = None,
fifo: Optional[str] = None,
outfile: Optional[str] = None,
duration: Optional[float] = None,
sample_rate: Optional[int] = None,
dtype: str = 'int16',
blocksize: Optional[int] = None,
latency: Union[float, str] = 'high',
channels: int = 1,
volume: float = 100,
redis_queue: Optional[str] = None,
format: str = 'wav', # pylint: disable=redefined-builtin
stream: bool = True,
stream_name: Optional[str] = None,
play_audio: bool = False,
) -> AudioRecorder:
"""
Create an audio recorder thread.
:param device: Audio device to use.
:param output_device: Output device to use.
:param fifo: Path to an output FIFO file to use to synchronize the audio
to other processes.
:param outfile: Optional output file for the recorded audio.
:param duration: Duration of the recording in seconds.
:param sample_rate: Sample rate of the stream.
:param dtype: Data type of the stream.
:param blocksize: Block size of the stream.
:param latency: Latency of the stream.
:param channels: Number of output channels.
:param volume: Input volume, between 0 and 100.
:param redis_queue: Name of the Redis queue to use.
:param format: Format of the recorded audio.
:param stream: Whether to stream the recorded audio.
:param play_audio: Whether to play the recorded audio in real-time.
:param stream_name: Name of the stream.
"""
blocksize = blocksize or self.input_blocksize
dev = self._device_manager.get_device(device, type=StreamType.OUTPUT)
if fifo:
fifo = os.path.expanduser(fifo)
if os.path.exists(fifo) and stat.S_ISFIFO(os.stat(fifo).st_mode):
self.logger.info('Removing previous input stream FIFO %s', fifo)
os.unlink(fifo)
os.mkfifo(fifo, 0o644)
outfile = fifo
elif outfile:
outfile = os.path.expanduser(outfile)
outfile = outfile or fifo or os.devnull
recorder = AudioRecorder(
device=(
(
dev.index,
self._device_manager.get_device(
type=StreamType.OUTPUT, device=output_device
).index,
)
if play_audio
else dev.index
),
outfile=outfile,
duration=duration,
sample_rate=sample_rate or dev.default_samplerate,
dtype=dtype,
blocksize=blocksize,
latency=latency,
output_format=format,
channels=channels,
volume=volume,
redis_queue=redis_queue,
stream=stream,
audio_pass_through=play_audio,
queue_size=self.queue_size,
should_stop=self._should_stop,
)
self._stream_manager.register(
recorder, dev, StreamType.INPUT, stream_name=stream_name
)
return recorder
def get_device(
self,
device: Optional[DeviceType] = None,
type: Optional[StreamType] = None, # pylint: disable=redefined-builtin
) -> AudioDevice:
"""
Proxy to ``self._device_manager.get_device``.
"""
return self._device_manager.get_device(device=device, type=type)
def get_devices(
self,
type: Optional[StreamType] = None, # pylint: disable=redefined-builtin
) -> List[AudioDevice]:
"""
Proxy to ``self._device_manager.get_devices``.
"""
return self._device_manager.get_devices(type=type)
def get_streams(
self,
device: Optional[DeviceType] = None,
type: Optional[StreamType] = None, # pylint: disable=redefined-builtin
streams: Optional[Iterable[Union[str, int]]] = None,
) -> List[AudioThread]:
"""
Proxy to ``self._stream_manager.get``.
"""
return self._stream_manager.get(device=device, type=type, streams=streams)
def stop_audio(
self,
device: Optional[DeviceType] = None,
type: Optional[StreamType] = None, # pylint: disable=redefined-builtin
streams: Optional[Iterable[Union[str, int]]] = None,
timeout: Optional[float] = 2,
):
"""
Stops audio sessions.
:param device: Filter by host audio device.
:param type: Filter by stream type (input or output).
:param streams: Filter by stream indices/names.
:param timeout: Wait timeout in seconds.
"""
streams_to_stop = self._stream_manager.get(device, type, streams=streams)
# Send the stop signals
for audio_thread in streams_to_stop:
audio_thread.notify_stop()
# Wait for termination (with timeout)
wait_start = time()
for audio_thread in streams_to_stop:
audio_thread.join(
timeout=max(0, timeout - (time() - wait_start))
if timeout is not None
else None
)
# Remove references
for audio_thread in streams_to_stop:
self._stream_manager.unregister(audio_thread)
def pause_audio(
self,
device: Optional[DeviceType] = None,
type: Optional[StreamType] = None, # pylint: disable=redefined-builtin
streams: Optional[Iterable[Union[str, int]]] = None,
):
"""
Pauses/resumes audio sessions.
:param device: Filter by host audio device.
:param type: Filter by stream type (input or output).
:param streams: Filter by stream indices/names.
"""
streams_to_pause = self._stream_manager.get(device, type, streams=streams)
# Send the pause toggle signals
for audio_thread in streams_to_pause:
audio_thread.notify_pause()
def set_volume(
self,
volume: float,
device: Optional[DeviceType] = None,
streams: Optional[Iterable[Union[str, int]]] = None,
):
"""
:param volume: New volume, between 0 and 100.
:param device: Set the volume only on the specified device (default:
all).
:param streams: Set the volume only on the specified list of stream
indices/names (default: all).
"""
stream_objs = self._stream_manager.get(device=device, streams=streams)
for stream in stream_objs:
stream.volume = volume