Skip to content

Commit 33efc69

Browse files
authored
Merge pull request #5 from olilarkin/soxr
Add optional SOXR resampling support
2 parents b657ebe + 33d5060 commit 33efc69

11 files changed

Lines changed: 181 additions & 7 deletions

File tree

CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ option(LIBROSA_BUILD_CROSSVAL_TESTS "Build cross-validation tests against Python
1818
option(LIBROSA_BUILD_CLI "Build the librosa CLI tool" OFF)
1919
option(LIBROSA_BUILD_WASM "Build the Emscripten WASM/npm binding" OFF)
2020
option(LIBROSA_BUILD_SWIFT_C_WRAPPER "Build the Swift-friendly C ABI wrapper" OFF)
21+
option(LIBROSA_USE_SOXR "Use libsoxr for explicit SOXR resampling modes" OFF)
2122
if(APPLE)
2223
option(LIBROSA_USE_AUDIOTOOLBOX "Use Apple AudioToolbox for audio file I/O" ON)
2324
else()
@@ -33,6 +34,10 @@ if(LIBROSA_BUILD_WASM AND NOT EMSCRIPTEN)
3334
message(FATAL_ERROR "LIBROSA_BUILD_WASM requires configuring with emcmake/Emscripten")
3435
endif()
3536

37+
if(LIBROSA_BUILD_WASM AND LIBROSA_USE_SOXR)
38+
message(FATAL_ERROR "LIBROSA_USE_SOXR is not supported for Emscripten/WASM builds")
39+
endif()
40+
3641
if(LIBROSA_USE_AUDIOTOOLBOX AND NOT APPLE)
3742
message(FATAL_ERROR "LIBROSA_USE_AUDIOTOOLBOX is only available on Apple platforms")
3843
endif()
@@ -54,6 +59,12 @@ else()
5459
endif()
5560
endif()
5661

62+
if(LIBROSA_USE_SOXR)
63+
find_package(PkgConfig REQUIRED)
64+
pkg_check_modules(SOXR REQUIRED soxr)
65+
message(STATUS "librosa SOXR resampler: enabled (${SOXR_VERSION})")
66+
endif()
67+
5768
# Resolve FFT backend selection.
5869
if(LIBROSA_FFT_BACKEND STREQUAL "auto")
5970
if(APPLE)
@@ -169,6 +180,13 @@ elseif(LIBROSA_USE_AUDIOTOOLBOX)
169180
target_link_libraries(librosa PUBLIC "-framework AudioToolbox" "-framework CoreFoundation")
170181
endif()
171182

183+
if(LIBROSA_USE_SOXR)
184+
target_compile_definitions(librosa PUBLIC LIBROSA_HAS_SOXR)
185+
target_include_directories(librosa PRIVATE ${SOXR_INCLUDE_DIRS})
186+
target_link_directories(librosa PUBLIC ${SOXR_LIBRARY_DIRS})
187+
target_link_libraries(librosa PUBLIC ${SOXR_LIBRARIES})
188+
endif()
189+
172190
if(_librosa_fft_backend STREQUAL "fftw")
173191
target_include_directories(librosa PRIVATE ${FFTW3_INCLUDE_DIRS})
174192
target_link_directories(librosa PUBLIC ${FFTW3_LIBRARY_DIRS})

NOTICE.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,15 @@ These are *not* bundled — they're expected to come from the user's system
3737
| [FFTW3](https://www.fftw.org/) | when `LIBROSA_FFT_BACKEND=fftw` (default on Linux/Windows) | GPL-2.0-or-later | If distributing binaries, consider using the `accelerate` or `pffft` backend, or obtain a non-GPL FFTW commercial license. |
3838
| Apple Accelerate framework | when `LIBROSA_FFT_BACKEND=accelerate` (default on Apple) | Apple SDK terms | System framework, no extra install. |
3939
| Apple AudioToolbox framework | when `LIBROSA_USE_AUDIOTOOLBOX=ON` or when building the Swift package | Apple SDK terms | System audio file I/O framework, no extra install. |
40+
| [libsoxr](https://sourceforge.net/projects/soxr/) | when `LIBROSA_USE_SOXR=ON` | LGPL-2.1-or-later | Optional SOXR resampler for Python-librosa parity checks. Off by default. |
4041

41-
### LGPL notes (libsndfile)
42+
### LGPL notes (libsndfile, libsoxr)
4243

43-
libsndfile is LGPL-2.1. Apple SwiftPM builds do not link it. For non-Apple
44-
CMake builds that do link it, librosa.cpp itself remains ISC, but a binary that
45-
links against an LGPL library inherits LGPL obligations for the combined work.
46-
In practice this means:
44+
libsndfile is LGPL-2.1 and libsoxr is LGPL-2.1-or-later. Apple SwiftPM builds
45+
do not link either one by default. For CMake builds that do link an LGPL
46+
library, librosa.cpp itself remains ISC, but a binary that links against an
47+
LGPL library inherits LGPL obligations for the combined work. In practice this
48+
means:
4749

4850
- Dynamic linking (the default on all platforms when using system packages)
4951
satisfies the LGPL naturally — end users can swap the `.so` / `.dylib`.

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ Build-time:
4343
- An internal Kaiser-windowed sinc resampler provides the `kaiser_*` modes used
4444
by the CQT/default resample path, including `kaiser_hq`. No libsoxr
4545
install or LGPL resampler link is required.
46+
- Optional SOXR resampling modes (`soxr_vhq`, `soxr_hq`, `soxr_mq`,
47+
`soxr_lq`, `soxr_qq`) can be enabled with `-DLIBROSA_USE_SOXR=ON` for
48+
Python-librosa parity checks. This is off by default because libsoxr is LGPL.
4649

4750
Bundled (no action needed):
4851

@@ -64,6 +67,14 @@ Accelerate backend):
6467
brew install cmake ninja
6568
```
6669

70+
To enable optional SOXR resampling modes in CMake builds, install libsoxr and
71+
configure with `-DLIBROSA_USE_SOXR=ON`:
72+
73+
```bash
74+
brew install libsoxr
75+
cmake -S . -B build-soxr -DLIBROSA_USE_SOXR=ON
76+
```
77+
6778
## Build
6879

6980
```bash

include/librosa/core/audio.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ ArrayXr to_mono(const ArrayXr& y); // Pass-through for already mono
7575
/// @param y Input signal
7676
/// @param orig_sr Original sample rate
7777
/// @param target_sr Target sample rate
78-
/// @param res_type Resampling method ("kaiser_*", "fft", or "linear")
78+
/// @param res_type Resampling method ("kaiser_*", "fft", "linear", or
79+
/// "soxr_*" when built with LIBROSA_USE_SOXR)
7980
/// @param fix Adjust length to match expected
8081
/// @param scale Scale for energy preservation
8182
/// @return Resampled signal

include/librosa/effects.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ ArrayXr time_stretch(
3939
/// @param sr Sample rate
4040
/// @param n_steps Number of steps to shift (can be fractional)
4141
/// @param bins_per_octave Number of steps per octave
42-
/// @param res_type Resampling method ("kaiser_*", "fft", or "linear")
42+
/// @param res_type Resampling method ("kaiser_*", "fft", "linear", or
43+
/// "soxr_*" when built with LIBROSA_USE_SOXR)
4344
/// @param n_fft FFT window size
4445
/// @param hop_length Samples between frames
4546
/// @return Pitch-shifted audio

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
# Pin to the upstream version librosa.cpp is ported from.
66
librosa==0.11.0
77
numpy
8+
soxr

src/core/audio.cpp

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
#include <cmath>
1414
#include <limits>
1515
#include <vector>
16+
#ifdef LIBROSA_HAS_SOXR
17+
#include <soxr.h>
18+
#endif
1619
#include "../internal/fft.hpp"
1720

1821
namespace librosa {
@@ -285,6 +288,10 @@ namespace {
285288
res_type == "kaiser_fast";
286289
}
287290

291+
bool is_soxr_resampler(const std::string& res_type) {
292+
return res_type.rfind("soxr", 0) == 0;
293+
}
294+
288295
SincResamplerSpec kaiser_resampler_spec(const std::string& res_type) {
289296
if (res_type == "kaiser_vhq") {
290297
return {96, 16.0, 0.975};
@@ -373,6 +380,57 @@ namespace {
373380

374381
return y_hat;
375382
}
383+
384+
#ifdef LIBROSA_HAS_SOXR
385+
unsigned long soxr_quality_recipe(const std::string& res_type) {
386+
if (res_type == "soxr_vhq") return SOXR_VHQ;
387+
if (res_type == "soxr_hq") return SOXR_HQ;
388+
if (res_type == "soxr_mq") return SOXR_MQ;
389+
if (res_type == "soxr_lq") return SOXR_LQ;
390+
if (res_type == "soxr_qq") return SOXR_QQ;
391+
throw ParameterError("Unknown SOXR resampling type: " + res_type);
392+
}
393+
394+
ArrayXr soxr_resample(const ArrayXr& y, Real orig_sr, Real target_sr,
395+
Eigen::Index n_samples, const std::string& res_type) {
396+
ArrayXr y_hat(n_samples);
397+
if (n_samples == 0) {
398+
return y_hat;
399+
}
400+
401+
soxr_io_spec_t io_spec = soxr_io_spec(SOXR_FLOAT64_I, SOXR_FLOAT64_I);
402+
soxr_quality_spec_t quality_spec =
403+
soxr_quality_spec(soxr_quality_recipe(res_type), 0);
404+
soxr_runtime_spec_t runtime_spec = soxr_runtime_spec(1);
405+
406+
size_t idone = 0;
407+
size_t odone = 0;
408+
soxr_error_t err = soxr_oneshot(
409+
static_cast<double>(orig_sr),
410+
static_cast<double>(target_sr),
411+
1,
412+
y.data(),
413+
static_cast<size_t>(y.size()),
414+
&idone,
415+
y_hat.data(),
416+
static_cast<size_t>(n_samples),
417+
&odone,
418+
&io_spec,
419+
&quality_spec,
420+
&runtime_spec);
421+
422+
if (err) {
423+
throw ParameterError(std::string("SOXR resampling failed: ") +
424+
soxr_strerror(err));
425+
}
426+
427+
if (odone != static_cast<size_t>(y_hat.size())) {
428+
y_hat.conservativeResize(static_cast<Eigen::Index>(odone));
429+
}
430+
431+
return y_hat;
432+
}
433+
#endif
376434
}
377435

378436
// ============================================================================
@@ -603,6 +661,13 @@ ArrayXr resample(const ArrayXr& y, Real orig_sr, Real target_sr,
603661

604662
if (is_kaiser_resampler(res_type)) {
605663
y_hat = kaiser_sinc_resample(y, ratio, n_samples, kaiser_resampler_spec(res_type));
664+
} else if (is_soxr_resampler(res_type)) {
665+
#ifdef LIBROSA_HAS_SOXR
666+
y_hat = soxr_resample(y, orig_sr, target_sr, n_samples, res_type);
667+
#else
668+
throw ParameterError(
669+
"SOXR resampling requires configuring with -DLIBROSA_USE_SOXR=ON");
670+
#endif
606671
} else if (res_type == "fft" || res_type == "scipy") {
607672
int n_fft = y.size();
608673
int n_out = n_samples;

tests/crossval/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,13 @@ Different modules may require different tolerance levels due to:
6868

6969
Current tolerances:
7070
- `DEFAULT_TOLERANCE = 1e-5`: For exact mathematical operations
71+
- `1e-6`: For optional SOXR resampling parity when libsoxr is enabled
7172
- `LOOSE_TOLERANCE = 1e-3`: For filter banks, spectral features
7273

7374
## Modules Covered
7475

7576
- [x] Convert (hz_to_mel, mel_to_hz, hz_to_midi, amplitude_to_db, power_to_db)
77+
- [x] Audio (SOXR resampling modes when built with `LIBROSA_USE_SOXR=ON`)
7678
- [x] Filters (mel filterbank, chroma filterbank)
7779
- [x] Spectrum (STFT magnitude/phase)
7880
- [x] Features (melspectrogram, MFCC, chroma, spectral features, RMS, ZCR)

tests/crossval/generate_references.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,32 @@ def generate_spectrum_references():
9696
{"n_fft": n_fft, "hop_length": hop_length})
9797

9898

99+
def generate_resample_references():
100+
"""Generate references for SOXR resampling."""
101+
print("\n=== Resample Module ===")
102+
103+
sr = 22050
104+
target_sr = 8000
105+
duration = 0.25
106+
t = np.arange(int(sr * duration), dtype=np.float64) / sr
107+
y = (
108+
0.5 * np.sin(2 * np.pi * 220.0 * t)
109+
+ 0.25 * np.sin(2 * np.pi * 997.0 * t)
110+
+ 0.1 * np.sin(2 * np.pi * 3200.0 * t)
111+
)
112+
y[::997] += 0.05
113+
y = y.astype(np.float64)
114+
115+
save_array("resample_test_signal", y, {"sr": sr, "target_sr": target_sr})
116+
117+
for res_type in ["soxr_vhq", "soxr_hq", "soxr_mq", "soxr_lq", "soxr_qq"]:
118+
y_hat = librosa.resample(
119+
y, orig_sr=sr, target_sr=target_sr, res_type=res_type
120+
)
121+
save_array(f"resample_{res_type}", y_hat,
122+
{"sr": sr, "target_sr": target_sr, "res_type": res_type})
123+
124+
99125
def generate_filters_references():
100126
"""Generate references for filters module."""
101127
print("\n=== Filters Module ===")
@@ -717,6 +743,7 @@ def main():
717743

718744
generate_convert_references()
719745
generate_spectrum_references()
746+
generate_resample_references()
720747
generate_filters_references()
721748
generate_feature_references()
722749
generate_onset_references()

tests/crossval/test_crossval.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
// Include all librosa headers
1717
#include <librosa/types.hpp>
18+
#include <librosa/core/audio.hpp>
1819
#include <librosa/core/convert.hpp>
1920
#include <librosa/core/spectrum.hpp>
2021
#include <librosa/core/harmonic.hpp>
@@ -490,6 +491,35 @@ TEST_F(CrossValidationTest, ChromaFilterbank) {
490491
expectArrayNear(chroma_fb, expected, LOOSE_TOLERANCE, "chroma filterbank");
491492
}
492493

494+
// ============================================================================
495+
// Audio Resampling Tests
496+
// ============================================================================
497+
498+
TEST_F(CrossValidationTest, ResampleSoxrModes) {
499+
if (dataDir.empty()) GTEST_SKIP() << "Reference data not found";
500+
501+
#ifndef LIBROSA_HAS_SOXR
502+
GTEST_SKIP() << "libsoxr support not enabled";
503+
#else
504+
json_util::ArrayData signal_ref;
505+
if (!loadArray("resample_test_signal", signal_ref)) GTEST_SKIP();
506+
507+
ArrayXr y = signal_ref.toArrayXr();
508+
const std::vector<std::string> modes = {
509+
"soxr_vhq", "soxr_hq", "soxr_mq", "soxr_lq", "soxr_qq"
510+
};
511+
512+
for (const auto& mode : modes) {
513+
json_util::ArrayData expected_ref;
514+
ASSERT_TRUE(loadArray("resample_" + mode, expected_ref));
515+
516+
ArrayXr actual = resample(y, 22050, 8000, mode, true, false);
517+
ArrayXr expected = expected_ref.toArrayXr();
518+
expectArrayNear(actual, expected, 1e-6, "resample " + mode);
519+
}
520+
#endif
521+
}
522+
493523
// ============================================================================
494524
// Spectrum Module Tests
495525
// ============================================================================

0 commit comments

Comments
 (0)