From bad8f8aa51178e0cf3d09bdf7b9ae7e34e6f562c Mon Sep 17 00:00:00 2001 From: gui217 Date: Tue, 30 Dec 2025 13:32:36 +0200 Subject: [PATCH 1/3] Fix rnnoise filter to handle empty audio --- src/pipecat/audio/filters/rnnoise_filter.py | 4 ++ tests/test_rnnoise_filter.py | 52 +++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/pipecat/audio/filters/rnnoise_filter.py b/src/pipecat/audio/filters/rnnoise_filter.py index 1262c5a55..ab4c8f87b 100644 --- a/src/pipecat/audio/filters/rnnoise_filter.py +++ b/src/pipecat/audio/filters/rnnoise_filter.py @@ -115,6 +115,10 @@ class RNNoiseFilter(BaseAudioFilter): if self._sample_rate != 48000 and self._resampler_in: in_audio = await self._resampler_in.resample(audio, self._sample_rate, 48000) + # If audio is empty, return empty bytes (no point in noise cancellation) + if len(in_audio) == 0: + return b"" + # Convert bytes to numpy array (int16) audio_samples = np.frombuffer(in_audio, dtype=np.int16) diff --git a/tests/test_rnnoise_filter.py b/tests/test_rnnoise_filter.py index 6a99b3ba0..a448565c6 100644 --- a/tests/test_rnnoise_filter.py +++ b/tests/test_rnnoise_filter.py @@ -5,6 +5,7 @@ # import unittest +from unittest.mock import AsyncMock, MagicMock, patch import numpy as np @@ -100,3 +101,54 @@ class TestRNNoiseFilter(unittest.IsolatedAsyncioTestCase): self.assertGreater(len(result2), 0) await filter.stop() + + async def test_rnnoise_filter_handles_empty_resampler_output(self): + """Test that RNNoise filter handles empty bytes from resampler gracefully. + + This reproduces the issue where mute/silence input (like "bx000") causes + the resampler to return empty bytes. The filter should handle this gracefully + without raising a memory error. + + Currently this test FAILS because the code raises MemoryError when empty bytes + are passed to denoise_chunk. After fixing the code to check for empty bytes + before calling denoise_chunk, this test should PASS. + """ + filter = RNNoiseFilter() + + # Initialize with a sample rate that requires resampling (not 48kHz) + await filter.start(sample_rate=16000) + + # Enable filtering + await filter.process_frame(FilterEnableFrame(enable=True)) + + # Create mute/silence audio chunk (like "bx000" - all zeros) + # This simulates a mute sound that causes resampler to return empty bytes + mute_audio = b"\x00\x00" * 160 # 160 samples of silence at 16kHz (10ms) + + # Mock the resampler to return empty bytes when given mute input + # This simulates the behavior where resampler returns b"" for mute sounds + async def mock_resample(audio, in_rate, out_rate): + # If input is all zeros (mute), return empty bytes + # This simulates the real-world scenario where resampler returns b"" for silence + audio_data = np.frombuffer(audio, dtype=np.int16) + if len(audio_data) == 0 or np.all(audio_data == 0): + return b"" # Return empty bytes - this triggers the bug + # Otherwise, do normal resampling + return audio + + filter._resampler_in.resample = AsyncMock(side_effect=mock_resample) + + # Don't mock denoise_chunk - let it use the real pyrnnoise library + # Currently this will raise MemoryError when empty bytes are passed + # After the fix, this should return empty bytes gracefully + + # The filter should handle empty bytes gracefully without raising an error + # Currently this will FAIL because the code raises MemoryError + # After fixing, this should PASS + result = await filter.filter(mute_audio) + + # When resampler returns empty bytes, filter should return empty bytes + # (or handle it gracefully without crashing) + self.assertEqual(result, b"", "Filter should return empty bytes when resampler returns empty bytes") + + await filter.stop() From ec5fb392c499d7b949bbc28a486e1e19502b0186 Mon Sep 17 00:00:00 2001 From: gui217 Date: Tue, 30 Dec 2025 13:35:13 +0200 Subject: [PATCH 2/3] Clean up test comments after rnnoise filter fix --- tests/test_rnnoise_filter.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/test_rnnoise_filter.py b/tests/test_rnnoise_filter.py index a448565c6..ea6e54a39 100644 --- a/tests/test_rnnoise_filter.py +++ b/tests/test_rnnoise_filter.py @@ -108,10 +108,6 @@ class TestRNNoiseFilter(unittest.IsolatedAsyncioTestCase): This reproduces the issue where mute/silence input (like "bx000") causes the resampler to return empty bytes. The filter should handle this gracefully without raising a memory error. - - Currently this test FAILS because the code raises MemoryError when empty bytes - are passed to denoise_chunk. After fixing the code to check for empty bytes - before calling denoise_chunk, this test should PASS. """ filter = RNNoiseFilter() @@ -137,14 +133,7 @@ class TestRNNoiseFilter(unittest.IsolatedAsyncioTestCase): return audio filter._resampler_in.resample = AsyncMock(side_effect=mock_resample) - - # Don't mock denoise_chunk - let it use the real pyrnnoise library - # Currently this will raise MemoryError when empty bytes are passed - # After the fix, this should return empty bytes gracefully - - # The filter should handle empty bytes gracefully without raising an error - # Currently this will FAIL because the code raises MemoryError - # After fixing, this should PASS + result = await filter.filter(mute_audio) # When resampler returns empty bytes, filter should return empty bytes From 32c3298effe0b57dd90d8657bd54fa14f7eaec6d Mon Sep 17 00:00:00 2001 From: gui217 Date: Tue, 30 Dec 2025 13:39:36 +0200 Subject: [PATCH 3/3] Apply ruff formatting to test file --- tests/test_rnnoise_filter.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/test_rnnoise_filter.py b/tests/test_rnnoise_filter.py index ea6e54a39..ff425f203 100644 --- a/tests/test_rnnoise_filter.py +++ b/tests/test_rnnoise_filter.py @@ -104,23 +104,23 @@ class TestRNNoiseFilter(unittest.IsolatedAsyncioTestCase): async def test_rnnoise_filter_handles_empty_resampler_output(self): """Test that RNNoise filter handles empty bytes from resampler gracefully. - + This reproduces the issue where mute/silence input (like "bx000") causes the resampler to return empty bytes. The filter should handle this gracefully without raising a memory error. """ filter = RNNoiseFilter() - + # Initialize with a sample rate that requires resampling (not 48kHz) await filter.start(sample_rate=16000) - + # Enable filtering await filter.process_frame(FilterEnableFrame(enable=True)) - + # Create mute/silence audio chunk (like "bx000" - all zeros) # This simulates a mute sound that causes resampler to return empty bytes mute_audio = b"\x00\x00" * 160 # 160 samples of silence at 16kHz (10ms) - + # Mock the resampler to return empty bytes when given mute input # This simulates the behavior where resampler returns b"" for mute sounds async def mock_resample(audio, in_rate, out_rate): @@ -131,13 +131,15 @@ class TestRNNoiseFilter(unittest.IsolatedAsyncioTestCase): return b"" # Return empty bytes - this triggers the bug # Otherwise, do normal resampling return audio - + filter._resampler_in.resample = AsyncMock(side_effect=mock_resample) - + result = await filter.filter(mute_audio) - + # When resampler returns empty bytes, filter should return empty bytes # (or handle it gracefully without crashing) - self.assertEqual(result, b"", "Filter should return empty bytes when resampler returns empty bytes") - + self.assertEqual( + result, b"", "Filter should return empty bytes when resampler returns empty bytes" + ) + await filter.stop()