feat: cleanup and rework audio system
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -20,27 +20,3 @@ class Camera:
|
||||
byte_stream = BytesIO()
|
||||
jpg.save(byte_stream, 'JPEG')
|
||||
return bytes(byte_stream.getbuffer())
|
||||
|
||||
def generate_images(self):
|
||||
try:
|
||||
while True:
|
||||
return_code, image = self.capture.read()
|
||||
if not return_code:
|
||||
continue
|
||||
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
jpg = Image.fromarray(image_rgb)
|
||||
byte_stream = BytesIO()
|
||||
jpg.save(byte_stream, 'JPEG')
|
||||
yield bytes(byte_stream.getbuffer())
|
||||
finally:
|
||||
self.capture.release()
|
||||
|
||||
def mjpeg_stream(self, boundary: bytes):
|
||||
for frame in self.generate_images():
|
||||
if not frame:
|
||||
break
|
||||
yield b''.join([
|
||||
b'--', boundary, b'\r\n',
|
||||
b'Content-Type: image/jpeg\r\n\r\n',
|
||||
frame,
|
||||
b'\r\n'])
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import atexit
|
||||
import logging
|
||||
|
||||
try:
|
||||
from RPi import GPIO
|
||||
except ImportError:
|
||||
from .mockGpio import GPIO
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
PWM_FREQUENCY = 10_000 # 10 KHz
|
||||
MOTORS = []
|
||||
@@ -17,7 +19,7 @@ def setup_gpio():
|
||||
|
||||
|
||||
def cleanup_gpio():
|
||||
print("cleaning up")
|
||||
LOG.info("cleaning up")
|
||||
for motor in MOTORS:
|
||||
# noinspection PyProtectedMember
|
||||
motor._stop()
|
||||
@@ -49,7 +51,7 @@ class DcMotor:
|
||||
self._set_reverse(val)
|
||||
absval = abs(val)
|
||||
if absval > 1:
|
||||
print(f"clipping {val} to 1")
|
||||
logging.warning(f"clipping {val} to 1")
|
||||
absval = 1
|
||||
|
||||
self.pwm_enable.ChangeDutyCycle(absval * 100)
|
||||
|
||||
Reference in New Issue
Block a user