Skip to content

Commit 5313545

Browse files
Copilotshauneccles
andcommitted
Phase 4 complete: Implement resample() function with nanobind
Co-authored-by: shauneccles <21007065+shauneccles@users.noreply.github.com>
1 parent 7c2f339 commit 5313545

1 file changed

Lines changed: 144 additions & 0 deletions

File tree

src/samplerate_nb.cpp

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
#include <samplerate.h>
3232

3333
#include <cmath>
34+
#include <cstring>
3435
#include <iostream>
36+
#include <sstream>
3537
#include <string>
3638
#include <typeinfo>
3739
#include <vector>
@@ -104,6 +106,114 @@ void error_handler(int errnum) {
104106
}
105107
}
106108

109+
nb::ndarray<nb::numpy, float> resample(
110+
const nb::ndarray<nb::numpy, const float, nb::c_contig> &input,
111+
double sr_ratio, const nb::object &converter_type, bool verbose) {
112+
// input array has shape (n_samples, n_channels)
113+
int converter_type_int = get_converter_type(converter_type);
114+
115+
// Get array dimensions
116+
size_t ndim = input.ndim();
117+
size_t num_frames = input.shape(0);
118+
119+
// set the number of channels
120+
int channels = 1;
121+
if (ndim == 2) {
122+
channels = input.shape(1);
123+
} else if (ndim > 2) {
124+
throw std::domain_error("Input array should have at most 2 dimensions");
125+
}
126+
127+
if (channels == 0) {
128+
throw std::domain_error("Invalid number of channels (0) in input data.");
129+
}
130+
131+
// Add buffer space to match Resampler.process() behavior with end_of_input=True
132+
// src_simple internally behaves like end_of_input=True, so it may generate
133+
// extra samples from buffer flushing, especially for certain converters
134+
const auto new_size =
135+
static_cast<size_t>(std::ceil(num_frames * sr_ratio))
136+
+ END_OF_INPUT_EXTRA_OUTPUT_FRAMES;
137+
138+
// Allocate output array
139+
size_t total_elements = new_size * channels;
140+
float* output_data = new float[total_elements];
141+
142+
// Create capsule for memory management
143+
nb::capsule owner(output_data, [](void* p) noexcept {
144+
delete[] static_cast<float*>(p);
145+
});
146+
147+
// libsamplerate struct
148+
SRC_DATA src_data = {
149+
const_cast<float *>(input.data()), // data_in
150+
output_data, // data_out
151+
static_cast<long>(num_frames), // input_frames
152+
long(new_size), // output_frames
153+
0, // input_frames_used, filled by libsamplerate
154+
0, // output_frames_gen, filled by libsamplerate
155+
0, // end_of_input, not used by src_simple ?
156+
sr_ratio // src_ratio, sampling rate conversion ratio
157+
};
158+
159+
// Release GIL for the entire resampling operation
160+
int err_code;
161+
long output_frames_gen;
162+
long input_frames_used;
163+
{
164+
nb::gil_scoped_release release;
165+
err_code = src_simple(&src_data, converter_type_int, channels);
166+
output_frames_gen = src_data.output_frames_gen;
167+
input_frames_used = src_data.input_frames_used;
168+
}
169+
error_handler(err_code);
170+
171+
// Handle unexpected output size
172+
if ((size_t)output_frames_gen > new_size) {
173+
// This means our fudge factor is too small.
174+
throw std::runtime_error("Generated more output samples than expected!");
175+
}
176+
177+
if (verbose) {
178+
nb::print("samplerate info:");
179+
std::ostringstream oss1, oss2;
180+
oss1 << input_frames_used << " input frames used";
181+
oss2 << output_frames_gen << " output frames generated";
182+
nb::print(oss1.str().c_str());
183+
nb::print(oss2.str().c_str());
184+
}
185+
186+
// Create output ndarray with proper shape and stride
187+
size_t output_shape[2];
188+
int64_t output_stride[2];
189+
190+
if (ndim == 2) {
191+
output_shape[0] = output_frames_gen;
192+
output_shape[1] = channels;
193+
output_stride[0] = channels * sizeof(float);
194+
output_stride[1] = sizeof(float);
195+
196+
return nb::ndarray<nb::numpy, float>(
197+
output_data,
198+
2,
199+
output_shape,
200+
owner,
201+
output_stride
202+
);
203+
} else {
204+
output_shape[0] = output_frames_gen;
205+
output_stride[0] = sizeof(float);
206+
207+
return nb::ndarray<nb::numpy, float>(
208+
output_data,
209+
1,
210+
output_shape,
211+
owner,
212+
output_stride
213+
);
214+
}
215+
}
216+
107217
} // namespace samplerate
108218

109219
namespace sr = samplerate;
@@ -153,7 +263,41 @@ NB_MODULE(samplerate, m) {
153263
.value("linear", sr::ConverterType::linear)
154264
.export_values();
155265

266+
m_converters.def("resample", &sr::resample, R"mydelimiter(
267+
Resample the signal in `input_data` at once.
268+
269+
Parameters
270+
----------
271+
input_data : ndarray
272+
Input data.
273+
Input data with one or more channels is represented as a 2D array of shape
274+
(`num_frames`, `num_channels`).
275+
A single channel can be provided as a 1D array of `num_frames` length.
276+
For use with `libsamplerate`, `input_data`
277+
is converted to 32-bit float and C (row-major) memory order.
278+
ratio : float
279+
Conversion ratio = output sample rate / input sample rate.
280+
converter_type : ConverterType, str, or int
281+
Sample rate converter (default: `sinc_best`).
282+
verbose : bool
283+
If `True`, print additional information about the conversion.
284+
285+
Returns
286+
-------
287+
output_data : ndarray
288+
Resampled input data.
289+
290+
Note
291+
----
292+
If samples are to be processed in chunks, `Resampler` and
293+
`CallbackResampler` will provide better results and allow for variable
294+
conversion ratios.
295+
)mydelimiter",
296+
"input"_a, "ratio"_a, "converter_type"_a = "sinc_best",
297+
"verbose"_a = false);
298+
156299
// Convenience imports
157300
m.attr("ResamplingError") = m_exceptions.attr("ResamplingError");
301+
m.attr("resample") = m_converters.attr("resample");
158302
m.attr("ConverterType") = m_converters.attr("ConverterType");
159303
}

0 commit comments

Comments
 (0)