111 lines
3.7 KiB
Python
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]
|