left0ver's picture
Publish com.left0ver.agentcade 0.1.0
ad88834 verified
Raw History Blame Contribute Delete
3.75 kB
import asyncio
import time
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from fractions import Fraction
from av import AudioFrame
from av.filter import Graph
from watcherobot.audio import PCMAudio
from watcherobot.robot import AudioDomain
PlaybackCallback = Callable[[float], Awaitable[None]]
@dataclass(frozen=True)
class CapturedPcm:
data: bytes
captured_at: float
muted: bool = False
class PcmInputGain:
def __init__(self, gain):
if not 1 <= gain <= 8:
raise ValueError("麦克风输入增益必须在 1 至 8 之间")
self.graph = Graph()
source = self.graph.add_abuffer(sample_rate=16000, format="s16", layout="mono")
volume = self.graph.add("volume", f"volume={gain}:precision=fixed")
sink = self.graph.add("abuffersink")
source.link_to(volume)
volume.link_to(sink)
self.graph.configure()
self.samples = 0
def process(self, pcm):
if not pcm or len(pcm) % 2:
raise ValueError("输入增益处理需要完整的 PCM 采样")
frame = AudioFrame(format="s16", layout="mono", samples=len(pcm) // 2)
frame.sample_rate = 16000
frame.time_base = Fraction(1, 16000)
frame.pts = self.samples
frame.planes[0].update(pcm)
self.samples += frame.samples
self.graph.push(frame)
output = self.graph.pull()
if output.samples != frame.samples or output.format.name != "s16":
raise ValueError("输入增益处理改变了 PCM 格式或采样数量")
return bytes(output.planes[0])[:output.samples * 2]
@dataclass(frozen=True)
class PlaybackMetrics:
pcm_bytes: int
playback_id: int
started_seconds: float
completed_seconds: float
async def sdk_command(function, *args, **kwargs):
# SDK 指令在工作线程等待确认,取消时也要等待该指令结束。
task = asyncio.create_task(asyncio.to_thread(function, *args, **kwargs))
try:
return await asyncio.shield(task)
except asyncio.CancelledError:
await task
raise
class PcmAudioPlayer:
def __init__(self, audio: AudioDomain):
self.audio = audio
self.playing = False
async def play(
self, pcm: bytes, *, on_start: PlaybackCallback | None = None,
) -> PlaybackMetrics:
if self.playing:
raise RuntimeError("请按顺序提交语音播报")
source = PCMAudio(pcm)
self.playing = True
playback = None
epoch = time.monotonic()
try:
opening = asyncio.create_task(asyncio.to_thread(self.audio.play_pcm, source.data))
try:
playback = await asyncio.shield(opening)
except asyncio.CancelledError:
# 取得已启动的播放任务,确保取消期间仍能停止设备播放。
playback = await opening
raise
started = time.monotonic() - epoch
async with asyncio.timeout(playback.expected_duration_seconds + 15):
if on_start is not None:
await on_start(playback.expected_duration_seconds)
while not playback.state.terminal:
await asyncio.sleep(0.02)
playback.wait(0)
return PlaybackMetrics(
pcm_bytes=len(source.data), playback_id=playback.id,
started_seconds=started, completed_seconds=time.monotonic() - epoch,
)
finally:
try:
if playback is not None and not playback.state.terminal:
await sdk_command(playback.cancel)
finally:
self.playing = False