recipes: soundfile 0.14.0 + libsndfile and its codec chain - #121
Merged
Merged
Conversation
soundfile is a pure-Python cffi wrapper over libsndfile, so the recipe is Pattern H: a shared flet-libsndfile that the wrapper dlopens, plus a loader patch. Upstream's loader branches on sys.platform over darwin/win32/linux only and both fallbacks end in a bare `raise`, so `import soundfile` fails outright on android and ios -- confirmed on an emulator against the PyPI wheel. The patch wraps ctypes.util.find_library rather than rewriting the loader. cffi's ffi.dlopen is a raw dlopen(3) and, unlike ctypes.CDLL, cannot dereference the .fwork text pointer serious-python leaves when it relocates a library into an iOS framework (that dereference lives in iOS CPython's patched ctypes). So the shim lets ctypes resolve the name and hands cffi the path ctypes settled on, trying the bare soname (Android jniLibs), opt/lib/libsndfile.fwork (iOS) and opt/lib/libsndfile.so in turn. Everything downstream of find_library is upstream's, unchanged. flet-libsndfile is built with FLAC, Ogg Vorbis, Opus and MP3 compiled in, so the container list on a phone matches a desktop's. The six codec libraries are separate recipes built as static PIC archives and declared host_build, not host: they are absorbed into the one shared library, so promoting them would make every consuming app download six wheels whose contents it already has. iOS hand-links the shared image from the static build (CMake would emit a versioned dylib triplet, which serious-python's first-dot framework naming cannot represent) and restricts the export list to sf_*, matching what libsndfile's own version script already does on Android. Three fixes upstream would want back: - flet-libflac/patches/android-api-level.patch -- FLAC reads the Android API level from CMAKE_SYSTEM_VERSION, which the NDK toolchain pins to 1, so it disables fseeko at every level and armeabi-v7a then fails to compile against bionic's 64-bit-off_t declaration. - flet-libsndfile/patches/security-backports.patch -- 1.2.2 is three years old and still the only release; five heap-overflow and over-read fixes are cherry-picked from master. A mobile app hands libsndfile whatever file a user picked, so these are reachable. - FLAC's ENABLE_MULTITHREADING is off: it puts Threads::Threads in libFLAC's exported CMake target, which libsndfile cannot resolve. Full matrix green on both platforms for 3.12 and 3.14, and the consumer example round-trips all seven containers on an Android emulator.
…t's 3.14 default [skip ci] Three findings from the soundfile chain, each one a wasted cycle if unrecorded. new-mobile-recipe gains a cffi-ABI-mode row in the shape table and a deep-dive: a package whose wrapper is `ffi.dlopen` rather than `ctypes.CDLL` still needs a recipe even though nothing compiles, and its loader patch has to resolve the iOS .fwork through ctypes because cffi's dlopen cannot. The deep-dive also records the static-PIC-plus-host_build arrangement for a library with optional codecs, and the -headerpad flag a hand-linked iOS image needs and CMake adds for free. forge-error-catalogue gains the matching runtime entry and a build-time one: a CMake project reading the Android API level from CMAKE_SYSTEM_VERSION gets 1 under the NDK toolchain file, which silently disables fseeko and breaks only the 32-bit slices. local-recipe-testing's "match flet's python" gotcha was written when flet bundled 3.12; 0.86.5 bundles 3.14, and for a pure-Python recipe the mismatch produces no error at all -- pip just installs PyPI's unpatched wheel.
…lame mirror [skip ci]
CI run 34892127701 surfaced two things.
soundfile's file-object path is permanently unavailable on iOS. Virtual I/O hands
libsndfile a `ffi.callback()`, cffi writes that trampoline at runtime, and iOS
refuses write+execute pages to an app without the JIT entitlement:
MemoryError: Cannot allocate write+execute memory for ffi.callback().
Only the file-object form is affected — paths and integer file descriptors
(`sf_open_fd`) use no callbacks and were fine in the same run, 15 of 16 tests
passing on the simulator and 16 of 16 on the emulator. So it is documented rather
than worked around: the README leads the reading section with "a real path is the
only form that works everywhere" and gives the write-a-file replacement, and the
tests assert the MemoryError on iOS instead of skipping, so a future cffi or OS
change that lifts it does not go unnoticed. The example app wrote every container
through io.BytesIO and would have failed outright on iOS; it now writes real files
under FLET_APP_STORAGE_TEMP.
flet-libmp3lame moves off downloads.sourceforge.net, which bounces through a
randomly chosen mirror and timed out its TLS handshake on two of six legs. Debian's
pool serves the same 1524133 bytes, sha256 ddfe36ca…1da1e.
…kip ci] encode_all() took no arguments and closed over nothing its inner work() did not already close over, so the nesting bought a level of indentation and nothing else. Flatten it: the body becomes encode_all itself, and the two call sites say page.run_thread(encode_all) — which also reads more honestly than a bare call, since "this goes to a worker thread" is the thing a reader needs to know here. Nesting of this shape is worth keeping only when the outer function takes a parameter the inner one closes over, the way soxr's resample_to(rate) does. Verified on the emulator: first sweep on load, then a tap on Re-encode re-runs it with no output on console.log.
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
soundfile— the library nearly every scientific-Python audio pipeline opens with — plus the seven native recipes it needs. Answers flet-dev/flet#6834.Why it needs a recipe at all
soundfileships no compiled code:soundfile_build.pycallsffibuilder.set_source("_soundfile", None), so it is cffi ABI mode — a pure-Python wrapper thatdlopens libsndfile. It still cannot work unpatched, because its loader branches onsys.platformoverdarwin/win32/linuxand both fallbacks end in a bareraise. Confirmed against PyPI's own wheel on an emulator before touching anything:What's here
Eight recipes.
flet-libogg1.3.6,flet-libvorbis1.3.7,flet-libflac1.5.0,flet-libopus1.5.2,flet-libmpg1231.33.7 andflet-libmp3lame3.100 are built as static PIC archives and declaredrequirements.host_buildofflet-libsndfile1.2.2, which absorbs them into a single shared library. They are deliberately notrequirements.host: nothing of them is loaded at runtime, so promoting them would make every consuming app download six wheels whose contents already live insidelibsndfile.so. Only that one library ships — 1.9–2.7 MB per Android ABI, 2.9–3.2 MB per iOS slice — andsoundfileitself is a 21 KB pure-Python wheel.The result is that FLAC, Ogg Vorbis, Opus and MP3 all work, so
sf.available_formats()on a phone returns the same list as on a desktop.The loader patch is the interesting part.
ffi.dlopenis a rawdlopen(3)and, unlikectypes.CDLL, cannot dereference the.fworktext pointer serious-python leaves when it relocates a library into an iOS framework — that dereference lives only in iOS CPython's patchedctypes/__init__.py. Sopatches/mobile.patchwrapsctypes.util.find_library, lets ctypes resolve the name, and hands cffi the absolute path ctypes settled on (CDLL(candidate)._name), trying the bare soname (Android jniLibs),opt/lib/libsndfile.fwork(iOS) andopt/lib/libsndfile.soin turn. Everything downstream offind_libraryis upstream's, untouched.Three smaller decisions worth a reviewer's eye:
flet-libflac/patches/android-api-level.patch— FLAC reads the Android API level fromCMAKE_SYSTEM_VERSION, which the NDK toolchain pins to1. It therefore disablesfseekoat every API level, andcompat.h's#define fseeko fseekthen collides with bionic's 64-bit-off_tdeclaration on armeabi-v7a only. Passing-DCMAKE_SYSTEM_VERSIONdoes not help — the toolchain shadows it with a non-cacheset().flet-libsndfile/patches/security-backports.patch— 1.2.2 (August 2023) is still the only libsndfile release, and ~80 commits have landed since. Five heap-overflow and over-read fixes are cherry-picked from master, all clean. A mobile app hands libsndfile whatever file a user picked, so these are reachable.-Wl,-exported_symbol,'_sf_*'so the absorbed codec symbols stay hidden, matching what libsndfile's own version script already does on Android. 41 exported symbols per iOS slice.One platform limitation, documented not worked around
sf.read(io.BytesIO(...))raisesMemoryErroron iOS. Virtual I/O hands libsndfile affi.callback(); cffi writes that trampoline at runtime and iOS refuses write+execute pages without the JIT entitlement. Only the file-object form is affected — paths and integer file descriptors (sf_open_fd) use no callbacks and pass. The README leads its reading section with "a real path is the only form that works everywhere" and gives the write-a-file replacement, and the tests assert theMemoryErroron iOS rather than skipping, so a future cffi or OS change that lifts it does not go unnoticed.Validation
CI run 34905677660 — 6/6 jobs green, both platforms × Python 3.12 / 3.13 / 3.14, with on-device tests on the 3.12 and 3.14 legs. Each of the four test legs: 17 passed, 1 skipped,
EXIT 0(the skip is the other platform's arm of the virtual-I/O split). 18 cases from 9 test functions, one parametrized across ten containers.Wheel hygiene checked per slice: correct
Machineper ABI,SONAMEexactlylibsndfile.so,DT_NEEDEDlimited tolibc/libm/libdl, every AndroidLOADsegment aligned0x4000; iOS filetypeDYLIB,otool -Lshowing onlylibSystem,LC_BUILD_VERSIONplatform 2 on device and 7 on the simulators.soundfile's METADATA promotesflet-libsndfile (==1.2.2)and none of the six codec libraries.Locally, the consumer example round-trips all seven containers on an Android emulator. The iOS half was never runnable on this machine (Xcode has no iOS platform component installed), so iOS rests on the CI run above.
Consumer notes
soundfilereads and writes audio files as numpy arrays and is the front door for librosa-, wfdb- and pywavelets-shaped pipelines. With this build a Flet app gets WAV, AIFF, AU, CAF/ALAC, W64, RF64, FLAC, Ogg Vorbis, Opus and MP3 on both platforms, from a singledependencies = ["soundfile"].Details, the storage and threading guidance, the iOS file-object limitation and the traps worth knowing are in recipes/soundfile/README.md, with a runnable app in recipes/soundfile/examples/codec-roundtrip.
Also in this branch
.claude/skills/picks up the three findings: the cffi-ABI-mode recipe shape innew-mobile-recipe, the cffi/.fworkandCMAKE_SYSTEM_VERSIONentries inforge-error-catalogue, and a correction inlocal-recipe-testing—flet build0.86.5 bundles Python 3.14, and for a pure-Python recipe a cp312-only build produces no error at all, it just silently loses to PyPI's unpatched wheel.