From a3ba9439b7e5322286e26de3f627ffe3e6a0d8f4 Mon Sep 17 00:00:00 2001 From: Tino Wagner Date: Thu, 25 Jan 2024 22:48:06 +0100 Subject: [PATCH 1/8] Release GIL while resampling Fixes #13. --- src/samplerate.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/samplerate.cpp b/src/samplerate.cpp index 42bb4d3..31cb41e 100644 --- a/src/samplerate.cpp +++ b/src/samplerate.cpp @@ -179,7 +179,10 @@ class Resampler { sr_ratio // src_ratio, sampling rate conversion ratio }; - error_handler(src_process(_state, &src_data)); + error_handler([&]() { + py::gil_scoped_release release; + return src_process(_state, &src_data); + }()); // create a shorter view of the array if ((size_t)src_data.output_frames_gen < new_size) { @@ -294,8 +297,11 @@ class CallbackResampler { if (_state == nullptr) _create(); // read from the callback - size_t output_frames_gen = src_callback_read( - _state, _ratio, (long)frames, static_cast(outbuf.ptr)); + size_t output_frames_gen = [&]() { + py::gil_scoped_release release; + return src_callback_read(_state, _ratio, (long)frames, + static_cast(outbuf.ptr)); + }(); // check error status if (output_frames_gen == 0) { @@ -407,9 +413,10 @@ py::array_t resample( sr_ratio // src_ratio, sampling rate conversion ratio }; - int ret_code = src_simple(&src_data, converter_type_int, channels); - - error_handler(ret_code); + error_handler([&]() { + py::gil_scoped_release release; + return src_simple(&src_data, converter_type_int, channels); + }()); // create a shorter view of the array if ((size_t)src_data.output_frames_gen < new_size) { From e2ed8bd290c9cf751fd44ad6dd35273de3d7f947 Mon Sep 17 00:00:00 2001 From: Tino Wagner Date: Thu, 25 Jan 2024 23:03:20 +0100 Subject: [PATCH 2/8] fixup! Release GIL while resampling acquire GIL for execution of callback --- src/samplerate.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/samplerate.cpp b/src/samplerate.cpp index 31cb41e..a58605c 100644 --- a/src/samplerate.cpp +++ b/src/samplerate.cpp @@ -346,6 +346,8 @@ long the_callback_func(void *cb_data, float **data) { CallbackResampler *cb = static_cast(cb_data); int cb_channels = cb->get_channels(); + py::gil_scoped_acquire acquire; + // get the data as a numpy array auto input = cb->callback(); From 50ac76549c4c19312398a7d16f5df099d1054c1c Mon Sep 17 00:00:00 2001 From: Tino Wagner Date: Fri, 26 Jan 2024 01:17:42 +0100 Subject: [PATCH 3/8] fixup! Release GIL while resampling attempt to fix exception handling on windows, not tested --- src/samplerate.cpp | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/samplerate.cpp b/src/samplerate.cpp index a58605c..08ced74 100644 --- a/src/samplerate.cpp +++ b/src/samplerate.cpp @@ -297,11 +297,12 @@ class CallbackResampler { if (_state == nullptr) _create(); // read from the callback - size_t output_frames_gen = [&]() { + size_t output_frames_gen = 0; + { py::gil_scoped_release release; - return src_callback_read(_state, _ratio, (long)frames, - static_cast(outbuf.ptr)); - }(); + output_frames_gen = src_callback_read(_state, _ratio, (long)frames, + static_cast(outbuf.ptr)); + } // check error status if (output_frames_gen == 0) { @@ -346,13 +347,14 @@ long the_callback_func(void *cb_data, float **data) { CallbackResampler *cb = static_cast(cb_data); int cb_channels = cb->get_channels(); - py::gil_scoped_acquire acquire; - - // get the data as a numpy array - auto input = cb->callback(); + py::buffer_info inbuf; + { + py::gil_scoped_acquire acquire; - // accessors for the arrays - py::buffer_info inbuf = input.request(); + // get the data as a numpy array + auto input = cb->callback(); + inbuf = input.request(); + } // end of stream is signaled by a None, which is cast to a ndarray with ndim // == 0 From 272a4a6762cd518f17e88ff9e97019ac363a44ab Mon Sep 17 00:00:00 2001 From: Arlo Faria Date: Tue, 11 Jun 2024 13:12:55 -0700 Subject: [PATCH 4/8] Increase expected size of output sample buffer. --- src/samplerate.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/samplerate.cpp b/src/samplerate.cpp index 42bb4d3..b9522fc 100644 --- a/src/samplerate.cpp +++ b/src/samplerate.cpp @@ -158,8 +158,14 @@ class Resampler { if (channels != _channels || channels == 0) throw std::domain_error("Invalid number of channels in input data."); + // Add a "fudge factor" of 10,000. This is because the actual number of + // output samples generated on the last call when input is terminated can + // be more than the expected number of output samples during mid-stream + // steady-state processing. (Also, when the stream is started, the number + // of output samples generated will generally be zero or otherwise less + // than the number of samples in mid-stream processing.) const auto new_size = - static_cast(std::ceil(inbuf.shape[0] * sr_ratio)); + static_cast(std::ceil(inbuf.shape[0] * sr_ratio)) + 10000; // allocate output array std::vector out_shape{new_size}; From 62d5c162aaa0083dac38cc7b3c2dae9d9dd5c87e Mon Sep 17 00:00:00 2001 From: Arlo Faria Date: Tue, 11 Jun 2024 13:56:33 -0700 Subject: [PATCH 5/8] Use a macro definition and throw/raise a runtime error/exception. --- src/samplerate.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/samplerate.cpp b/src/samplerate.cpp index b9522fc..0869225 100644 --- a/src/samplerate.cpp +++ b/src/samplerate.cpp @@ -40,6 +40,9 @@ #define VERSION_INFO "nightly" #endif +// This value was empirically and somewhat arbitrarily chosen; increase it for further safety. +#define END_OF_INPUT_EXTRA_OUTPUT_FRAMES 10000 + namespace py = pybind11; using namespace pybind11::literals; @@ -158,14 +161,15 @@ class Resampler { if (channels != _channels || channels == 0) throw std::domain_error("Invalid number of channels in input data."); - // Add a "fudge factor" of 10,000. This is because the actual number of + // Add a "fudge factor" to the size. This is because the actual number of // output samples generated on the last call when input is terminated can // be more than the expected number of output samples during mid-stream // steady-state processing. (Also, when the stream is started, the number // of output samples generated will generally be zero or otherwise less // than the number of samples in mid-stream processing.) const auto new_size = - static_cast(std::ceil(inbuf.shape[0] * sr_ratio)) + 10000; + static_cast(std::ceil(inbuf.shape[0] * sr_ratio)) + + END_OF_INPUT_EXTRA_OUTPUT_FRAMES; // allocate output array std::vector out_shape{new_size}; @@ -191,6 +195,9 @@ class Resampler { if ((size_t)src_data.output_frames_gen < new_size) { out_shape[0] = src_data.output_frames_gen; output.resize(out_shape); + } else if ((size_t)src_data.output_frames_gen >= new_size) { + // This means our fudge factor is too small. + throw std::runtime_error("Generated more output samples than expected!"); } return output; From c6c046e81cd3b4dc405fb6ca5e360777250618ce Mon Sep 17 00:00:00 2001 From: Robin Scheibler Date: Sat, 11 Oct 2025 08:23:39 +0900 Subject: [PATCH 6/8] Removes python 3.8 and adds 3.13 and 3.14. --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 79cd4c6..2952274 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -11,7 +11,7 @@ jobs: max-parallel: 12 matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + python-version: [3.9, "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v3 - name: Checkout submodules From ffadacebc5ed4dae628c7ec9a462ff59a2084ad0 Mon Sep 17 00:00:00 2001 From: Robin Scheibler Date: Sun, 19 Oct 2025 14:15:17 +0900 Subject: [PATCH 7/8] Update the license specifier format in pyproject.toml --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9977bb5..481bc76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,14 +16,13 @@ classifiers=[ "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Science/Research", - "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Topic :: Scientific/Engineering", "Topic :: Multimedia :: Sound/Audio", ] keywords=["samplerate", "converter", "signal processing", "audio"] -license = {text = "MIT"} +license = "MIT" dependencies = ["numpy"] [tool.setuptools.dynamic] From dfa6e5c70f4bb5e7c5fc53fbe370fd512020add7 Mon Sep 17 00:00:00 2001 From: Shaun Eccles-Smith Date: Wed, 19 Nov 2025 15:29:45 +1100 Subject: [PATCH 8/8] Enhance GIL management and error handling in resampling operations --- src/samplerate.cpp | 87 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 20 deletions(-) diff --git a/src/samplerate.cpp b/src/samplerate.cpp index 894ef2d..485cd66 100644 --- a/src/samplerate.cpp +++ b/src/samplerate.cpp @@ -189,16 +189,21 @@ class Resampler { sr_ratio // src_ratio, sampling rate conversion ratio }; - error_handler([&]() { + // Release GIL for the entire resampling operation + int err_code; + long output_frames_gen; + { py::gil_scoped_release release; - return src_process(_state, &src_data); - }()); + err_code = src_process(_state, &src_data); + output_frames_gen = src_data.output_frames_gen; + } + error_handler(err_code); // create a shorter view of the array - if ((size_t)src_data.output_frames_gen < new_size) { - out_shape[0] = src_data.output_frames_gen; + if ((size_t)output_frames_gen < new_size) { + out_shape[0] = output_frames_gen; output.resize(out_shape); - } else if ((size_t)src_data.output_frames_gen >= new_size) { + } else if ((size_t)output_frames_gen >= new_size) { // This means our fudge factor is too small. throw std::runtime_error("Generated more output samples than expected!"); } @@ -227,6 +232,7 @@ class CallbackResampler { callback_t _callback = nullptr; np_array_f32 _current_buffer; size_t _buffer_ndim = 0; + std::string _callback_error_msg = ""; public: double _ratio = 0.0; @@ -275,6 +281,7 @@ class CallbackResampler { _callback(r._callback), _current_buffer(std::move(r._current_buffer)), _buffer_ndim(r._buffer_ndim), + _callback_error_msg(std::move(r._callback_error_msg)), _ratio(r._ratio), _converter_type(r._converter_type), _channels(r._channels) { @@ -290,6 +297,11 @@ class CallbackResampler { void set_buffer(const np_array_f32 &new_buf) { _current_buffer = new_buf; } size_t get_channels() { return _channels; } + void set_callback_error(const std::string &error_msg) { + _callback_error_msg = error_msg; + } + std::string get_callback_error() const { return _callback_error_msg; } + void clear_callback_error() { _callback_error_msg = ""; } np_array_f32 callback(void) { auto input = _callback(); @@ -309,17 +321,32 @@ class CallbackResampler { if (_state == nullptr) _create(); - // read from the callback + // clear any previous callback error + clear_callback_error(); + + // read from the callback - note: GIL is managed by the_callback_func + // which acquires it only when calling the Python callback size_t output_frames_gen = 0; + int err_code = 0; { py::gil_scoped_release release; output_frames_gen = src_callback_read(_state, _ratio, (long)frames, static_cast(outbuf.ptr)); + // Get error code while GIL is released + if (output_frames_gen == 0) { + err_code = src_error(_state); + } + } + + // check if callback had an error + std::string callback_error = get_callback_error(); + if (!callback_error.empty()) { + throw std::domain_error(callback_error); } // check error status if (output_frames_gen == 0) { - error_handler(src_error(_state)); + error_handler(err_code); } // if there is only one channel and the input array had only on dimension @@ -377,11 +404,17 @@ long the_callback_func(void *cb_data, float **data) { int channels = 1; if (inbuf.ndim == 2) channels = inbuf.shape[1]; - else if (inbuf.ndim > 2) - throw std::domain_error("Input array should have at most 2 dimensions"); + else if (inbuf.ndim > 2) { + // Cannot throw exception in C callback - store error and return 0 + cb->set_callback_error("Input array should have at most 2 dimensions"); + return 0; + } - if (channels != cb_channels || channels == 0) - throw std::domain_error("Invalid number of channels in input data."); + if (channels != cb_channels || channels == 0) { + // Cannot throw exception in C callback - store error and return 0 + cb->set_callback_error("Invalid number of channels in input data."); + return 0; + } *data = static_cast(inbuf.ptr); @@ -409,8 +442,12 @@ py::array_t resample( if (channels == 0) throw std::domain_error("Invalid number of channels (0) in input data."); + // Add buffer space to match Resampler.process() behavior with end_of_input=True + // src_simple internally behaves like end_of_input=True, so it may generate + // extra samples from buffer flushing, especially for certain converters const auto new_size = - static_cast(std::ceil(inbuf.shape[0] * sr_ratio)); + static_cast(std::ceil(inbuf.shape[0] * sr_ratio)) + + END_OF_INPUT_EXTRA_OUTPUT_FRAMES; // allocate output array std::vector out_shape{new_size}; @@ -430,21 +467,31 @@ py::array_t resample( sr_ratio // src_ratio, sampling rate conversion ratio }; - error_handler([&]() { + // Release GIL for the entire resampling operation + int err_code; + long output_frames_gen; + long input_frames_used; + { py::gil_scoped_release release; - return src_simple(&src_data, converter_type_int, channels); - }()); + err_code = src_simple(&src_data, converter_type_int, channels); + output_frames_gen = src_data.output_frames_gen; + input_frames_used = src_data.input_frames_used; + } + error_handler(err_code); // create a shorter view of the array - if ((size_t)src_data.output_frames_gen < new_size) { - out_shape[0] = src_data.output_frames_gen; + if ((size_t)output_frames_gen < new_size) { + out_shape[0] = output_frames_gen; output.resize(out_shape); + } else if ((size_t)output_frames_gen >= new_size) { + // This means our fudge factor is too small. + throw std::runtime_error("Generated more output samples than expected!"); } if (verbose) { py::print("samplerate info:"); - py::print(src_data.input_frames_used, " input frames used"); - py::print(src_data.output_frames_gen, " output frames generated"); + py::print(input_frames_used, " input frames used"); + py::print(output_frames_gen, " output frames generated"); } return output;