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

111 lines
3.0 KiB
Python

from abc import ABC
from typing import IO, Iterable, List, Optional, Self, Type, Union
from typing_extensions import override
import numpy as np
import sounddevice as sd
from platypush.message.event.sound import (
SoundPlaybackPausedEvent,
SoundPlaybackResumedEvent,
SoundPlaybackStartedEvent,
SoundPlaybackStoppedEvent,
)
from ..._converters import RawOutputAudioConverter
from ..._model import StreamType
from .._base import AudioThread
class AudioPlayer(AudioThread, ABC):
"""
Base ``AudioPlayer`` class.
An ``AudioPlayer`` thread is responsible for playing audio (either from a
file/URL or from a synthetic source) to an output device, writing it to the
converter process and dispatching the converted audio to the registered
consumers.
"""
def __init__(
self, *args, sound: Optional[Union[dict, Iterable[dict]]] = None, **kwargs
):
super().__init__(*args, **kwargs)
self.sound = sound
@classmethod
def build(
cls,
infile: Optional[str] = None,
sound: Optional[Union[dict, Iterable[dict]]] = None,
**kwargs,
) -> Self:
from ._resource import AudioResourcePlayer
from ._synth import AudioSynthPlayer, Sound
if infile:
return AudioResourcePlayer(infile=infile, **kwargs)
if sound:
sounds: List[dict] = ( # type: ignore
[sound] if isinstance(sound, dict) else sound
)
return AudioSynthPlayer(sounds=[Sound.build(**s) for s in sounds], **kwargs)
raise AssertionError('Either infile or url must be specified')
@property
@override
def direction(self) -> StreamType:
return StreamType.OUTPUT
@override
def _on_converter_timeout(self, *_, **__) -> bool:
return False # break
@property
@override
def _stream_type(self) -> Type[sd.RawOutputStream]:
return sd.RawOutputStream
@property
@override
def _audio_converter_type(self) -> Type[RawOutputAudioConverter]:
return RawOutputAudioConverter
@override
def _on_audio_converted(self, data: bytes, out_f: Optional[IO] = None):
if self.audio_stream:
self.audio_stream.write(
np.asarray(
self.gain
* np.frombuffer(data, dtype=self.dtype).reshape(-1, self.channels),
dtype=self.dtype,
)
)
super()._on_audio_converted(data, out_f)
@property
@override
def _started_event_type(self) -> Type[SoundPlaybackStartedEvent]:
return SoundPlaybackStartedEvent
@property
@override
def _stopped_event_type(self) -> Type[SoundPlaybackStoppedEvent]:
return SoundPlaybackStoppedEvent
@property
@override
def _paused_event_type(self) -> Type[SoundPlaybackPausedEvent]:
return SoundPlaybackPausedEvent
@property
@override
def _resumed_event_type(self) -> Type[SoundPlaybackResumedEvent]:
return SoundPlaybackResumedEvent
# vim:sw=4:ts=4:et: