recipe: soxr 1.1.0 - #115
Merged
Merged
Conversation
Adds a recipe for [soxr](https://github.com/dofuuz/python-soxr) 1.1.0 — the SoX Resampler ([libsoxr](https://sourceforge.net/projects/soxr/)) bound to Python: one-dimensional sample-rate conversion, mono or multi-channel, `float32`/`float64`/`int16`/`int32`, one-shot or streaming. Requested in [flet#6774](flet-dev/flet#6774). - [Docs](recipes/soxr/README.md) - [Example](recipes/soxr/examples/resample-tone) Resampling is the step almost every on-device audio pipeline needs and almost no model provides — recorders hand you 44.1/48 kHz, speech models want 16 kHz — and soxr does it in a few hundred KB with numpy as its only dependency. ## Recipe shape scikit-build-core + CMake over a self-contained sdist that vendors libsoxr — the `duckdb`/`rapidfuzz` archetype, **zero patches**. Upstream is unusually well-behaved for a cross build: it already guards nanobind's stub generation behind `NOT CMAKE_CROSSCOMPILING`, ships `vr-coefs.h` pre-generated so no host code generator runs, uses no `try_run` anywhere, and turns OpenMP, the LSR bindings and shared libraries off before `add_subdirectory(libsoxr)`. First **nanobind** recipe in the repo. It needs exactly two non-obvious settings, both commented in place: - **`-DCMAKE_SYSTEM_PROCESSOR={{ arch }}`, iOS lane only.** libsoxr's `SetSystemProcessor.cmake` probes `__x86_64__`/`__i386__`/`__arm__`; iOS arm64 defines `__aarch64__` and matches none, so `CMAKE_SYSTEM_PROCESSOR` stays empty, `FindSIMD32` falls through to its SSE branch, fails, and `cr32s`/`pffft32s`/`util32s` silently vanish from the archive. **The build stays green and a naive import-and-resample test still passes** — you just get the scalar resampler. Forcing the scalar engine on an otherwise identical build (`SOXR_USE_SIMD32=0`, 10 s mono 48k→16k at `HQ`, macOS arm64) measured 2.8x: 0.70 → 1.98 ms. The NDK presets the variable so Android must not get this flag; iOS-sim x86_64 self-detects. `test_simd_engine_compiled_in` asserts `engine() == "cr32s"` on device so a bump can't lose it quietly. - **libpython named explicitly in `CMAKE_MODULE_LINKER_FLAGS`, Android lane.** `nanobind_add_module` builds a `MODULE` library and the NDK links those with `-Wl,--no-undefined`, so the usual leave-Python-symbols-unresolved convention fails with a wall of `undefined symbol: PyExc_…`. scikit-build-core shlex-splits `CMAKE_ARGS`, so the `-L`/`-l` pair is comma-joined into the single `-Wl` argument. `STABLE_ABI` resolves on Apple but not under the NDK, so the platforms ship structurally different modules (`soxr_ext.abi3.so` vs `soxr_ext.cpython-312.so`). forge's `fix_wheel` rewrites the tag and accepts both names — no handling needed, noted only because it is surprising when diffing two wheels. ## Verification All six slices built locally (NDK r27d-era 27.0, iOS SDK 26.2, Python 3.12). Every wheel carries `cr32s`; the x86_64 slices additionally carry `cr64s` (libsoxr's AVX engine — there is no ARM equivalent, so its absence on ARM is expected). Correct `Machine` per ABI, every Android `LOAD` segment aligned `0x4000`, `DT_NEEDED` limited to bionic + `libc++_shared` + `libpython3.12`, iOS `LC_BUILD_VERSION` platform 2 on device and 7 on the simulators, `Requires-Dist: numpy` with `flet-libcpp-shared` promoted on Android only, 10 entries per wheel with no stray install artifacts. CI green across the full matrix: 18/18 wheels, 3.12/3.13/3.14 x 6 slices. On-device the suite was run on **every** Python rather than only the 3.12 default (`mobile_test_pythons=ALL`): 4 collected / 4 passed / EXIT 0 on all six legs — 3.12, 3.13 and 3.14 x android + ios — with `test_simd_engine_compiled_in` PASSED (not skipped) on each, so the SIMD engine is confirmed present and selected, not merely compiled. Run also verified locally on an arm64 emulator and arm64 simulator; CI's devices are x86_64, so between the two every arch that ships is covered on-device. ## Consumer notes (usage & recommendations) `soxr` converts audio sample rates — `soxr.resample(x, 48000, 16000)` for an array you hold, `soxr.ResampleStream(...).resample_chunk(chunk, last=...)` for a microphone feed or a file too big to hold at once. Input is mono `(frames,)` or `(frames, channels)`; the output dtype always matches the input. ### Install ```toml [project] dependencies = [ "flet", "soxr", ] ``` `numpy` comes with it, and is the only dependency. Wheels cover all Android ABIs and iOS slices; no `target_arch` restriction and no `source_packages` opt-in. ### Things worth knowing - **`HQ` (the default) is the right setting on a phone.** `QQ`/`LQ`/`MQ`/`HQ` all run on the SIMD engine; `VHQ` crosses into libsoxr's *double-precision* engine, whose SIMD variant is AVX-only — so on both ARM slices `VHQ` falls back to scalar `cr64` and is the slow option rather than the good one. - **Set `last=True` exactly once**, on the final chunk, or the filter tail never comes out. Chunked output then matches a single whole-signal `resample` to within 1e-6 (asserted). - **`resample_chunk` type-checks exactly** — `type(x) != np.ndarray`, so a numpy *subclass* is rejected even with the right dtype. Pass `np.asarray(x)`. - **It releases the GIL**, so `page.run_thread(...)` buys real parallelism. A `ResampleStream` carries filter state, so don't drive one from two threads. - **soxr does not decode or encode audio files** — it resamples arrays. `.wav` is the stdlib `wave` module; anything else needs its own package. - **Licensing:** soxr is **LGPL-2.1-or-later** and this wheel links libsoxr statically (the licence texts ship in the wheel's `dist-info/licenses/`). Not a first for the repo — `pymupdf` (AGPL), `pymssql`, `psycopg2` and `opaque` are already copyleft — but worth a deliberate decision for closed-source store apps, and the repo has no stated policy on it. Flagging, not advising.
- **iOS green-but-degraded wheels.** New entry for the inverse of the existing hardcoded-`CMAKE_SYSTEM_PROCESSOR` case: leaving it *unset* on the iOS lane makes a vendored library's own CPU probe fall through to its x86 branch and silently drop its SIMD sources. Nothing fails — the wheel just gets slower (measured 2.8x on libsoxr). Records the tell (the absent `-- Found SIMD32:` line), the artifact check, and the general defence: assert the fast path is compiled in, not merely that the module imports. Also warns that the right processor token is library-specific — libsoxr accepts `arm64`, opencv needs `aarch64` — so neither recipe should be copied blind. - **`--no-undefined` is not always upstream's doing.** Extends the existing entry: the NDK adds the flag itself, so any CMake `MODULE` target hits it on Android with no upstream involvement, which makes every nanobind recipe a candidate. Notes that `CMAKE_SHARED_LINKER_FLAGS` is the wrong knob for a MODULE target and how to check. Separable from the recipe commit — drop it if you'd rather keep skills out of a recipe PR. [skip ci] — touches no recipe, so a CI run here would only smoke-test and cancel the live soxr build.
Review feedback: the block before `### Storage` was partly upstream API reference, which belongs behind the link it already carries rather than in a recipe README somebody has to keep in sync. Applied test: **does being on a phone, in a Flet app, change the answer?** - Dropped the frames-first / mono-vs-multichannel paragraph. True identically on a desktop, and python-soxr's own docs say it. - Dropped the quality-value enumeration, kept the half that is mobile-specific: `HQ` is the highest setting still on the SIMD engine, and `VHQ` drops to a scalar core on ARM. And fixed the actual defect the review exposed: a section titled "Usage in a Flet app" contained no Flet. The lead now lands the result in a control via `page.run_thread`, which is what README.rst asks the section to open with. `### Threading` keeps the reasoning (GIL released, `ResampleStream` not shareable, `run_thread` pools) and no longer repeats the snippet. Net two lines longer, materially more Flet-specific, 199 lines. Also verified while here, which had been claimed but not tested: the example builds and runs as-is. `flet build ios-simulator` produces a working app that resamples on device — 480,000 frames @48k -> 160,000 @16k in 6.7 ms (1,490x realtime), with `soxr_ext.fwork` and numpy 2.4.6 confirmed inside the bundle. Its footer reads `QQ=cr32s LQ=cr32s MQ=cr32s HQ=cr32s VHQ=cr64` computed on the device, which independently confirms the SIMD claim this README makes. [skip ci] — docs only; recipe READMEs are in the changed-files ignore list, so a run here would only smoke-test.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a recipe for soxr 1.1.0 — the SoX Resampler (libsoxr) behind a Python API: one-dimensional sample-rate conversion, mono or multi-channel,
float32/float64/int16/int32, one-shot or streaming. Requested in flet#6774.Resampling is the step almost every on-device audio pipeline needs and almost no model provides — recorders hand you 44.1/48 kHz, speech models want 16 kHz — and soxr does it in a few hundred KB with numpy as its only dependency.
Recipe shape
scikit-build-core + CMake over a self-contained sdist that vendors libsoxr — the
duckdb/rapidfuzzarchetype, zero patches. Upstream is unusually well-behaved for a cross build: it already guards nanobind's stub generation behindNOT CMAKE_CROSSCOMPILING, shipsvr-coefs.hpre-generated so no host code generator runs, uses notry_runanywhere, and turns OpenMP, the LSR bindings and shared libraries off beforeadd_subdirectory(libsoxr).First nanobind recipe in the repo. It needs exactly two non-obvious settings, both commented in place:
-DCMAKE_SYSTEM_PROCESSOR={{ arch }}, iOS lane only. libsoxr'sSetSystemProcessor.cmakeprobes__x86_64__/__i386__/__arm__; iOS arm64 defines__aarch64__and matches none, soCMAKE_SYSTEM_PROCESSORstays empty,FindSIMD32falls through to its SSE branch, fails, andcr32s/pffft32s/util32ssilently vanish from the archive. The build stays green and a naive import-and-resample test still passes — you just get the scalar resampler. Forcing the scalar engine on an otherwise identical build (SOXR_USE_SIMD32=0, 10 s mono 48k→16k atHQ, macOS arm64) measured 2.8x: 0.70 → 1.98 ms. The NDK presets the variable so Android must not get this flag; iOS-sim x86_64 self-detects.test_simd_engine_compiled_inassertsengine() == "cr32s"on device so a bump can't lose it quietly.CMAKE_MODULE_LINKER_FLAGS, Android lane.nanobind_add_modulebuilds aMODULElibrary and the NDK links those with-Wl,--no-undefined, so the usual leave-Python-symbols-unresolved convention fails with a wall ofundefined symbol: PyExc_…. scikit-build-core shlex-splitsCMAKE_ARGS, so the-L/-lpair is comma-joined into the single-Wlargument.STABLE_ABIresolves on Apple but not under the NDK, so the platforms ship structurally different modules (soxr_ext.abi3.sovssoxr_ext.cpython-3XX-*.so). forge'sfix_wheelrewrites the tag and accepts both names — no handling needed, noted only because it is surprising when diffing two wheels.