feat(dictate): add voice-reactive listening waveform

This commit is contained in:
jb
2026-08-26 13:40:47 -04:00
parent e234468a73
commit d1a811a5c5
16 changed files with 383 additions and 73 deletions
+69 -3
View File
@@ -1,19 +1,34 @@
import io
import math
import subprocess
import threading
import unittest
from pi_dictate_service.recorder import PW_RECORD_COMMAND, PwRecorder, RecorderError
from pi_dictate_service.recorder import (
AUDIO_LEVEL_WINDOW_BYTES,
PW_RECORD_COMMAND,
PwRecorder,
RecorderError,
)
class TrackingStream(io.BytesIO):
def __init__(self, value: bytes, events: list[str], name: str) -> None:
def __init__(
self,
value: bytes,
events: list[str],
name: str,
max_read_size: int | None = None,
) -> None:
super().__init__(value)
self._events = events
self._name = name
self._max_read_size = max_read_size
self.eof = threading.Event()
def read(self, size: int = -1) -> bytes:
if self._max_read_size is not None:
size = min(size, self._max_read_size)
value = super().read(size)
if not value and not self.eof.is_set():
self._events.append(f"{self._name}-eof")
@@ -91,10 +106,11 @@ class PwRecorderTests(unittest.TestCase):
stderr_limit: int = 64 * 1024,
clock: Clock | None = None,
terminate_returncode: int = -15,
stdout_read_size: int | None = None,
) -> tuple[PwRecorder, FakeProcess, ProcessFactory, list[str]]:
events: list[str] = []
process = FakeProcess(
TrackingStream(pcm, events, "stdout"),
TrackingStream(pcm, events, "stdout", stdout_read_size),
TrackingStream(stderr, events, "stderr"),
returncode=returncode,
timeout=timeout,
@@ -138,6 +154,56 @@ class PwRecorderTests(unittest.TestCase):
self.assertIn("stdout-eof", events)
self.assertIn("wait", events)
def test_tracks_normalized_audio_level_from_pcm_without_exposing_audio(self) -> None:
recorder, process, _factory, _events = self.make_recorder(
pcm=b"\xff\x7f" * (AUDIO_LEVEL_WINDOW_BYTES // 2),
max_bytes=AUDIO_LEVEL_WINDOW_BYTES,
)
recorder.start()
self.assertTrue(process.stdout.eof.wait(1))
self.assertGreater(recorder.audio_level, 0.99)
self.assertLessEqual(recorder.audio_level, 1.0)
recorder.cancel()
def test_audio_level_accumulates_fragmented_reads_into_100ms_windows(self) -> None:
loud_sample = (32767).to_bytes(2, "little", signed=True)
pcm = (
loud_sample * (AUDIO_LEVEL_WINDOW_BYTES // 4)
+ b"\x00" * (AUDIO_LEVEL_WINDOW_BYTES // 2)
+ loud_sample * 400
)
recorder, process, _factory, _events = self.make_recorder(
pcm=pcm,
max_bytes=len(pcm),
stdout_read_size=800,
)
recorder.start()
self.assertTrue(process.stdout.eof.wait(1))
expected_rms = (0.5**0.5) * 32767 / 32768
expected_level = (20 * math.log10(expected_rms) + 60) / 60
self.assertAlmostEqual(recorder.audio_level, expected_level, places=3)
recorder.cancel()
def test_audio_level_maps_silence_known_rms_decay_and_partial_samples(self) -> None:
recorder = PwRecorder()
recorder._update_audio_level(b"\x00\x00" * 16)
self.assertEqual(recorder.audio_level, 0.0)
amplitude = 3277 # approximately -20 dBFS
recorder._update_audio_level(amplitude.to_bytes(2, "little", signed=True) * 16)
self.assertAlmostEqual(recorder.audio_level, 2 / 3, places=3)
recorder._update_audio_level(b"\x00\x00" * 16)
self.assertAlmostEqual(recorder.audio_level, (2 / 3) * 0.65, places=3)
previous = recorder.audio_level
recorder._update_audio_level(b"\xff")
self.assertEqual(recorder.audio_level, previous)
def test_accepts_pw_record_code_one_after_intentional_sigterm(self) -> None:
recorder, process, _factory, _events = self.make_recorder(terminate_returncode=1)
recorder.start()