"""AMR/任意音频 -> 8kHz 单声道 PCM s16le 转码。 使用系统 ffmpeg(Dockerfile 已 apt-get install ffmpeg)。 """ from __future__ import annotations import asyncio import logging from typing import Optional logger = logging.getLogger(__name__) async def decode_to_pcm_s16le( audio_bytes: bytes, *, duration_sec: int = 3, sample_rate: int = 8000, channels: int = 1, mime: str = "", ) -> bytes: """把任意音频(amr/mp3/wav/m4a 等)转成 8kHz 单声道 PCM s16le。 Args: audio_bytes: 输入音频字节 duration_sec: 截取时长(秒),默认 3 sample_rate: 采样率,默认 8000(网易云 shazam_v2 要求) channels: 声道数,默认 1 mime: MIME 类型。若为 audio/pcm 则视为已是 PCM s16le 直接截断返回 Returns: PCM s16le 字节流 Raises: RuntimeError: ffmpeg 失败 """ # 裸 PCM 直通(便于测试 / 手表若直接录 PCM 也支持) if mime in ("audio/pcm", "audio/x-pcm", "application/pcm"): needed = sample_rate * channels * 2 * duration_sec return audio_bytes[:needed] args = [ "ffmpeg", "-i", "pipe:0", "-f", "s16le", "-acodec", "pcm_s16le", "-ar", str(sample_rate), "-ac", str(channels), "-t", str(duration_sec), "pipe:1", ] logger.info("[amr-decoder] ffmpeg start, input=%d bytes mime=%s", len(audio_bytes), mime) proc = await asyncio.create_subprocess_exec( *args, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await proc.communicate(input=audio_bytes) if proc.returncode != 0: err = stderr.decode("utf-8", errors="replace")[-500:] raise RuntimeError(f"ffmpeg exit {proc.returncode}: {err}") logger.info("[amr-decoder] ffmpeg ok, pcm=%d bytes", len(stdout)) return stdout