Skip to content

Commit fc463fa

Browse files
committed
Expose libswresample options in AudioResampler
closes #2262 Add an `options` parameter to `AudioResampler` so callers can pass `libswresample` options (e.g. `resampler`, `filter_size`, `phase_shift`, `cutoff`, `precision`) through to the underlying resampler. When options are supplied, the conversion is performed by an explicit `aresample` filter (which owns the SwrContext) instead of the one FFmpeg auto-inserts before `aformat`. Options are forwarded as filter options, which search child contexts and reach the SwrContext. No new C bindings are required.
1 parent 188f1b3 commit fc463fa

5 files changed

Lines changed: 71 additions & 4 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Breaking:
3737

3838
Features:
3939

40-
- Nothing (yet)
40+
- Add ``options`` parameter to ``AudioResampler`` for passing ``libswresample`` options (e.g. ``resampler``, ``filter_size``, ``cutoff``) by :gh-user:`WyattBlue` (:issue:`2262`).
4141

4242
Fixes:
4343

av/audio/resampler.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ cdef class AudioResampler:
1313
cdef readonly AudioLayout layout
1414
cdef readonly int rate
1515
cdef readonly unsigned int frame_size
16+
cdef readonly dict options
1617

1718
cdef Graph graph
1819
cpdef list resample(self, AudioFrame)

av/audio/resampler.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,23 @@
99
@cython.final
1010
@cython.cclass
1111
class AudioResampler:
12-
"""AudioResampler(format=None, layout=None, rate=None)
12+
"""AudioResampler(format=None, layout=None, rate=None, frame_size=None, options=None)
1313
1414
:param AudioFormat format: The target format, or string that parses to one
1515
(e.g. ``"s16"``).
1616
:param AudioLayout layout: The target layout, or an int/string that parses
1717
to one (e.g. ``"stereo"``).
1818
:param int rate: The target sample rate.
19+
:param int frame_size: The number of samples per output frame.
20+
:param dict options: ``libswresample`` options passed to the underlying
21+
``aresample`` filter (e.g. ``{"resampler": "soxr", "precision": "28"}``).
22+
See the `FFmpeg resampler documentation
23+
<https://ffmpeg.org/ffmpeg-resampler.html>`_ for the full list.
1924
"""
2025

21-
def __cinit__(self, format=None, layout=None, rate=None, frame_size=None):
26+
def __cinit__(
27+
self, format=None, layout=None, rate=None, frame_size=None, options=None
28+
):
2229
if format is not None:
2330
self.format = (
2431
format if isinstance(format, AudioFormat) else AudioFormat(format)
@@ -29,6 +36,7 @@ def __cinit__(self, format=None, layout=None, rate=None, frame_size=None):
2936

3037
self.rate = int(rate) if rate else 0
3138
self.frame_size = int(frame_size) if frame_size else 0
39+
self.options = {str(k): str(v) for k, v in options.items()} if options else {}
3240
self.graph = None
3341

3442
@cython.ccall
@@ -91,7 +99,17 @@ def resample(self, frame: AudioFrame | None) -> list:
9199
channel_layouts=self.layout.name,
92100
)
93101
abuffersink = self.graph.add("abuffersink")
94-
abuffer.link_to(aformat)
102+
103+
# When libswresample options are given, do the conversion with an
104+
# explicit aresample filter (which owns the SwrContext) instead of
105+
# relying on the one FFmpeg auto-inserts before aformat.
106+
if self.options:
107+
aresample = self.graph.add("aresample", **self.options)
108+
abuffer.link_to(aresample)
109+
aresample.link_to(aformat)
110+
else:
111+
abuffer.link_to(aformat)
112+
95113
aformat.link_to(abuffersink)
96114
self.graph.configure()
97115

av/audio/resampler.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class AudioResampler:
88
rate: int
99
frame_size: int
1010
format: AudioFormat
11+
layout: AudioLayout
12+
options: dict[str, str]
1113
graph: Graph | None
1214

1315
def __init__(
@@ -16,5 +18,6 @@ class AudioResampler:
1618
layout: str | int | AudioLayout | None = None,
1719
rate: int | None = None,
1820
frame_size: int | None = None,
21+
options: dict[str, str] | None = None,
1922
) -> None: ...
2023
def resample(self, frame: AudioFrame | None) -> list[AudioFrame]: ...

tests/test_audioresampler.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,51 @@ def test_pts_missing_time_base() -> None:
269269
assert oframe.samples == 16
270270

271271

272+
def test_swr_options() -> None:
273+
"""
274+
libswresample options are passed through to the underlying aresample filter.
275+
"""
276+
resampler = AudioResampler(
277+
"fltp",
278+
"mono",
279+
16000,
280+
options={"filter_size": "32", "phase_shift": "12", "cutoff": "0.95"},
281+
)
282+
assert resampler.options == {
283+
"filter_size": "32",
284+
"phase_shift": "12",
285+
"cutoff": "0.95",
286+
}
287+
288+
iframe = AudioFrame("s16", "stereo", 1024)
289+
iframe.sample_rate = 48000
290+
iframe.time_base = Fraction(1, 48000)
291+
iframe.pts = 0
292+
293+
oframes = resampler.resample(iframe)
294+
assert len(oframes) == 1
295+
296+
oframe = oframes[0]
297+
assert oframe.sample_rate == 16000
298+
assert oframe.format.name == "fltp"
299+
assert oframe.layout.name == "mono"
300+
301+
302+
def test_swr_options_invalid() -> None:
303+
"""
304+
An unknown option is reported rather than silently ignored.
305+
"""
306+
resampler = AudioResampler("s16", "mono", 44100, options={"not_a_real_option": "1"})
307+
308+
iframe = AudioFrame("s16", "stereo", 1024)
309+
iframe.sample_rate = 48000
310+
iframe.time_base = Fraction(1, 48000)
311+
iframe.pts = 0
312+
313+
with pytest.raises(ValueError, match="unused config: not_a_real_option"):
314+
resampler.resample(iframe)
315+
316+
272317
def test_mismatched_input() -> None:
273318
"""
274319
Consecutive frames must have the same layout, sample format and sample rate.

0 commit comments

Comments
 (0)