USB mic switch acts as on/off: listen only while the USB source exists, chime when it appears

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzKpYaEsuWDevuNnvSjcFW
This commit is contained in:
idriz
2026-09-02 19:14:00 +02:00
parent 7e69c3af24
commit 92e6254888
2 changed files with 42 additions and 11 deletions

View File

@@ -9,7 +9,7 @@ from pathlib import Path
from openai import AsyncOpenAI
from . import coloring, printer, render, textgen, wiki
from . import audio, coloring, printer, render, textgen, wiki
from .audio import Mic, Speaker
from .config import Config
@@ -175,20 +175,29 @@ class Assistant:
# ---------- audio petlje ----------
async def mic_loop(self) -> None:
mic_missing = False
"""Šalje zvuk modelu. Ako je USB mikrofon isključen prekidačem (nestane iz PipeWire-a), ne sluša ništa."""
last_check = 0.0
present = False
while True:
now = asyncio.get_event_loop().time()
if now - last_check > 1.5:
present = await audio.source_present(self.cfg.mic_device)
last_check = now
if not present:
if self.mic.running:
await self.mic.stop()
log.info("mikrofon isključen (prekidač), ne slušam")
await asyncio.sleep(0.5)
continue
if not self.mic.running:
await self.mic.start()
log.info("mikrofon uključen, slušam")
await self.speaker.write(audio.tone())
chunk = await self.mic.read()
if not chunk:
if not mic_missing:
log.error("mikrofon ne daje podatke (isključen?), čekam da se pojavi")
mic_missing = True
await self.mic.stop()
await asyncio.sleep(2)
await self.mic.start()
await asyncio.sleep(1)
continue
if mic_missing:
log.info("mikrofon ponovo radi")
mic_missing = False
if self.speaker.is_playing():
continue # half-duplex: ne šalji dok Idriz govori
await self.conn.send({"type": "input_audio_buffer.append", "audio": base64.b64encode(chunk).decode()})
@@ -261,7 +270,6 @@ class Assistant:
self.conn = None
async def run(self) -> None:
await self.mic.start()
await self.speaker.start()
if not await printer.printer_ok(self.cfg.printer):
log.warning("printer %s nije spreman", self.cfg.printer)

View File

@@ -2,6 +2,7 @@
import array
import asyncio
import logging
import math
import time
log = logging.getLogger("idriz.audio")
@@ -28,12 +29,33 @@ def apply_gain(pcm: bytes, gain: float) -> bytes:
return samples.tobytes()
def tone(freq: float = 880.0, ms: int = 150, volume: float = 0.3) -> bytes:
"""Kratak signalni ton (PCM16 mono)."""
n = RATE * ms // 1000
samples = array.array("h", (int(volume * 32767 * math.sin(2 * math.pi * freq * i / RATE) * min(1.0, (n - i) / (n * 0.3))) for i in range(n)))
return samples.tobytes()
async def source_present(name: str) -> bool:
"""Da li PipeWire trenutno ima izvor tog imena (USB mikrofon nestane kad se isključi prekidač)."""
if not name:
return True
proc = await asyncio.create_subprocess_exec("pactl", "list", "sources", "short",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL)
out, _ = await proc.communicate()
return any(name in line for line in out.decode(errors="replace").splitlines())
class Mic:
def __init__(self, device: str = "", gain: float = 1.0) -> None:
self.device = device
self.gain = gain
self.proc: asyncio.subprocess.Process | None = None
@property
def running(self) -> bool:
return self.proc is not None and self.proc.returncode is None
async def start(self) -> None:
self.proc = await asyncio.create_subprocess_exec(
*_pacat_args("--record", self.device),
@@ -50,6 +72,7 @@ class Mic:
if self.proc and self.proc.returncode is None:
self.proc.kill()
await self.proc.wait()
self.proc = None
class Speaker: