From 1e1e275fea70cc47730c8049353e461263306550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6kmen=20G=C3=B6rgen?= Date: Mon, 19 Jan 2026 15:51:28 +0100 Subject: [PATCH] address feedback. --- src/pipecat/audio/filters/aic_filter.py | 85 +++++++++++++------------ 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/src/pipecat/audio/filters/aic_filter.py b/src/pipecat/audio/filters/aic_filter.py index 14b95c92d..16062c6ea 100644 --- a/src/pipecat/audio/filters/aic_filter.py +++ b/src/pipecat/audio/filters/aic_filter.py @@ -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)