platypush/platypush/plugins/sound/_streams/_player/_synth/_player.py

118 lines
3.5 KiB
Python

from contextlib import contextmanager
from queue import Queue
from threading import Event
from typing import Any, Generator, Iterable, Optional, Type
import sounddevice as sd
from ...._model import AudioState
from ..._player import AudioPlayer
from ._generator import AudioGenerator
from ._mix import Mix
from ._output import AudioOutputCallback
from ._sound import Sound
class AudioSynthPlayer(AudioPlayer):
"""
The ``AudioSynthPlayer`` can play synthetic sounds (specified either by MIDI
note or raw frequency) to an audio device.
"""
def __init__(
self,
*args,
volume: float,
channels: int,
dtype, # : DTypeLike,
sounds: Optional[Iterable[Sound]] = None,
**kwargs
):
sounds = sounds or []
self.mix = Mix(*sounds, volume=volume, channels=channels, dtype=dtype)
super().__init__(*args, volume=volume, channels=channels, dtype=dtype, **kwargs)
self._generator_stopped = Event()
self._completed_callback_event = Event()
self._audio_queue = Queue( # Queue[NDArray[np.number]]
maxsize=self.queue_size or 0
)
@property
def _stream_type(self) -> Type[sd.OutputStream]:
return sd.OutputStream
@property
def _audio_converter_type(self) -> None:
pass
def __setattr__(self, __name: str, __value: Any):
"""
Make sure that the relevant attributes are synchronized to the mix
object upon set/update.
"""
if __name == 'volume':
# Propagate the volume changes to the mix object.
self.mix.volume = __value
return super().__setattr__(__name, __value)
def _on_converter_timeout(self, *_, **__) -> bool:
"""
Don't break the audio stream if the output converter failed
"""
return True
@property
def _stream_args(self) -> dict:
"""
Register an :class:`.AudioOutputCallback` to fill up the audio buffers.
"""
return {
'callback': AudioOutputCallback(
audio_queue=self._audio_queue,
channels=self.channels,
blocksize=self.blocksize,
queue_timeout=self._queue_timeout,
should_stop=lambda: self.should_stop
or self._generator_stopped.is_set(),
is_paused=lambda: self.state == AudioState.PAUSED,
),
'finished_callback': self._completed_callback_event.set,
**super()._stream_args,
}
@property
def _queue_timeout(self) -> float:
"""
Estimated max read/write timeout on the audio queue.
"""
return self.blocksize * (self.queue_size or 5) / self.sample_rate
@contextmanager
def _audio_generator(self) -> Generator[AudioGenerator, None, None]:
stop_generator = Event()
gen = AudioGenerator(
audio_queue=self._audio_queue,
mix=self.mix,
blocksize=self.blocksize,
sample_rate=self.sample_rate,
queue_timeout=self._queue_timeout,
should_stop=lambda: self.should_stop or stop_generator.is_set(),
wait_running=self._wait_running,
on_stop=self._on_stop,
)
self._generator_stopped.clear()
gen.start()
yield gen
stop_generator.set()
gen.join()
def _on_stop(self):
self._generator_stopped.set()
self.notify_stop()
# vim:sw=4:ts=4:et: