Files
WallE/control/audio.py

111 lines
3.7 KiB
Python

import json
import logging
import os.path
import sched
import uuid
import wave
from queue import Empty, Queue
from typing import Optional
from pyaudio import PyAudio, Stream, paContinue
LOG = logging.getLogger(__name__)
CHUNK_SIZE = 1024
INTERVAL_QUEUE = 0.1
INTERVAL_STREAM = 0.1
class PlayEntry:
wave_file: wave.Wave_read
stream: Optional[Stream]
def __init__(self, wave_file: wave.Wave_read):
self.wave_file = wave_file
self.stream = None
def set_stream(self, stream: Stream):
self.stream = stream
def pyaudio_callback(self, in_data, frame_count, time_info, status):
data = self.wave_file.readframes(frame_count)
return data, paContinue
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.default_output_device = self.audio.get_default_output_device_info()
self.default_sample_rate = int(self.default_output_device['defaultSampleRate'])
self.scheduler = scheduler
self.scheduler.enter(0, 2, self.start_streams)
self.scheduler.enter(0, 1, self.stream_cleanup)
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)
LOG.info(f"Queueing stream for {item!r}")
wave_file = wave.open(item) # type: wave.Wave_read
entry = PlayEntry(wave_file)
stream = self.audio.open(
format=self.audio.get_format_from_width(wave_file.getsampwidth()),
channels=wave_file.getnchannels(),
rate=wave_file.getframerate(),
output=True,
stream_callback=entry.pyaudio_callback,
)
entry.set_stream(stream)
entry_id = uuid.uuid4()
while entry_id in self.playing:
entry_id = uuid.uuid4()
self.playing[entry_id] = entry
self.to_play.task_done()
except Empty:
pass
except IOError as e:
LOG.warning('Error in opening stream', exc_info=e)
except Exception as e:
LOG.error('Fatal error in opening stream', exc_info=e)
raise e
def stream_cleanup(self):
self.scheduler.enter(INTERVAL_STREAM, 1, self.stream_cleanup)
finished_streams = []
for stream_id, entry in self.playing.items(): # type: uuid.UUID, PlayEntry
try:
if entry.stream.is_active():
continue
else:
LOG.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:
LOG.warning('A fatal error occurred while rendering sound', exc_info=e)
raise e
for finished in finished_streams:
del self.playing[finished]