feat: cleanup and rework audio system

This commit is contained in:
2020-12-22 16:50:30 +01:00
parent 57b92b2864
commit 56fa2a92a9
12 changed files with 302 additions and 161 deletions

View File

@@ -1,44 +1,35 @@
from typing import Optional, NamedTuple
from queue import Queue, Empty
import wave
import uuid
import sched
import logging
import json
import logging
import os.path
import sched
import uuid
import wave
from queue import Empty, Queue
from typing import Optional
import audioop
from pyaudio import PyAudio, Stream
from pyaudio import PyAudio, Stream, paContinue
logger = logging.getLogger(__name__)
LOG = logging.getLogger(__name__)
CHUNK_SIZE = 1024
INTERVAL_QUEUE = 0.1
INTERVAL_STREAM_FAST = 0.0001
INTERVAL_STREAM_SLOW = 0.001
INTERVAL_STREAM = 0.1
class PlayEntry:
def __init__(self, wave_file: wave.Wave_read, stream: Stream, device_framerate):
self.wave_file = wave_file
self.stream = stream
self.cvstate = None
self.device_framerate = device_framerate
self.sample_width = wave_file.getsampwidth()
self.channels = wave_file.getnchannels()
self.frame_rate = wave_file.getframerate()
def get_actual_data(self, nframes):
data = self.wave_file.readframes(nframes)
new_data, self.cvstate = audioop.ratecv(data, self.sample_width, self.channels, self.frame_rate,
self.device_framerate, self.cvstate)
return new_data
wave_file: wave.Wave_read
stream: Stream
cvstate: Optional['audioop.RatecvState']
actual_size: int = None
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:
@@ -53,7 +44,7 @@ class AudioSystem:
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.fill_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'])
@@ -75,50 +66,45 @@ class AudioSystem:
# drain the queue
while True:
item = self.to_play.get(False)
logger.info(f"Queueing stream for {item!r}")
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=self.default_sample_rate,
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] = PlayEntry(wave_file, stream, self.default_sample_rate)
self.playing[entry_id] = entry
self.to_play.task_done()
except Empty:
pass
except IOError as e:
logger.warning('Error in opening stream', exc_info=e)
LOG.warning('Error in opening stream', exc_info=e)
except Exception as e:
logger.error('Fatal error in opening stream', exc_info=e)
LOG.error('Fatal error in opening stream', exc_info=e)
raise e
def fill_streams(self):
self.scheduler.enter(INTERVAL_STREAM_FAST if len(self.playing) > 0 else INTERVAL_STREAM_SLOW,
1, self.fill_streams)
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.get_write_available() < (CHUNK_SIZE * 1.5):
continue # not enough space in buffer, wait for next iteration
try:
data = entry.get_actual_data(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)
if entry.stream.is_active():
continue
else:
logger.debug("Done playing sound, shutting down stream")
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:
logger.warning('A fatal error occurred while rendering sound', exc_info=e)
LOG.warning('A fatal error occurred while rendering sound', exc_info=e)
raise e
for finished in finished_streams:
del self.playing[finished]