From 4cf210e50398f23631d7da7db7c6159edefa8456 Mon Sep 17 00:00:00 2001 From: Fiber <48828021+leafliber@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:33:36 +0800 Subject: [PATCH] fix: resample and downmix WAV files for Tencent Silk encoding (#9100) * fix: resample and downmix WAV files for Tencent Silk encoding * fix: improve WAV to Tencent Silk conversion by handling sample width and resampling --- astrbot/core/utils/tencent_record_helper.py | 21 +++- tests/test_media_utils.py | 108 ++++++++++++++++++-- 2 files changed, 120 insertions(+), 9 deletions(-) diff --git a/astrbot/core/utils/tencent_record_helper.py b/astrbot/core/utils/tencent_record_helper.py index 493496842..600c450d1 100644 --- a/astrbot/core/utils/tencent_record_helper.py +++ b/astrbot/core/utils/tencent_record_helper.py @@ -1,6 +1,7 @@ """Tencent Silk audio conversion helpers.""" import asyncio +import audioop import os import subprocess import wave @@ -8,6 +9,9 @@ from io import BytesIO from astrbot.core import logger +# The SILK SDK only supports these rates +_PYSILK_SUPPORTED_RATES = frozenset({8000, 12000, 16000, 24000, 32000, 48000}) + async def tencent_silk_to_wav(silk_path: str, output_path: str) -> str: """Decode a Tencent Silk file to 24 kHz mono PCM WAV. @@ -69,8 +73,19 @@ async def wav_to_tencent_silk(wav_path: str, output_path: str) -> float: with wave.open(wav_path, "rb") as wav: rate = wav.getframerate() - frames = wav.getnframes() - pcm_data = wav.readframes(frames) + channels = wav.getnchannels() + sampwidth = wav.getsampwidth() + pcm_data = wav.readframes(wav.getnframes()) + + # Downmix to mono, resample to 24 kHz if needed, and convert to 16-bit PCM + # (pysilk only accepts 16-bit linear PCM) + if channels == 2: + pcm_data = audioop.tomono(pcm_data, sampwidth, 0.5, 0.5) + if rate not in _PYSILK_SUPPORTED_RATES: + pcm_data, _ = audioop.ratecv(pcm_data, sampwidth, 1, rate, 24000, None) + rate = 24000 + if sampwidth != 2: + pcm_data = audioop.lin2lin(pcm_data, sampwidth, 2) input_io = BytesIO(pcm_data) output_io = BytesIO() @@ -78,7 +93,7 @@ async def wav_to_tencent_silk(wav_path: str, output_path: str) -> float: pysilk.encode(input_io, output_io, rate, rate, tencent=True) with open(output_path, "wb") as f: f.write(output_io.getvalue()) - return frames / rate if rate else 0 + return len(pcm_data) / (2 * rate) if rate else 0 async def convert_to_pcm_wav(input_path: str, output_path: str) -> str: diff --git a/tests/test_media_utils.py b/tests/test_media_utils.py index 6edfcd4d7..2b6c2dd38 100644 --- a/tests/test_media_utils.py +++ b/tests/test_media_utils.py @@ -2,6 +2,7 @@ import base64 import math import os import struct +import sys import wave from io import BytesIO from pathlib import Path @@ -653,19 +654,39 @@ def test_path_mapping_accepts_standard_and_legacy_file_uri(tmp_path): @pytest.mark.asyncio -async def test_tencent_silk_encoding_uses_pysilk_tencent_format(tmp_path, monkeypatch): +@pytest.mark.parametrize( + "rate, channels", + [ + (24000, 1), # supported, no resample + (44100, 1), # unsupported rate, triggers resample + (22050, 1), # unsupported rate, triggers resample + (48000, 2), # stereo at supported rate, triggers downmix + (44100, 2), # stereo + unsupported rate, triggers both + ], + ids=["24k-mono", "44.1k-mono", "22.05k-mono", "48k-stereo", "44.1k-stereo"], +) +async def test_tencent_silk_encoding_uses_pysilk_tencent_format( + rate, channels, tmp_path, monkeypatch +): + """Real pysilk end-to-end across sample rates that previously failed. + + 44100 Hz was the regression trigger: pysilk rejects it with + ENC_INPUT_INVALID_NO_OF_SAMPLES. The fix resamples to 24 kHz mono via + audioop.ratecv before encoding. + """ monkeypatch.setattr(media_utils, "get_astrbot_temp_path", lambda: str(tmp_path)) wav_path = tmp_path / "tone.wav" silk_path = tmp_path / "tone.silk" - rate = 24000 - frames = int(rate * 0.2) + secs = 0.2 + frames = int(rate * secs) with wave.open(str(wav_path), "wb") as wav: - wav.setnchannels(1) + wav.setnchannels(channels) wav.setsampwidth(2) wav.setframerate(rate) for i in range(frames): sample = int(0.2 * 32767 * math.sin(2 * math.pi * 440 * i / rate)) - wav.writeframesraw(struct.pack("