scsynth-sys 0.1.0

Raw FFI bindings to a statically-linked SuperCollider scsynth engine.
docs.rs failed to build scsynth-sys-0.1.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

scsynth-sys

Raw FFI bindings to a statically-linked SuperCollider scsynth engine.

This crate builds libscsynth from a pinned SuperCollider revision, compiles a small C/C++ shim, and generates Rust bindings over the engine's extern "C" surface with bindgen. It is the only place unsafe FFI lives; safe abstractions belong in the scsynth crate.

Two completely different builds sit behind one binding surface, selected by target in build.rs:

  • native - CMake builds a static libscsynth against the system C/C++ toolchain, libsndfile and fftw, with our host-pumped external audio driver.
  • wasm (wasm32-unknown-unknown) - no system toolchain or sndfile/fftw exist, so the engine's DSP core + static UGen plugins are compiled from source against a from-source musl + libc++ (see wasm-toolchain/), driverless (mRealTime=false).

Where the source comes from

SuperCollider is not vendored in-tree. source.toml pins its { url, rev }, and build.rs resolves the source as: SCSYNTH_SYS_SUPERCOLLIDER_DIR if set (the Nix build and dev shell set it - they never fetch), otherwise a git fetch of that exact revision into a rev-keyed cache (~/.cache/scsynth-rs/, overridable with SCSYNTH_SRC_CACHE_DIR). The Nix flake reads the same source.toml, so the two cannot drift.

We fetch at build time rather than vendoring because SuperCollider's source (~135 MB) is far larger than crates.io's package-size limit - vendoring it would make this crate unpublishable. The cost: a first build needs git + network (or the env-var override; offline and docs.rs builds must use it).

How SuperCollider is modified

The fetched source is never touched: the build copies it into OUT_DIR and applies every change to the copy. Updating SuperCollider is therefore just bumping the rev in source.toml - the patches re-apply automatically (or fail loudly; see below). The changes are of two kinds: files we add, and small text patches to SC's own sources.

Files we add (csrc/)

File Purpose
SC_ExternalDriver.cpp The native custom backend: an SC_AUDIO_API_EXTERNAL audio driver that owns no device and no audio thread. The host (a cpal callback, a test, ...) pumps it one buffer at a time via scsynth_pump. Modelled on SuperCollider's externally-driven SC_WebAudio.cpp.
SC_WasmPump.cpp The wasm driverless shim (scsynth_wasm_new/perform/pump), plus the "fake implementations" that fill the link holes left by the SC files excluded on wasm - mirroring SC's own SC_WebAudio.cpp.
shim.cpp scsynth_default_world_options: constructs a default WorldOptions C++-side (bindgen can't reproduce its C++ default member initializers).
libc_gap.c The few libc symbols the minimal wasm libc doesn't provide.

Text patches to SC sources

Applied to the OUT_DIR copy via assert-guarded find-and-replace:

  • Native (patch_audioapi_external): patches server/scsynth/CMakeLists.txt to (1) accept external in the AUDIOAPI validation regex and (2) add one elseif(AUDIOAPI STREQUAL external) branch that compiles SC_ExternalDriver.cpp and defines SC_AUDIO_API_EXTERNAL. That is the entire native tweak - one CMake branch.

  • Wasm (patch_sc_sources): a handful of #ifndef SC_WASM guards so the engine links without the filesystem / networking / shared-memory code that doesn't exist on bare wasm:

    • SC_Endian.h - add a wasm branch (little-endian, no <netinet/in.h>);
    • SC_ReplyImpl.hpp - drop the boost::asio address field;
    • SC_World.cpp - guard <filesystem> and the never-called World_LoadGraphDefs;
    • SC_GraphDef.cpp - guard the <filesystem> /d_load file-loaders.

    Each guard extends a conditional SuperCollider already has for __EMSCRIPTEN__; we just fire it on our SC_WASM define as well.

Build steps (per target, in build.rs)

  1. copy_tree - copy the fetched source into OUT_DIR (skipping .git).
  2. sync_file - drop SC_ExternalDriver.cpp into the copy (native).
  3. patch_audioapi_external / patch_sc_sources - the text patches.
  4. generate_version_header - produce SC_Version.hpp (wasm only; CMake does this natively).
  5. Build from the copy - CMake (native) or the cc crate (wasm).

Why this survives SuperCollider updates

Every replacement first assert!s that its anchor text is present. If an upstream change moves or renames an anchor, the build fails immediately with a precise message (e.g. "...SuperCollider CMake layout changed", "...guard anchor not found in ") instead of silently mis-building. A routine SC bump just works; a breaking one points you at the exact patch to update.

Plugins (UGens)

Both targets build with STATIC_PLUGINS, so the available UGens are exactly those registered by SuperCollider's static-plugin load list (the #ifdef STATIC_PLUGINS block in SC_Lib_Cintf.cpp): the core DSP set - IOUGens, OscUGens, DelayUGens, BinaryOp/UnaryOp/MulAdd, FilterUGens, GendynUGens, LFUGens, NoiseUGens/DynNoiseUGens, GrainUGens, PanUGens, ReverbUGens, TriggerUGens, PhysicalModelingUGens, TestUGens, DemandUGens, and the FFT/PV chain (FFT_UGens + PV_UGens + PartitionedConvolution).

The wasm build compiles and registers this same set; the only intentional difference is DiskIO (DiskIn/DiskOut/VDiskIn), which native registers through libsndfile but wasm omits (no filesystem/libsndfile on bare wasm). So web and native expose an identical UGen set apart from disk I/O.

This is SuperCollider's static set, which is narrower than a full desktop install's dynamically loaded plugins: UGens that SuperCollider ships only as separately-loaded plugins - ChaosUGens, the MIR/analysis set (MFCC, BeatTrack, KeyTrack, Loudness, Onsets, ...), the PV third-party onset detectors, UnpackFFTUGens and DemoUGens - are compiled by the native CMake build but not registered by the static load list, so they are unavailable on both targets (a SynthDef that references one fails to load with UGen '<name>' not installed). Exposing them would mean extending that load list (a patch applied to both targets) and is intentionally out of scope.

Pinning

The SuperCollider revision lives in source.toml - the single source of truth, shared with the Nix build (which reads the same file). Bump rev there to update; the build-time patches re-apply automatically.