feat: added sound system
This commit is contained in:
91
control/audio.py
Normal file
91
control/audio.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from typing import Optional, NamedTuple
|
||||
from queue import Queue, Empty
|
||||
import wave
|
||||
import uuid
|
||||
import sched
|
||||
import logging
|
||||
|
||||
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 AudioThread:
|
||||
audio: Optional[PyAudio]
|
||||
scheduler: sched.scheduler
|
||||
|
||||
def __init__(self, scheduler):
|
||||
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)
|
||||
|
||||
def quit(self):
|
||||
self.audio.terminate()
|
||||
|
||||
def queue_sound(self, sound):
|
||||
self.to_play.put(sound)
|
||||
|
||||
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.debug(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]
|
||||
Reference in New Issue
Block a user