pi features docs
This commit is contained in:
64
pi/audio_handler.py
Normal file
64
pi/audio_handler.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Audio alarm functions — WAV loading and PCM streaming."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import wave
|
||||
from pathlib import Path
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
CHUNK_SIZE = 4096
|
||||
AUDIO_DIR = Path(__file__).parent / "assets" / "alarm"
|
||||
|
||||
|
||||
def find_wav() -> Path:
|
||||
"""Find the first .wav file in the alarm assets directory."""
|
||||
wavs = list(AUDIO_DIR.glob("*.wav"))
|
||||
if not wavs:
|
||||
raise FileNotFoundError(f"No .wav files found in {AUDIO_DIR}")
|
||||
log.info("Using audio file: %s", wavs[0].name)
|
||||
return wavs[0]
|
||||
|
||||
|
||||
def read_wav(path: Path) -> tuple[bytes, int, int, int]:
|
||||
"""Read WAV file and return (pcm_data, sample_rate, channels, bits_per_sample)."""
|
||||
with wave.open(str(path), "rb") as wf:
|
||||
sr = wf.getframerate()
|
||||
ch = wf.getnchannels()
|
||||
bits = wf.getsampwidth() * 8
|
||||
pcm = wf.readframes(wf.getnframes())
|
||||
log.info("WAV loaded: %dHz %dch %dbit, %.1fs, %d bytes",
|
||||
sr, ch, bits, len(pcm) / (sr * ch * (bits // 8)), len(pcm))
|
||||
return pcm, sr, ch, bits
|
||||
|
||||
|
||||
def chunk_bytes(data: bytes, size: int):
|
||||
"""Yield data in fixed-size chunks."""
|
||||
for i in range(0, len(data), size):
|
||||
yield data[i : i + size]
|
||||
|
||||
|
||||
async def stream_alarm(ws, pcm: bytes, sr: int, ch: int, bits: int):
|
||||
"""Stream one alarm cycle to the connected client."""
|
||||
bytes_per_sec = sr * ch * (bits // 8)
|
||||
chunk_duration = CHUNK_SIZE / bytes_per_sec
|
||||
pace_delay = chunk_duration * 0.9 # 90% real-time to avoid underrun
|
||||
|
||||
total_chunks = (len(pcm) + CHUNK_SIZE - 1) // CHUNK_SIZE
|
||||
|
||||
start_msg = json.dumps({
|
||||
"type": "alarm_start",
|
||||
"sample_rate": sr,
|
||||
"channels": ch,
|
||||
"bits": bits,
|
||||
})
|
||||
await ws.send(start_msg)
|
||||
log.info("Sent alarm_start (%d chunks, pace %.1fms)", total_chunks, pace_delay * 1000)
|
||||
|
||||
for i, chunk in enumerate(chunk_bytes(pcm, CHUNK_SIZE)):
|
||||
await ws.send(chunk)
|
||||
await asyncio.sleep(pace_delay)
|
||||
|
||||
await ws.send(json.dumps({"type": "alarm_stop"}))
|
||||
log.info("Sent alarm_stop")
|
||||
Reference in New Issue
Block a user