[全双工依赖接入]:完成真实APM依赖安装和接线,包含aec-audio-processing、音频自测和最终门禁
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user