M2-PT-DRP/source/managers/AudioManager.py

95 lines
2.9 KiB
Python

import threading
import typing
from datetime import datetime
import numpy
import pause
import sortedcontainers
import sounddevice
from source import packets, managers
from source.utils.audio.audio import sample_width_to_type
class AudioManager:
def __init__(self, manager: "managers.Manager"):
self.stream: typing.Optional[sounddevice.OutputStream] = None
# buffer containing the set of audio chunk to play. Sort them by their time to play
self.buffer = sortedcontainers.SortedList(key=lambda audio: audio.time)
# thread support
self.lock = threading.Lock()
self.new_audio_event = threading.Event() # event triggered when a new audio have been added
def add_audio(self, audio: packets.AudioPacket) -> None:
"""
Add a new audio chunk to play
:param audio: the audio chunk to play
"""
with self.lock:
# add the audio packet to the buffer
self.buffer.add(audio)
# trigger the new audio event
self.new_audio_event.set()
def play_audio(self, audio: packets.AudioPacket) -> None:
# create a numpy array for our sample
sample = numpy.frombuffer(audio.data, dtype=sample_width_to_type(audio.sample_width))
# reshape it to have a sub-array for each channels
sample = sample.reshape((-1, audio.channels))
# normalize the sample to be between -1 and 1
sample = sample / (2 ** (audio.sample_width * 8 - 1))
# use float32 for the audio library
sample = sample.astype(numpy.float32)
# wait for the audio given time
pause.until(audio.time)
# update the stream if the audio use different settings
if (
self.stream is None or
self.stream.samplerate != audio.sample_rate or
self.stream.channels != audio.channels
):
self.stream = sounddevice.OutputStream(
samplerate=audio.sample_rate,
channels=audio.channels,
)
# play
self.stream.start()
# write the audio to the stream
self.stream.write(sample)
def handle(self) -> None:
"""
Play the audio chunk in the buffer at the given time
"""
# wait for a new audio packet
# TODO(Faraphel): use self.lock ? seem to softlock the application
if len(self.buffer) == 0:
self.new_audio_event.clear()
self.new_audio_event.wait()
# get the most recent audio packet to play
audio: packets.AudioPacket = self.buffer.pop(0)
# if the audio should have been played before, skip it
if audio.time < datetime.now():
return
# play the audio packet
self.play_audio(audio)
def loop(self) -> None:
"""
Handle forever
"""
while True:
self.handle()