Files
Bolt-Assistant/bolt_assistant/audio.py
T

62 lines
2.2 KiB
Python

"""The default microphone in, the default speaker out, and two short chimes.
sounddevice (PortAudio) for both directions; MIC_DEVICE / SPEAKER_DEVICE in
.env pick something other than the system default. The chimes are made
here from sine waves — no sound files to ship.
"""
from __future__ import annotations
import array
import math
from typing import Callable, Iterable
from . import config
def open_mic(on_frame: Callable[[bytes], None]):
"""Start the microphone: 16 kHz mono int16, one 80 ms frame per call to
*on_frame*, from PortAudio's thread. Returns the stream (stop()/close())."""
import sounddevice as sd
stream = sd.RawInputStream(
samplerate=config.SAMPLE_RATE, channels=1, dtype="int16",
blocksize=config.FRAME_SAMPLES, device=config.MIC_DEVICE,
callback=lambda data, _frames, _time, _status: on_frame(bytes(data)),
)
stream.start()
return stream
def play(chunks: Iterable[bytes], sample_rate: int = config.TTS_SAMPLE_RATE,
should_stop: Callable[[], bool] = lambda: False) -> bool:
"""Play PCM16 mono chunks as they arrive. False if stopped early."""
import sounddevice as sd
with sd.RawOutputStream(samplerate=sample_rate, channels=1, dtype="int16",
device=config.SPEAKER_DEVICE) as out:
for chunk in chunks:
if should_stop():
out.abort()
return False
out.write(chunk)
return True
def tone(notes, sample_rate: int = config.TTS_SAMPLE_RATE, volume: float = 0.18) -> bytes:
"""(frequency Hz, seconds) notes as PCM16, each with a quick fade in and
out so it doesn't click."""
samples = array.array("h")
for freq, seconds in notes:
count = int(sample_rate * seconds)
fade = max(1, int(sample_rate * 0.012))
for i in range(count):
envelope = min(1.0, i / fade, (count - i) / fade)
samples.append(int(32767 * volume * envelope * math.sin(2 * math.pi * freq * i / sample_rate)))
return samples.tobytes()
# Rising two notes: "I'm listening". Falling: "didn't work".
WAKE_CHIME = tone([(880, 0.07), (1320, 0.09)])
ERROR_CHIME = tone([(520, 0.10), (390, 0.14)])