[全双工依赖接入]:完成真实APM依赖安装和接线,包含aec-audio-processing、音频自测和最终门禁

This commit is contained in:
mkbk
2026-06-19 13:26:25 +08:00
parent 98ae711971
commit caf35783f8
5 changed files with 199 additions and 17 deletions
+150 -2
View File
@@ -4,6 +4,7 @@ import importlib.util
from collections import deque
from dataclasses import dataclass
from threading import RLock
from typing import Any
from typing import Protocol
from .config import AppConfig
@@ -346,15 +347,142 @@ class FakeWebRtcAudioProcessingProvider:
)
class AecAudioProcessingProvider:
name = "aec_audio_processing"
def __init__(
self,
*,
sample_rate: int = 16000,
channels: int = 1,
enable_aec: bool = True,
enable_ns: bool = True,
enable_agc: bool = True,
) -> None:
self.sample_rate = sample_rate
self.channels = channels
self.enable_aec = enable_aec
self.enable_ns = enable_ns
self.enable_agc = enable_agc
self.render_frames: list[AudioFrame] = []
self._processor: Any = None
self._load_processor()
def process_capture(self, frame: AudioFrame) -> AudioFrame:
self._validate_format(frame)
try:
pcm = self._process_pcm_chunks(frame.pcm, self._processor.process_stream)
except Exception as exc:
raise ProviderError(
ErrorCode.AUDIO_APM_PROCESS_FAILED,
f"aec_audio_processing capture failed: {exc}",
True,
self.name,
"audio_apm",
) from exc
metadata = dict(frame.metadata)
metadata["apm_provider"] = self.name
if metadata.get("assistant_echo") and self.render_frames and _rms_int16(pcm) <= _rms_int16(frame.pcm) * 0.85:
metadata["echo_suppressed"] = True
metadata["speech"] = False
return AudioFrame(
pcm,
frame.sample_rate,
frame.channels,
frame.timestamp_ms,
frame.frame_id,
metadata,
)
def process_render(self, frame: AudioFrame) -> None:
self._validate_format(frame)
try:
self._process_pcm_chunks(frame.pcm, self._processor.process_reverse_stream)
except Exception as exc:
raise ProviderError(
ErrorCode.AUDIO_APM_PROCESS_FAILED,
f"aec_audio_processing render reference failed: {exc}",
True,
self.name,
"audio_apm",
) from exc
self.render_frames.append(frame)
def reset_stream(self) -> None:
self.render_frames.clear()
self._load_processor()
def health_check(self) -> AudioProcessingHealth:
return AudioProcessingHealth(
provider=self.name,
available=True,
message="aec_audio_processing native WebRTC APM binding loaded",
)
def _load_processor(self) -> None:
try:
from aec_audio_processing import AudioProcessor # type: ignore[import-not-found]
self._processor = AudioProcessor(
enable_aec=self.enable_aec,
enable_ns=self.enable_ns,
enable_agc=self.enable_agc,
enable_vad=False,
)
self._processor.set_stream_format(self.sample_rate, self.channels, self.sample_rate, self.channels)
self._processor.set_reverse_stream_format(self.sample_rate, self.channels)
except Exception as exc:
raise ProviderError(
ErrorCode.AUDIO_APM_UNAVAILABLE,
f"cannot initialize aec_audio_processing: {exc}",
False,
self.name,
"audio_apm",
) from exc
def _process_pcm_chunks(self, pcm: bytes, processor_method) -> bytes:
frame_bytes = max(2 * self.channels, int(self._processor.get_frame_size()) * self.channels * 2)
frame_bytes -= frame_bytes % (2 * self.channels)
if frame_bytes <= 0:
raise ValueError("invalid APM frame size")
processed = bytearray()
offset = 0
while offset < len(pcm):
chunk = pcm[offset : offset + frame_bytes]
padding = b""
if len(chunk) < frame_bytes:
padding = b"\x00" * (frame_bytes - len(chunk))
chunk += padding
output = processor_method(chunk)
if output is None:
output = chunk
if padding:
output = output[: len(output) - len(padding)]
processed.extend(output)
offset += frame_bytes
return bytes(processed)
def _validate_format(self, frame: AudioFrame) -> None:
if frame.sample_rate != self.sample_rate or frame.channels != self.channels:
raise ProviderError(
ErrorCode.AUDIO_APM_FORMAT_MISMATCH,
"audio frame format does not match aec_audio_processing configuration",
False,
self.name,
"audio_apm",
)
def webrtc_apm_probe() -> AudioProcessingHealth:
candidates = (
"aec_audio_processing",
"webrtc_audio_processing",
"webrtc_audio_processing_module",
)
for module_name in candidates:
if importlib.util.find_spec(module_name) is not None:
return AudioProcessingHealth(
provider="webrtc",
provider=module_name,
available=True,
message=f"found {module_name}",
)
@@ -387,7 +515,15 @@ def build_audio_processing_provider(config: AppConfig) -> AudioProcessingProvide
health = webrtc_apm_probe()
if health.available:
message = "WebRTC APM binding is detected but native processing is not wired yet"
if health.provider == "aec_audio_processing":
return AecAudioProcessingProvider(
sample_rate=config.sample_rate,
channels=config.channels,
enable_aec=config.audio_aec_enabled,
enable_ns=config.audio_ns_enabled,
enable_agc=config.audio_agc_enabled,
)
message = f"{health.provider} binding is detected but native processing is not wired yet"
if config.audio_apm_required:
raise ProviderError(
ErrorCode.AUDIO_APM_UNAVAILABLE,
@@ -409,3 +545,15 @@ def build_audio_processing_provider(config: AppConfig) -> AudioProcessingProvide
fallback_active=True,
message=health.message,
)
def _rms_int16(pcm: bytes) -> float:
if len(pcm) < 2:
return 0.0
samples = [
int.from_bytes(pcm[index : index + 2], "little", signed=True)
for index in range(0, len(pcm) - 1, 2)
]
if not samples:
return 0.0
return (sum(sample * sample for sample in samples) / len(samples)) ** 0.5