address feedback.
This commit is contained in:
@@ -11,7 +11,7 @@ enhance audio streams in real time. It mirrors the structure of other filters li
|
||||
the Koala filter and integrates with Pipecat's input transport pipeline.
|
||||
|
||||
Classes:
|
||||
AICFilter: For aic-sdk >= 2.0.0 (uses 'aic_sdk' module)
|
||||
AICFilter: For aic-sdk (uses 'aic_sdk' module)
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -30,7 +30,7 @@ class AICFilter(BaseAudioFilter):
|
||||
"""Audio filter using ai-coustics' AIC SDK for real-time enhancement.
|
||||
|
||||
Buffers incoming audio to the model's preferred block size and processes
|
||||
planar frames in-place using float32 samples in the linear -1..+1 range.
|
||||
frames using float32 samples normalized to the -1..+1 range.
|
||||
|
||||
.. note::
|
||||
This class requires aic-sdk >= 2.0.0 (uses 'aic_sdk' module).
|
||||
@@ -84,12 +84,21 @@ class AICFilter(BaseAudioFilter):
|
||||
self._frames_per_block = 0
|
||||
self._audio_buffer = bytearray()
|
||||
|
||||
# Audio format constants
|
||||
self._bytes_per_sample = 2 # int16 = 2 bytes
|
||||
self._dtype = np.int16
|
||||
self._scale = 32768.0 # 2^15, for normalizing int16 (-32768..32767) to float32 (-1.0..1.0)
|
||||
|
||||
# AIC SDK objects
|
||||
self._model = None
|
||||
self._processor = None
|
||||
self._processor_ctx = None
|
||||
self._vad_ctx = None
|
||||
|
||||
# Pre-allocated buffers (resized in start() once frames_per_block is known)
|
||||
self._in_f32 = None
|
||||
self._out_i16 = None
|
||||
|
||||
def get_vad_context(self):
|
||||
"""Return the VAD context once the processor exists.
|
||||
|
||||
@@ -156,6 +165,13 @@ class AICFilter(BaseAudioFilter):
|
||||
logger.debug(f"Model downloaded to: {model_path}")
|
||||
self._model = Model.from_file(model_path)
|
||||
|
||||
# Get optimal frames for this sample rate
|
||||
self._frames_per_block = self._model.get_optimal_num_frames(self._sample_rate)
|
||||
|
||||
# Allocate processing buffers now that we know the block size
|
||||
self._in_f32 = np.zeros((1, self._frames_per_block), dtype=np.float32)
|
||||
self._out_i16 = np.zeros(self._frames_per_block, dtype=np.int16)
|
||||
|
||||
# Create configuration
|
||||
config = ProcessorConfig.optimal(
|
||||
self._model,
|
||||
@@ -163,7 +179,7 @@ class AICFilter(BaseAudioFilter):
|
||||
)
|
||||
|
||||
# Create async processor
|
||||
self._processor = ProcessorAsync(self._model, self._license_key or "", config)
|
||||
self._processor = ProcessorAsync(self._model, self._license_key, config)
|
||||
|
||||
# Get contexts for parameter control and VAD
|
||||
self._processor_ctx = self._processor.get_processor_context()
|
||||
@@ -171,12 +187,10 @@ class AICFilter(BaseAudioFilter):
|
||||
|
||||
# Apply initial parameters
|
||||
if self._enhancement_level is not None:
|
||||
level = float(self._enhancement_level if self._enabled else 0.0)
|
||||
level = self._enhancement_level if self._enabled else 0.0
|
||||
self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level)
|
||||
if self._voice_gain is not None:
|
||||
self._processor_ctx.set_parameter(
|
||||
ProcessorParameter.VoiceGain, float(self._voice_gain)
|
||||
)
|
||||
self._processor_ctx.set_parameter(ProcessorParameter.VoiceGain, self._voice_gain)
|
||||
|
||||
self._aic_ready = True
|
||||
|
||||
@@ -225,7 +239,7 @@ class AICFilter(BaseAudioFilter):
|
||||
self._enabled = frame.enable
|
||||
if self._processor_ctx is not None:
|
||||
try:
|
||||
level = float(self._enhancement_level if self._enabled else 0.0)
|
||||
level = self._enhancement_level if self._enabled else 0.0
|
||||
self._processor_ctx.set_parameter(ProcessorParameter.EnhancementLevel, level)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.error(f"AIC set_parameter failed: {e}")
|
||||
@@ -237,52 +251,41 @@ class AICFilter(BaseAudioFilter):
|
||||
model's required block length. Returns enhanced audio data.
|
||||
|
||||
Args:
|
||||
audio: Raw audio data as bytes to be filtered (int16 PCM, planar).
|
||||
audio: Raw audio data as bytes (int16 PCM).
|
||||
|
||||
Returns:
|
||||
Enhanced audio data as bytes (int16 PCM, planar).
|
||||
Enhanced audio data as bytes (int16 PCM).
|
||||
"""
|
||||
if not self._aic_ready or self._processor is None:
|
||||
return audio
|
||||
|
||||
self._audio_buffer.extend(audio)
|
||||
available_frames = len(self._audio_buffer) // self._bytes_per_sample
|
||||
num_blocks = available_frames // self._frames_per_block
|
||||
|
||||
if num_blocks == 0:
|
||||
return b""
|
||||
|
||||
filtered_chunks: List[bytes] = []
|
||||
mv = memoryview(self._audio_buffer)
|
||||
block_size = self._frames_per_block * self._bytes_per_sample
|
||||
|
||||
# Number of int16 samples currently buffered
|
||||
available_frames = len(self._audio_buffer) // 2
|
||||
for i in range(num_blocks):
|
||||
start = i * block_size
|
||||
block_i16 = np.frombuffer(mv[start : start + block_size], dtype=self._dtype)
|
||||
|
||||
while available_frames >= self._frames_per_block:
|
||||
# Consume exactly one block worth of frames
|
||||
samples_to_consume = self._frames_per_block * 1
|
||||
bytes_to_consume = samples_to_consume * 2
|
||||
block_bytes = bytes(self._audio_buffer[:bytes_to_consume])
|
||||
# Reuse input buffer, in-place divide
|
||||
np.copyto(self._in_f32[0], block_i16)
|
||||
self._in_f32 /= self._scale
|
||||
|
||||
# Convert to float32 in -1..+1 range and reshape to (channels, frames)
|
||||
block_i16 = np.frombuffer(block_bytes, dtype=np.int16)
|
||||
# Convert to float32 and normalize
|
||||
block_f32 = block_i16.astype(np.float32)
|
||||
|
||||
block_f32 *= (1.0 / 32768.0)
|
||||
|
||||
# Reshape to (1, frames) for AIC SDK
|
||||
block_f32 = block_f32.reshape((1, self._frames_per_block))
|
||||
out_f32 = await self._processor.process_async(self._in_f32)
|
||||
|
||||
# Process via async processor; returns ndarray (same shape)
|
||||
out_f32 = await self._processor.process_async(block_f32)
|
||||
# Convert float32 output back to int16
|
||||
np.multiply(out_f32, self._scale, out=self._in_f32) # reuse in_f32 as temp
|
||||
np.clip(self._in_f32, -self._scale, self._scale - 1, out=self._in_f32)
|
||||
np.copyto(self._out_i16, self._in_f32[0].astype(self._dtype))
|
||||
|
||||
# Convert back to int16 bytes
|
||||
# Denormalize and convert back to int16
|
||||
out_f32 *= 32768.0
|
||||
|
||||
# In-place clip to valid int16 range (-32768 to 32767)
|
||||
np.clip(out_f32, -32768.0, 32767, out=out_f32)
|
||||
out_i16 = out_f32.astype(dtype)
|
||||
filtered_chunks.append(out_i16.reshape(-1).tobytes())
|
||||
filtered_chunks.append(self._out_i16.tobytes())
|
||||
|
||||
# Slide buffer
|
||||
self._audio_buffer = self._audio_buffer[bytes_to_consume:]
|
||||
available_frames = len(self._audio_buffer) // 2
|
||||
|
||||
# Do not flush incomplete frames; keep them buffered for the next call
|
||||
self._audio_buffer = self._audio_buffer[num_blocks * block_size :]
|
||||
return b"".join(filtered_chunks)
|
||||
|
||||
Reference in New Issue
Block a user