62 lines
2.2 KiB
Python
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)])
|