pipecat-resampler 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pipecat_resampler-0.1.0/LICENSE +24 -0
- pipecat_resampler-0.1.0/MANIFEST.in +3 -0
- pipecat_resampler-0.1.0/PKG-INFO +131 -0
- pipecat_resampler-0.1.0/README.md +113 -0
- pipecat_resampler-0.1.0/pyproject.toml +59 -0
- pipecat_resampler-0.1.0/scripts/bench.py +178 -0
- pipecat_resampler-0.1.0/setup.cfg +4 -0
- pipecat_resampler-0.1.0/setup.py +32 -0
- pipecat_resampler-0.1.0/src/pipecat_resampler/__init__.py +11 -0
- pipecat_resampler-0.1.0/src/pipecat_resampler/_kernel.c +515 -0
- pipecat_resampler-0.1.0/src/pipecat_resampler/_kernel.h +62 -0
- pipecat_resampler-0.1.0/src/pipecat_resampler/_resampler.c +225 -0
- pipecat_resampler-0.1.0/src/pipecat_resampler/_resampler.pyi +27 -0
- pipecat_resampler-0.1.0/src/pipecat_resampler/py.typed +0 -0
- pipecat_resampler-0.1.0/src/pipecat_resampler.egg-info/PKG-INFO +131 -0
- pipecat_resampler-0.1.0/src/pipecat_resampler.egg-info/SOURCES.txt +17 -0
- pipecat_resampler-0.1.0/src/pipecat_resampler.egg-info/dependency_links.txt +1 -0
- pipecat_resampler-0.1.0/src/pipecat_resampler.egg-info/top_level.txt +1 -0
- pipecat_resampler-0.1.0/tests/test_resampler.py +281 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Daily
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pipecat-resampler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Streaming audio resampler whose output keeps pace with its input
|
|
5
|
+
License-Expression: BSD-2-Clause
|
|
6
|
+
Project-URL: Source, https://github.com/pipecat-ai/pipecat-resampler
|
|
7
|
+
Project-URL: Issues, https://github.com/pipecat-ai/pipecat-resampler/issues
|
|
8
|
+
Keywords: audio,resampling,real-time,voice
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: C
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# pipecat-resampler
|
|
20
|
+
|
|
21
|
+
A small C extension for Python that converts 16-bit PCM audio, mono or with interleaved channels, from one sample rate to another, chunk by chunk. It's made for real-time audio, such as voice pipelines built with [Pipecat](https://github.com/pipecat-ai/pipecat). Equal chunks in give equal chunks out: 40 ms of audio in always gives 40 ms out, the first chunk included.
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from pipecat_resampler import Resampler
|
|
25
|
+
|
|
26
|
+
resampler = Resampler(24000, 16000)
|
|
27
|
+
chunk_16k = resampler.process(chunk_24k) # bytes in, bytes out
|
|
28
|
+
tail = resampler.flush() # the audio the filter is delaying
|
|
29
|
+
|
|
30
|
+
stereo = Resampler(48000, 16000, channels=2)
|
|
31
|
+
frames_16k = stereo.process(frames_48k) # interleaved frames in and out
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Platforms
|
|
35
|
+
|
|
36
|
+
The extension uses Python's stable ABI, so one wheel per platform covers Python 3.11 and later.
|
|
37
|
+
|
|
38
|
+
| Platform | Wheels |
|
|
39
|
+
|---|---|
|
|
40
|
+
| Linux x86-64 and ARM64 | manylinux (glibc 2.17 or later) and musllinux (Alpine 3.13 or later) |
|
|
41
|
+
| macOS | x86-64 and ARM64 |
|
|
42
|
+
| Windows | x86-64 |
|
|
43
|
+
|
|
44
|
+
## Why it exists
|
|
45
|
+
|
|
46
|
+
A real-time voice pipeline has to resample whenever its parts run at different rates. WebRTC audio arrives at 48 kHz while a speech recognizer might want 16 kHz, or a TTS voice that only comes at 24 kHz has to go out on an 8 kHz phone call. When that happens, the conversion runs chunk by chunk, every 10 to 40 ms, for every stream the process is handling.
|
|
47
|
+
|
|
48
|
+
We wanted a resampler built for that:
|
|
49
|
+
|
|
50
|
+
- **Sinc quality.** A band-limited polyphase sinc filter, flat to 90% of the band, with about 90 dB of aliasing rejection.
|
|
51
|
+
- **Fast.** C with SIMD, so one process can carry many streams at once.
|
|
52
|
+
- **Small.** About 0.1 MB installed, with no runtime dependencies.
|
|
53
|
+
- **Steady output.** The same input size gives the same output size on every call, so nothing downstream has to absorb bursts of audio or wait for audio that's held back.
|
|
54
|
+
- **Low latency.** A fixed delay of a few milliseconds.
|
|
55
|
+
- **Easy to ship.** BSD 2-Clause licensed, like Pipecat, with wheels for Linux (glibc 2.17 or later, and Alpine), macOS and Windows.
|
|
56
|
+
|
|
57
|
+
Each library we tried covers part of this list, and none covered all of it.
|
|
58
|
+
|
|
59
|
+
## Comparison
|
|
60
|
+
|
|
61
|
+
Each resampler converted 24 kHz noise to 16 kHz in 40 ms chunks, called from Python, on an Intel i9-10920X. Aliasing is how loud a 10 kHz tone comes out after it folds down to 6 kHz; lower is better.
|
|
62
|
+
|
|
63
|
+
| Resampler | Samples per 40 ms chunk (640 expected) | Aliasing | CPU per chunk | Install size |
|
|
64
|
+
|---|---|---|---|---|
|
|
65
|
+
| soxr VHQ | 0 to 1100 | -93 dB | 13.5 µs | 0.5 MB |
|
|
66
|
+
| soxr QQ | 639 to 640 | -3 dB | 6.5 µs | 0.5 MB |
|
|
67
|
+
| samplerate (libsamplerate) `sinc_medium` | 640, first chunk 594 | -100 dB | 198 µs | 1.8 MB |
|
|
68
|
+
| PyAV (FFmpeg) | 640, first chunk 624 | -88 dB | 33.7 µs | 127 MB |
|
|
69
|
+
| Speex resampler, quality 7 | 640 | -100 dB | 26.9 µs | no PyPI binding |
|
|
70
|
+
| **pipecat-resampler** | 640 | -98 dB | 9.1 µs | 0.1 MB |
|
|
71
|
+
|
|
72
|
+
How each one fits this use:
|
|
73
|
+
|
|
74
|
+
- **soxr** reaches its quality cheaply by filtering with FFTs over large blocks. Fed small chunks, that means its output arrives in bursts. Its quick setting, QQ, is steady and the fastest option here; it uses cubic interpolation instead of a long filter, so content above the new rate folds back into the speech. soxr is licensed under the LGPL 2.1, and its Linux wheels need glibc 2.27 or later (2.26 on ARM), with none for Alpine.
|
|
75
|
+
- **samplerate** (libsamplerate) is steady after its first chunk and very clean. Its sinc converters compute the filter for each output sample, which handles any ratio and costs about 15 times soxr's CPU here.
|
|
76
|
+
- **PyAV** wraps FFmpeg's resampler, which is steady after its first chunk and fast. It brings FFmpeg along, 127 MB installed, and each call from Python costs about 30 µs whatever the chunk size.
|
|
77
|
+
- **Speex** has a steady, clean resampler. It has no Python binding on PyPI (the `speexdsp` package covers its echo canceller), so we measured the system library through ctypes. It uses about twice soxr's CPU here, and more at 44.1 kHz, where it computes the filter on the fly.
|
|
78
|
+
|
|
79
|
+
## Performance
|
|
80
|
+
|
|
81
|
+
Called from Python, in microseconds per chunk, from the same run as the comparison; lower is better.
|
|
82
|
+
|
|
83
|
+
At 24 → 16 kHz, by chunk size:
|
|
84
|
+
|
|
85
|
+
| Resampler | 10 ms | 20 ms | 40 ms |
|
|
86
|
+
|---|---|---|---|
|
|
87
|
+
| **pipecat-resampler** | 2.4 | 4.7 | 9.1 |
|
|
88
|
+
| soxr VHQ | 5.7 | 8.1 | 13.5 |
|
|
89
|
+
| soxr QQ | 3.3 | 4.6 | 6.5 |
|
|
90
|
+
| samplerate `sinc_medium` | 55.0 | 103.0 | 198.1 |
|
|
91
|
+
| PyAV | 29.0 | 31.3 | 33.7 |
|
|
92
|
+
| Speex quality 7 | 9.2 | 14.9 | 26.9 |
|
|
93
|
+
|
|
94
|
+
At 40 ms chunks, the size Pipecat's output transport sends, by rate pair:
|
|
95
|
+
|
|
96
|
+
| Resampler | 24 → 16 kHz | 24 → 8 kHz | 16 → 8 kHz | 8 → 16 kHz | 48 → 16 kHz | 44.1 → 16 kHz |
|
|
97
|
+
|---|---|---|---|---|---|---|
|
|
98
|
+
| **pipecat-resampler** | 9.2 | 8.6 | 6.1 | 7.9 | 17.0 | 17.0 |
|
|
99
|
+
| soxr VHQ | 13.8 | 9.2 | 6.1 | 6.9 | 15.1 | 18.0 |
|
|
100
|
+
| soxr QQ | 7.0 | 4.8 | 4.6 | 6.9 | 7.2 | 7.1 |
|
|
101
|
+
| samplerate `sinc_medium` | 198.8 | 184.0 | 127.8 | 145.1 | 357.0 | 388.8 |
|
|
102
|
+
| PyAV | 33.5 | 32.8 | 31.8 | 31.2 | 36.0 | 35.6 |
|
|
103
|
+
| Speex quality 7 | 27.4 | 29.7 | 19.6 | 19.0 | 55.5 | 217.3 |
|
|
104
|
+
|
|
105
|
+
At 24 → 16 kHz, pipecat-resampler is the fastest of the filtered options at every chunk size, and its lead grows as chunks get smaller, because each call costs less. With 40 ms chunks, soxr VHQ is a little faster on 8 → 16 and 48 → 16 kHz, where its multistage FFT design is very cheap. Even pipecat-resampler's slowest case here, 17 µs per 40 ms chunk, is under 0.05% of one core per stream.
|
|
106
|
+
|
|
107
|
+
Run `uv run python scripts/bench.py` to measure on your machine. It times soxr, and also samplerate, PyAV and the Speex library when they're installed.
|
|
108
|
+
|
|
109
|
+
## How it works
|
|
110
|
+
|
|
111
|
+
It's a polyphase Kaiser-windowed sinc filter, the design WebRTC and Speex use. The filter keeps the lower rate's band flat up to 90% of its Nyquist frequency and attenuates aliasing and images by about 90 dB.
|
|
112
|
+
|
|
113
|
+
Each call works out how many output samples its input is worth and carries any fraction of a sample into the next call. That's what keeps the output steady, for any pair of rates and any chunk size. Instead of holding output back, the filter delays the audio by a few milliseconds; `flush()` returns that delayed end, and `reset()` discards it.
|
|
114
|
+
|
|
115
|
+
With several channels, the audio is interleaved frames, and each channel is filtered on its own with the same filter. The cost grows with the channel count, so stereo costs twice what mono does.
|
|
116
|
+
|
|
117
|
+
The inner loop picks the fastest kernel the CPU supports when a resampler is created: AVX2 with FMA or SSE2 on x86-64, NEON on ARM64, and plain C elsewhere. Filter phases with symmetric taps add mirrored samples before multiplying, which halves the multiplications. Set `PIPECAT_RESAMPLER_KERNEL` to `generic`, `sse2`, `avx2` or `neon` to force a kernel.
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
uv sync
|
|
123
|
+
uv run pytest
|
|
124
|
+
uv run python scripts/bench.py
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Wheels are built and tested with [cibuildwheel](https://cibuildwheel.pypa.io) on every pull request and every push to `main`.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
pipecat-resampler is released under the [BSD 2-Clause License](LICENSE).
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# pipecat-resampler
|
|
2
|
+
|
|
3
|
+
A small C extension for Python that converts 16-bit PCM audio, mono or with interleaved channels, from one sample rate to another, chunk by chunk. It's made for real-time audio, such as voice pipelines built with [Pipecat](https://github.com/pipecat-ai/pipecat). Equal chunks in give equal chunks out: 40 ms of audio in always gives 40 ms out, the first chunk included.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from pipecat_resampler import Resampler
|
|
7
|
+
|
|
8
|
+
resampler = Resampler(24000, 16000)
|
|
9
|
+
chunk_16k = resampler.process(chunk_24k) # bytes in, bytes out
|
|
10
|
+
tail = resampler.flush() # the audio the filter is delaying
|
|
11
|
+
|
|
12
|
+
stereo = Resampler(48000, 16000, channels=2)
|
|
13
|
+
frames_16k = stereo.process(frames_48k) # interleaved frames in and out
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Platforms
|
|
17
|
+
|
|
18
|
+
The extension uses Python's stable ABI, so one wheel per platform covers Python 3.11 and later.
|
|
19
|
+
|
|
20
|
+
| Platform | Wheels |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Linux x86-64 and ARM64 | manylinux (glibc 2.17 or later) and musllinux (Alpine 3.13 or later) |
|
|
23
|
+
| macOS | x86-64 and ARM64 |
|
|
24
|
+
| Windows | x86-64 |
|
|
25
|
+
|
|
26
|
+
## Why it exists
|
|
27
|
+
|
|
28
|
+
A real-time voice pipeline has to resample whenever its parts run at different rates. WebRTC audio arrives at 48 kHz while a speech recognizer might want 16 kHz, or a TTS voice that only comes at 24 kHz has to go out on an 8 kHz phone call. When that happens, the conversion runs chunk by chunk, every 10 to 40 ms, for every stream the process is handling.
|
|
29
|
+
|
|
30
|
+
We wanted a resampler built for that:
|
|
31
|
+
|
|
32
|
+
- **Sinc quality.** A band-limited polyphase sinc filter, flat to 90% of the band, with about 90 dB of aliasing rejection.
|
|
33
|
+
- **Fast.** C with SIMD, so one process can carry many streams at once.
|
|
34
|
+
- **Small.** About 0.1 MB installed, with no runtime dependencies.
|
|
35
|
+
- **Steady output.** The same input size gives the same output size on every call, so nothing downstream has to absorb bursts of audio or wait for audio that's held back.
|
|
36
|
+
- **Low latency.** A fixed delay of a few milliseconds.
|
|
37
|
+
- **Easy to ship.** BSD 2-Clause licensed, like Pipecat, with wheels for Linux (glibc 2.17 or later, and Alpine), macOS and Windows.
|
|
38
|
+
|
|
39
|
+
Each library we tried covers part of this list, and none covered all of it.
|
|
40
|
+
|
|
41
|
+
## Comparison
|
|
42
|
+
|
|
43
|
+
Each resampler converted 24 kHz noise to 16 kHz in 40 ms chunks, called from Python, on an Intel i9-10920X. Aliasing is how loud a 10 kHz tone comes out after it folds down to 6 kHz; lower is better.
|
|
44
|
+
|
|
45
|
+
| Resampler | Samples per 40 ms chunk (640 expected) | Aliasing | CPU per chunk | Install size |
|
|
46
|
+
|---|---|---|---|---|
|
|
47
|
+
| soxr VHQ | 0 to 1100 | -93 dB | 13.5 µs | 0.5 MB |
|
|
48
|
+
| soxr QQ | 639 to 640 | -3 dB | 6.5 µs | 0.5 MB |
|
|
49
|
+
| samplerate (libsamplerate) `sinc_medium` | 640, first chunk 594 | -100 dB | 198 µs | 1.8 MB |
|
|
50
|
+
| PyAV (FFmpeg) | 640, first chunk 624 | -88 dB | 33.7 µs | 127 MB |
|
|
51
|
+
| Speex resampler, quality 7 | 640 | -100 dB | 26.9 µs | no PyPI binding |
|
|
52
|
+
| **pipecat-resampler** | 640 | -98 dB | 9.1 µs | 0.1 MB |
|
|
53
|
+
|
|
54
|
+
How each one fits this use:
|
|
55
|
+
|
|
56
|
+
- **soxr** reaches its quality cheaply by filtering with FFTs over large blocks. Fed small chunks, that means its output arrives in bursts. Its quick setting, QQ, is steady and the fastest option here; it uses cubic interpolation instead of a long filter, so content above the new rate folds back into the speech. soxr is licensed under the LGPL 2.1, and its Linux wheels need glibc 2.27 or later (2.26 on ARM), with none for Alpine.
|
|
57
|
+
- **samplerate** (libsamplerate) is steady after its first chunk and very clean. Its sinc converters compute the filter for each output sample, which handles any ratio and costs about 15 times soxr's CPU here.
|
|
58
|
+
- **PyAV** wraps FFmpeg's resampler, which is steady after its first chunk and fast. It brings FFmpeg along, 127 MB installed, and each call from Python costs about 30 µs whatever the chunk size.
|
|
59
|
+
- **Speex** has a steady, clean resampler. It has no Python binding on PyPI (the `speexdsp` package covers its echo canceller), so we measured the system library through ctypes. It uses about twice soxr's CPU here, and more at 44.1 kHz, where it computes the filter on the fly.
|
|
60
|
+
|
|
61
|
+
## Performance
|
|
62
|
+
|
|
63
|
+
Called from Python, in microseconds per chunk, from the same run as the comparison; lower is better.
|
|
64
|
+
|
|
65
|
+
At 24 → 16 kHz, by chunk size:
|
|
66
|
+
|
|
67
|
+
| Resampler | 10 ms | 20 ms | 40 ms |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| **pipecat-resampler** | 2.4 | 4.7 | 9.1 |
|
|
70
|
+
| soxr VHQ | 5.7 | 8.1 | 13.5 |
|
|
71
|
+
| soxr QQ | 3.3 | 4.6 | 6.5 |
|
|
72
|
+
| samplerate `sinc_medium` | 55.0 | 103.0 | 198.1 |
|
|
73
|
+
| PyAV | 29.0 | 31.3 | 33.7 |
|
|
74
|
+
| Speex quality 7 | 9.2 | 14.9 | 26.9 |
|
|
75
|
+
|
|
76
|
+
At 40 ms chunks, the size Pipecat's output transport sends, by rate pair:
|
|
77
|
+
|
|
78
|
+
| Resampler | 24 → 16 kHz | 24 → 8 kHz | 16 → 8 kHz | 8 → 16 kHz | 48 → 16 kHz | 44.1 → 16 kHz |
|
|
79
|
+
|---|---|---|---|---|---|---|
|
|
80
|
+
| **pipecat-resampler** | 9.2 | 8.6 | 6.1 | 7.9 | 17.0 | 17.0 |
|
|
81
|
+
| soxr VHQ | 13.8 | 9.2 | 6.1 | 6.9 | 15.1 | 18.0 |
|
|
82
|
+
| soxr QQ | 7.0 | 4.8 | 4.6 | 6.9 | 7.2 | 7.1 |
|
|
83
|
+
| samplerate `sinc_medium` | 198.8 | 184.0 | 127.8 | 145.1 | 357.0 | 388.8 |
|
|
84
|
+
| PyAV | 33.5 | 32.8 | 31.8 | 31.2 | 36.0 | 35.6 |
|
|
85
|
+
| Speex quality 7 | 27.4 | 29.7 | 19.6 | 19.0 | 55.5 | 217.3 |
|
|
86
|
+
|
|
87
|
+
At 24 → 16 kHz, pipecat-resampler is the fastest of the filtered options at every chunk size, and its lead grows as chunks get smaller, because each call costs less. With 40 ms chunks, soxr VHQ is a little faster on 8 → 16 and 48 → 16 kHz, where its multistage FFT design is very cheap. Even pipecat-resampler's slowest case here, 17 µs per 40 ms chunk, is under 0.05% of one core per stream.
|
|
88
|
+
|
|
89
|
+
Run `uv run python scripts/bench.py` to measure on your machine. It times soxr, and also samplerate, PyAV and the Speex library when they're installed.
|
|
90
|
+
|
|
91
|
+
## How it works
|
|
92
|
+
|
|
93
|
+
It's a polyphase Kaiser-windowed sinc filter, the design WebRTC and Speex use. The filter keeps the lower rate's band flat up to 90% of its Nyquist frequency and attenuates aliasing and images by about 90 dB.
|
|
94
|
+
|
|
95
|
+
Each call works out how many output samples its input is worth and carries any fraction of a sample into the next call. That's what keeps the output steady, for any pair of rates and any chunk size. Instead of holding output back, the filter delays the audio by a few milliseconds; `flush()` returns that delayed end, and `reset()` discards it.
|
|
96
|
+
|
|
97
|
+
With several channels, the audio is interleaved frames, and each channel is filtered on its own with the same filter. The cost grows with the channel count, so stereo costs twice what mono does.
|
|
98
|
+
|
|
99
|
+
The inner loop picks the fastest kernel the CPU supports when a resampler is created: AVX2 with FMA or SSE2 on x86-64, NEON on ARM64, and plain C elsewhere. Filter phases with symmetric taps add mirrored samples before multiplying, which halves the multiplications. Set `PIPECAT_RESAMPLER_KERNEL` to `generic`, `sse2`, `avx2` or `neon` to force a kernel.
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
uv sync
|
|
105
|
+
uv run pytest
|
|
106
|
+
uv run python scripts/bench.py
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Wheels are built and tested with [cibuildwheel](https://cibuildwheel.pypa.io) on every pull request and every push to `main`.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
pipecat-resampler is released under the [BSD 2-Clause License](LICENSE).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pipecat-resampler"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Streaming audio resampler whose output keeps pace with its input"
|
|
9
|
+
license = "BSD-2-Clause"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
keywords = ["audio", "resampling", "real-time", "voice"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: C",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Multimedia :: Sound/Audio :: Conversion",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Source = "https://github.com/pipecat-ai/pipecat-resampler"
|
|
25
|
+
Issues = "https://github.com/pipecat-ai/pipecat-resampler/issues"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = ["pytest>=8", "numpy>=1.26", "soxr>=1.0"]
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
pipecat_resampler = ["py.typed", "*.pyi"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
testpaths = ["tests"]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 100
|
|
41
|
+
|
|
42
|
+
[tool.cibuildwheel]
|
|
43
|
+
# The extension uses the stable ABI, so a cp311 wheel serves every later Python.
|
|
44
|
+
build = "cp311-*"
|
|
45
|
+
skip = "*-win32 *-manylinux_i686 *-musllinux_i686"
|
|
46
|
+
test-requires = ["pytest", "numpy"]
|
|
47
|
+
test-command = "pytest {project}/tests"
|
|
48
|
+
|
|
49
|
+
[tool.cibuildwheel.macos]
|
|
50
|
+
archs = ["x86_64", "arm64"]
|
|
51
|
+
|
|
52
|
+
# Tag the glibc wheels for the oldest glibc they run on, 2.17, and nothing newer.
|
|
53
|
+
[[tool.cibuildwheel.overrides]]
|
|
54
|
+
select = "*-manylinux_x86_64"
|
|
55
|
+
repair-wheel-command = "auditwheel repair --plat manylinux_2_17_x86_64 --only-plat -w {dest_dir} {wheel}"
|
|
56
|
+
|
|
57
|
+
[[tool.cibuildwheel.overrides]]
|
|
58
|
+
select = "*-manylinux_aarch64"
|
|
59
|
+
repair-wheel-command = "auditwheel repair --plat manylinux_2_17_aarch64 --only-plat -w {dest_dir} {wheel}"
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2026, Daily
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""Time pipecat-resampler against other streaming resamplers, called from Python.
|
|
8
|
+
|
|
9
|
+
Prints microseconds per chunk, median of five runs: first at 24 -> 16 kHz by chunk
|
|
10
|
+
size, then by rate pair at 40 ms chunks, the size Pipecat's output transport sends.
|
|
11
|
+
soxr is a dev dependency; samplerate, PyAV and the Speex library are measured when
|
|
12
|
+
they're installed.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import ctypes
|
|
16
|
+
import ctypes.util
|
|
17
|
+
import statistics
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import soxr
|
|
22
|
+
|
|
23
|
+
from pipecat_resampler import Resampler
|
|
24
|
+
|
|
25
|
+
PAIRS = [
|
|
26
|
+
(24000, 16000),
|
|
27
|
+
(24000, 8000),
|
|
28
|
+
(16000, 8000),
|
|
29
|
+
(8000, 16000),
|
|
30
|
+
(48000, 16000),
|
|
31
|
+
(44100, 16000),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def pipecat_resampler(i, o):
|
|
36
|
+
process = Resampler(i, o).process
|
|
37
|
+
return lambda x: process(x.tobytes())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def soxr_vhq(i, o):
|
|
41
|
+
return soxr.ResampleStream(i, o, 1, dtype="int16", quality="VHQ").resample_chunk
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def soxr_qq(i, o):
|
|
45
|
+
return soxr.ResampleStream(i, o, 1, dtype="int16", quality="QQ").resample_chunk
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def samplerate_medium(i, o):
|
|
49
|
+
import samplerate
|
|
50
|
+
|
|
51
|
+
r = samplerate.Resampler("sinc_medium", channels=1)
|
|
52
|
+
ratio = o / i
|
|
53
|
+
|
|
54
|
+
def run(x):
|
|
55
|
+
y = r.process(x.astype(np.float32) * (1 / 32768), ratio)
|
|
56
|
+
return np.clip(np.round(y * 32768), -32768, 32767).astype(np.int16)
|
|
57
|
+
|
|
58
|
+
return run
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def pyav(i, o):
|
|
62
|
+
import av
|
|
63
|
+
|
|
64
|
+
r = av.AudioResampler(format="s16", layout="mono", rate=o)
|
|
65
|
+
|
|
66
|
+
def run(x):
|
|
67
|
+
frame = av.AudioFrame.from_ndarray(x.reshape(1, -1), format="s16", layout="mono")
|
|
68
|
+
frame.sample_rate = i
|
|
69
|
+
return [f.to_ndarray() for f in r.resample(frame)]
|
|
70
|
+
|
|
71
|
+
return run
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def speex_q7(i, o):
|
|
75
|
+
path = ctypes.util.find_library("speexdsp")
|
|
76
|
+
if not path:
|
|
77
|
+
raise OSError("libspeexdsp isn't installed")
|
|
78
|
+
lib = ctypes.CDLL(path)
|
|
79
|
+
lib.speex_resampler_init.restype = ctypes.c_void_p
|
|
80
|
+
lib.speex_resampler_init.argtypes = [
|
|
81
|
+
ctypes.c_uint32,
|
|
82
|
+
ctypes.c_uint32,
|
|
83
|
+
ctypes.c_uint32,
|
|
84
|
+
ctypes.c_int,
|
|
85
|
+
ctypes.POINTER(ctypes.c_int),
|
|
86
|
+
]
|
|
87
|
+
lib.speex_resampler_process_int.argtypes = [
|
|
88
|
+
ctypes.c_void_p,
|
|
89
|
+
ctypes.c_uint32,
|
|
90
|
+
ctypes.c_void_p,
|
|
91
|
+
ctypes.POINTER(ctypes.c_uint32),
|
|
92
|
+
ctypes.c_void_p,
|
|
93
|
+
ctypes.POINTER(ctypes.c_uint32),
|
|
94
|
+
]
|
|
95
|
+
err = ctypes.c_int(0)
|
|
96
|
+
state = lib.speex_resampler_init(1, i, o, 7, ctypes.byref(err))
|
|
97
|
+
out = np.zeros(o, dtype=np.int16)
|
|
98
|
+
in_len, out_len = ctypes.c_uint32(), ctypes.c_uint32()
|
|
99
|
+
|
|
100
|
+
def run(x):
|
|
101
|
+
in_len.value, out_len.value = len(x), len(out)
|
|
102
|
+
lib.speex_resampler_process_int(
|
|
103
|
+
state, 0, x.ctypes.data, ctypes.byref(in_len), out.ctypes.data, ctypes.byref(out_len)
|
|
104
|
+
)
|
|
105
|
+
return out[: out_len.value].copy()
|
|
106
|
+
|
|
107
|
+
return run
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def available():
|
|
111
|
+
candidates = {
|
|
112
|
+
"pipecat-resampler": pipecat_resampler,
|
|
113
|
+
"soxr VHQ": soxr_vhq,
|
|
114
|
+
"soxr QQ": soxr_qq,
|
|
115
|
+
"samplerate sinc_medium": samplerate_medium,
|
|
116
|
+
"PyAV": pyav,
|
|
117
|
+
"Speex quality 7": speex_q7,
|
|
118
|
+
}
|
|
119
|
+
found = {}
|
|
120
|
+
for name, make in candidates.items():
|
|
121
|
+
try:
|
|
122
|
+
make(24000, 16000)(np.zeros(960, dtype=np.int16))
|
|
123
|
+
except Exception:
|
|
124
|
+
continue
|
|
125
|
+
found[name] = make
|
|
126
|
+
return found
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def per_chunk_us(make, i, o, ms):
|
|
130
|
+
rng = np.random.default_rng(0)
|
|
131
|
+
parts = [(rng.standard_normal(i * ms // 1000) * 8000).astype(np.int16) for _ in range(500)]
|
|
132
|
+
# An untimed pass first, so the CPU and caches are warm for every resampler.
|
|
133
|
+
warm_up = make(i, o)
|
|
134
|
+
for p in parts:
|
|
135
|
+
warm_up(p)
|
|
136
|
+
runs = []
|
|
137
|
+
for _ in range(5):
|
|
138
|
+
process = make(i, o)
|
|
139
|
+
start = time.perf_counter()
|
|
140
|
+
for p in parts:
|
|
141
|
+
process(p)
|
|
142
|
+
runs.append((time.perf_counter() - start) / len(parts) * 1e6)
|
|
143
|
+
return statistics.median(runs)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def table(header, rows):
|
|
147
|
+
print("| " + " | ".join(header) + " |")
|
|
148
|
+
print("|" + "---|" * len(header))
|
|
149
|
+
for row in rows:
|
|
150
|
+
print("| " + " | ".join(row) + " |")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def main():
|
|
154
|
+
resamplers = available()
|
|
155
|
+
print(f"pipecat-resampler kernel: {Resampler(24000, 16000).kernel}\n")
|
|
156
|
+
|
|
157
|
+
print("24 -> 16 kHz, µs per chunk\n")
|
|
158
|
+
sizes = (10, 20, 40)
|
|
159
|
+
table(
|
|
160
|
+
["Resampler"] + [f"{ms} ms" for ms in sizes],
|
|
161
|
+
[
|
|
162
|
+
[name] + [f"{per_chunk_us(make, 24000, 16000, ms):.1f}" for ms in sizes]
|
|
163
|
+
for name, make in resamplers.items()
|
|
164
|
+
],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
print("\n40 ms chunks, µs per chunk\n")
|
|
168
|
+
table(
|
|
169
|
+
["Resampler"] + [f"{i / 1000:g} -> {o / 1000:g} kHz" for i, o in PAIRS],
|
|
170
|
+
[
|
|
171
|
+
[name] + [f"{per_chunk_us(make, i, o, 40):.1f}" for i, o in PAIRS]
|
|
172
|
+
for name, make in resamplers.items()
|
|
173
|
+
],
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
main()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2026, Daily
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""Builds the C extension on the stable ABI, one wheel per platform for Python 3.11+."""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from setuptools import Extension, setup
|
|
12
|
+
|
|
13
|
+
if sys.platform == "win32":
|
|
14
|
+
compile_args = ["/O2"]
|
|
15
|
+
libraries = []
|
|
16
|
+
else:
|
|
17
|
+
compile_args = ["-O3", "-fvisibility=hidden", "-std=c11"]
|
|
18
|
+
libraries = ["m"]
|
|
19
|
+
|
|
20
|
+
setup(
|
|
21
|
+
ext_modules=[
|
|
22
|
+
Extension(
|
|
23
|
+
"pipecat_resampler._resampler",
|
|
24
|
+
sources=["src/pipecat_resampler/_resampler.c", "src/pipecat_resampler/_kernel.c"],
|
|
25
|
+
define_macros=[("Py_LIMITED_API", "0x030B0000")],
|
|
26
|
+
py_limited_api=True,
|
|
27
|
+
extra_compile_args=compile_args,
|
|
28
|
+
libraries=libraries,
|
|
29
|
+
)
|
|
30
|
+
],
|
|
31
|
+
options={"bdist_wheel": {"py_limited_api": "cp311"}},
|
|
32
|
+
)
|