102 lines
3.0 KiB
Python
102 lines
3.0 KiB
Python
|
from logging import getLogger
|
||
|
from queue import Full, Queue
|
||
|
from threading import Thread
|
||
|
from time import time
|
||
|
from typing import Any, Callable, Optional
|
||
|
|
||
|
import numpy as np
|
||
|
from numpy.typing import NDArray
|
||
|
|
||
|
from ._mix import Mix
|
||
|
|
||
|
|
||
|
class AudioGenerator(Thread):
|
||
|
"""
|
||
|
The ``AudioGenerator`` class is a thread that generates synthetic raw audio
|
||
|
waves and dispatches them to a queue that can be consumed by other players,
|
||
|
streamers and converters.
|
||
|
"""
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
*args,
|
||
|
audio_queue: Queue[NDArray[np.number]],
|
||
|
mix: Mix,
|
||
|
blocksize: int,
|
||
|
sample_rate: int,
|
||
|
queue_timeout: Optional[float] = None,
|
||
|
should_stop: Callable[[], bool] = lambda: False,
|
||
|
wait_running: Callable[[], Any] = lambda: None,
|
||
|
on_stop: Callable[[], Any] = lambda: None,
|
||
|
**kwargs,
|
||
|
):
|
||
|
super().__init__(*args, **kwargs)
|
||
|
self._audio_queue = audio_queue
|
||
|
self._t_start: float = 0
|
||
|
self._blocksize: int = blocksize
|
||
|
self._sample_rate: int = sample_rate
|
||
|
self._blocktime = self._blocksize / self._sample_rate
|
||
|
self._should_stop = should_stop
|
||
|
self._queue_timeout = queue_timeout
|
||
|
self._wait_running = wait_running
|
||
|
self._on_stop = on_stop
|
||
|
self.mix = mix
|
||
|
self.logger = getLogger(__name__)
|
||
|
|
||
|
def _next_t(self, t: float) -> float:
|
||
|
"""
|
||
|
Calculates the next starting time for the wave function.
|
||
|
"""
|
||
|
return (
|
||
|
min(t + self._blocktime, self._duration)
|
||
|
if self._duration is not None
|
||
|
else t + self._blocktime
|
||
|
)
|
||
|
|
||
|
def should_stop(self) -> bool:
|
||
|
"""
|
||
|
Stops if the upstream dependencies have signalled to stop or if the
|
||
|
duration is set and we have reached it.
|
||
|
"""
|
||
|
return self._should_stop() or (
|
||
|
self._duration is not None and time() - self._t_start >= self._duration
|
||
|
)
|
||
|
|
||
|
@property
|
||
|
def _duration(self) -> Optional[float]:
|
||
|
"""
|
||
|
Proxy to the mix object's duration.
|
||
|
"""
|
||
|
return self.mix.duration()
|
||
|
|
||
|
def run(self):
|
||
|
super().run()
|
||
|
self._t_start = time()
|
||
|
t = 0
|
||
|
|
||
|
while not self.should_stop():
|
||
|
self._wait_running()
|
||
|
if self.should_stop():
|
||
|
break
|
||
|
|
||
|
next_t = self._next_t(t)
|
||
|
|
||
|
try:
|
||
|
data = self.mix.get_wave(
|
||
|
t_start=t, t_end=next_t, sample_rate=self._sample_rate
|
||
|
)
|
||
|
except Exception as e:
|
||
|
self.logger.warning('Could not generate the audio wave: %s', e)
|
||
|
break
|
||
|
|
||
|
try:
|
||
|
self._audio_queue.put(data, timeout=self._queue_timeout)
|
||
|
t = next_t
|
||
|
except Full:
|
||
|
self.logger.warning(
|
||
|
'The processing queue is full: either the audio consumer is stuck, '
|
||
|
'or you may want to increase queue_size'
|
||
|
)
|
||
|
|
||
|
self._on_stop()
|