Files
WallE/control/audio.py

102 lines
3.5 KiB
Python

from typing import Optional, NamedTuple
from queue import Queue, Empty
import wave
import uuid
import sched
import logging
import json
import os.path
from pyaudio import PyAudio, Stream
logger = logging.getLogger(__name__)
CHUNK_SIZE = 1024
INTERVAL_QUEUE = 0.1
INTERVAL_STREAM = 0.0001
class PlayEntry(NamedTuple):
wave_file: wave.Wave_read
stream: Stream
class AudioSystem:
audio: Optional[PyAudio]
scheduler: sched.scheduler
def __init__(self, scheduler: sched.scheduler, sound_file: str):
self.to_play = Queue()
self.playing = {}
self.audio = PyAudio()
self.scheduler = scheduler
self.scheduler.enter(0, 2, self.start_streams)
self.scheduler.enter(0, 1, self.fill_streams)
with open(sound_file) as f:
self.sound_data = json.load(f)
self.sound_dir = os.path.realpath(self.sound_data['sound_dir'])
def quit(self):
self.audio.terminate()
def queue_sound(self, sound):
try:
sound_entry = next(filter(lambda i: i['name'] == sound, self.sound_data['sounds']))
except StopIteration:
raise ValueError(f"Sound {sound!r} is not known")
self.to_play.put(os.path.join(self.sound_dir, sound_entry['path']))
def start_streams(self):
self.scheduler.enter(INTERVAL_QUEUE, 2, self.start_streams)
try:
# drain the queue
while True:
item = self.to_play.get(False)
logger.info(f"Queueing stream for {item!r}")
wave_file = wave.open(item) # type: wave.Wave_read
stream = self.audio.open(
format=self.audio.get_format_from_width(wave_file.getsampwidth()),
channels=wave_file.getnchannels(),
rate=wave_file.getframerate(),
output=True,
)
entry_id = uuid.uuid4()
while entry_id in self.playing:
entry_id = uuid.uuid4()
self.playing[entry_id] = PlayEntry(wave_file, stream)
self.to_play.task_done()
except Empty:
pass
except IOError as e:
logger.warning('Error in opening stream', exc_info=e)
except Exception as e:
logger.error('Fatal error in opening stream', exc_info=e)
raise e
def fill_streams(self):
self.scheduler.enter(INTERVAL_STREAM, 1, self.fill_streams)
finished_streams = []
for stream_id, entry in self.playing.items(): # type: uuid.UUID, PlayEntry
try:
if entry.stream.get_write_available() < CHUNK_SIZE:
continue # not enough space in buffer, wait for next iteration
try:
data = entry.wave_file.readframes(CHUNK_SIZE)
except IOError as e:
logger.warning('An IO Error occurred while reading WAV file', exc_info=e)
data = b''
if data:
entry.stream.write(data)
else:
logger.debug("Done playing sound, shutting down stream")
entry.stream.stop_stream()
entry.stream.close()
entry.wave_file.close()
finished_streams.append(stream_id)
except Exception as e:
logger.warning('A fatal error occurred while rendering sound', exc_info=e)
raise e
for finished in finished_streams:
del self.playing[finished]