yse 0.2.0 copy "yse: ^0.2.0" to clipboard
yse: ^0.2.0 copied to clipboard

Dart bindings for the YSE sound engine — 3D audio playback, mixing, DSP, MIDI, and generative music.

yse #

Dart bindings for the YSE sound engine — 3D audio playback, mixing, DSP, MIDI, and generative music. Runs on Windows x64, Linux x64 and Android (arm64-v8a, x86_64); the engine is downloaded and bundled by the package's build hook, so there is nothing else to install.

Quick start #

dart pub add yse        # Dart app
flutter pub add yse     # Flutter app
import 'package:yse/yse.dart';

void main() {
  final sys = System.instance;
  sys.init(); // throws YseException when no audio device opens

  final sound = Sound.fromFile('drone.ogg', loop: true);
  sound.play();

  // Pump the engine for 3 seconds.
  for (var i = 0; i < 180; i++) {
    sys.update();
    sys.sleep(16);
  }

  sound.dispose();
  sys.close();
}

dart run it, or run example/main.dart, which plays a generated tone and needs no sound file. System.startUpdateTimer() wraps the update loop in a Timer.periodic if you prefer event-driven code.

  • Dart ≥ 3.10 (Flutter ≥ 3.38): build hooks are stable from there on.
  • Android: set your app's minSdk to 26; see Android.
  • Linux: install the runtime libraries the engine links: sudo apt-get install -y libportaudio2 libsndfile1 librtmidi6 (Ubuntu 24.04 package names).
  • Windows, one program at a time: while a program that uses yse is running, another dart run or dart test in the same project fails with PathAccessException ... .dart_tool\lib\...dll. The Dart SDK re-copies the bundled DLLs on every start and Windows locks the ones in use. Stop the running program first.

Full documentation, tutorials and the API reference: https://yvanvds.github.io/dart-yse/.

Status #

Pre-release. v0.x supports Windows, Linux, and Android; iOS / macOS have no engine release yet.

0.2.0 binds libYSE v3.1.0 (0.1.0 bound libYSE 2.4). This is a breaking upgrade: System.init() now throws when no audio device opens, the default session rate is 48 kHz, DspBuffer subclasses are real classes, patcher IDs are int? and the patcher's bus slots moved under patcher.. The upgrade guide lists every change with before / after code, and the changelog has the full release notes.

The full C++ API is wrapped except for one upstream-deferred area:

  • Custom DSP sources (dspSourceObject subclassing) and the callback-based YSE::io VFS — these need audio-thread-safe callback plumbing that lands in a later release. BufferIO covers the common asset-pack use case in the meantime.

How the engine gets into your app #

The package's native-assets build hook (hook/build.dart) downloads the prebuilt engine its bindings were generated from (libYSE v3.1.0) from the engine's GitHub release, checks it against the sha256 digest embedded in the package, and bundles it into your app. dart run, dart test, dart build cli and flutter run / flutter build windows|linux|apk|appbundle all pick it up; on Windows the hook also bundles the engine's runtime DLLs (libc++, PortAudio, RtMidi, libsndfile and its codecs) next to libyse.dll.

  • Network once: the first build of a project downloads the engine (~9 MB on Windows, ~3 MB on Linux, ~58 MB for Android, whose asset carries the libsndfile sources for LGPL compliance) into .dart_tool/; later builds, offline ones included, reuse it. flutter clean deletes .dart_tool/, so the next build downloads again.
  • No second package is needed on Android (the former yse_flutter_libs plugin is deprecated and not published).

To run against another engine build (a source build, an unreleased engine), point YSE_DLL_PATH at the directory holding libyse.dll / libyse.so; it takes precedence over the bundled copy. System.version reports the library actually loaded.

Android #

flutter pub add yse

is all a Flutter Android app needs (#96). The build hook maps the target ABI to the engine's prebuilt Android release, libyse-<tag>-android.tar.gz, downloads it once per project, checks its sha256 and reports lib/<abi>/libyse.so as the code asset package:yse/libyse. Flutter packages it into the APK / AAB under lib/<abi>/ and the app's Android build strips its debug symbols (about 6 MiB per ABI in a release APK). The library links Oboe, libsndfile and the C++ runtime statically and needs only system libraries (libc, libm, libdl, liblog, libOpenSLES), so no libc++_shared.so or other library is bundled with it. No NDK or CMake build of the engine is involved.

The separate yse_flutter_libs plugin that used to package the engine (#91) is deprecated and will not be published: an app that still lists it keeps building (from 0.3.0 it packages nothing), but should drop it. See its README for the migration.

Offline builds, unreleased engines, 32-bit ABIs. The hook's engine_dir user-define replaces the download with a local folder laid out like an unpacked libyse-<tag>-android release, i.e. holding lib/<abi>/libyse.so. Set it in the app's pubspec.yaml (a relative path resolves against that file):

hooks:
  user_defines:
    yse:
      engine_dir: /path/to/libyse-android

For an offline build, dart run tool/fetch_engine.dart --platform android in a dart-yse checkout unpacks the pinned release into .engine/<tag>/libyse-<tag>-android/. For an unreleased engine or armeabi-v7a (which the release does not ship), build libyse.so per ABI from an engine checkout with the NDK, as the engine's release workflow does, and copy each into lib/<abi>/:

cmake -B build-android-arm64 -G Ninja \
  -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
  -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-26 \
  -DCMAKE_BUILD_TYPE=Release
cmake --build build-android-arm64 --target yse   # -> build-android-arm64/bin/libyse.so

With engine_dir set, the 64-bit ABIs must be in the folder; a 32-bit ABI it lacks just gets no engine, as with the release.

Requirements:

  • Flutter ≥ 3.38 (Dart 3.10), which runs build hooks for Android
  • minSdk 26 so Oboe negotiates the AAudio backend (older devices fall back to OpenSL ES, which the engine still supports through Oboe but has fewer guarantees on latency). Flutter's app template defaults lower, so set it in your app's build.gradle.
  • ABIs: arm64-v8a, x86_64, the ones the engine's Android release ships. On a 32-bit device without an engine_dir build, System throws a YseException saying the engine is not in the app.

A minimal end-to-end Flutter sample lives at example/android_sample/ — see the README there for the one-time flutter create scaffold and run instructions.

Permissions. Oboe playback needs no runtime permission. Add RECORD_AUDIO to your manifest only if you exercise an input-capture code path (none is exposed by package:yse today).

Threading. The audio callback runs on a thread owned by Oboe / AAudio. As with desktop builds, never call into yse from a callback you didn't install, and keep all yse.* calls on the same isolate that called System.init(). On Flutter, that is typically the UI isolate.

Render threads. The engine renders with physical cores - 1 worker threads (at most 8) by default. On low-end devices set System.instance.renderThreads = 0 before init() to render on the audio thread alone (the output is bit-identical); the Android sample does this.

What's wrapped #

Subsystem Coverage Notes
System Device and offline sessions, requestedSampleRate, headless speaker layout, device listing without a stream (refreshDevices), auto-reconnect with outcome (reconnectState, activeDevice), render threads, block size, RNG seeding, occlusion callback, MIDI device enumeration, global reverb, underwater FX, update loop, version / CPU load full
DomainClock / ClipTransport Named, rampable beat clocks; beat-timed clips played on the audio thread into synths and MIDI ports full
Listener Position, velocity, orientation full
Sound File / buffer / multichannel buffer / patcher / synth sources, transport, 3D, occlusion, mixing, DSP-chain attachment, busName full
Channel Pre-built singletons, user channels, aux sends and return buses, insert DSP, peak metering, virtualisation, reverb attach, busName full
Reverb Positioned + global, presets, full parameter surface, preset values + morph (ReverbPresetValues) full
Device Read-only descriptors + owned DeviceSetup builder full
DspBuffer DspBuffer / DrawableBuffer / FileBuffer / Wavetable subclasses, bulk Float32List I/O, drawing, load/save, buffer math, copy/swap, band-limited wavetable generators; DspMultiBuffer for multichannel sources full
DspObject 11 base effects (lowpass, highpass, bandpass, sweep, basicDelay + lp/hp variants, phaser, ringMod, difference, granulator) with the inherited bypass/impact/LFO surface, chain walking; mix-grade Compressor, ParametricEq, Chorus, PlateReverb, FeedbackDelay, MorphingReverb, Underwater; patcher inserts full
Patcher Max/MSP-style graph: all 305 objects as Obj constants, connect, names and patcher.<name>.<slot> bus slots, subpatchers, messages stream (the C++ oscHandler), GUI value protocol, JSON round-trip, full PHandle and registry introspection full
Synth Sine / virtual-analog / FM (DX7 banks from file or memory) / SFZ sampler voice pools, per-note 3D positions, busName full minus audio-thread hooks (note-rewrite callback, custom voices)
Bus Host publish, exact-address subscribe, prefix tap full
MidiFile Load / play / pause / stop, synth sink full
MidiOut Note / pressure / control / program / raw3 + raw (any length, SysEx) / reset / all-notes-off full (Windows/Linux)
MidiIn Raw and parsed message streams, synth sink full (Windows/Linux)
Note/PNote/Scale/Motif Pitch/volume/length/channel, position, has/nearest/clear, transpose, setFirstPitch full
Player Generative sequencer on a Synth: pitch / velocity / gap / length ranges, voices, scale (with cross-fade), weighted motifs full
LiveCoding Submit scripts to the embedded Python interpreter, error stream full (needs YSE_ENABLE_PYTHON=ON)
Log sendMessage, level, logfile, broadcast messages Stream full
BufferIO Register Uint8List bytes under an ID; load by ID full

example/main.dart is the minimal program; 13 more demos under example/ are ports of the C++ demos shipped with yse-soundengine, plus a few extras (demo19_bus.dart, demo20_clocks_clips.dart, live coding, log + buffer I/O).

Threading #

  • All Dart calls must come from the same isolate that called System.init(). The engine maintains internal queues per-thread.
  • The audio callback runs on a private thread the wrapper never exposes — never call into yse from a callback you didn't install.
  • System.update() must be called from that isolate every frame (or use startUpdateTimer). It drives message delivery, listener-velocity computation, the occlusion callback, bus delivery and the auto-reconnect watchdog.
  • Engine callbacks reach Dart either synchronously inside update() (occlusion) or as stream events (Bus, Patcher.messages, Log.messages, MidiIn). The docs' mental model page has the per-callback table.

License #

Eclipse Public License 2.0 — matches the upstream YSE license.

Working on dart-yse #

The rest of this README is for working on dart-yse itself, from a clone of https://github.com/yvanvds/dart-yse.

The engine submodule #

third_party/yse-soundengine is an ordinary git submodule pinned to an engine release tag. The C headers ffigen reads, the patcher metadata the drift tests compare against, and the docs data all come from that pinned tree, so a local checkout sees exactly what CI sees.

  • Get it: clone with --recurse-submodules (as below), or run git submodule update --init in an existing clone.

  • Bump it: check out the new release tag inside the submodule and commit the gitlink:

    git -C third_party/yse-soundengine fetch --tags
    git -C third_party/yse-soundengine checkout v3.2.0   # the new tag
    git add third_party/yse-soundengine
    git commit -m "chore(infra): bump libYSE to v3.2.0"
    

    Then regenerate the bindings (dart run ffigen --config ffigen.yaml), set the new tag and its asset digests in lib/src/engine_release.dart (what the build hook downloads), and rerun the tests, test/engine_release_test.dart included. Committed bindings always come from the pinned tag's headers; never commit bindings generated against an unreleased engine (see Unreleased engine).

  • Engine development happens in a separate clone of yse-soundengine, not through the submodule. Keep the submodule on its release tag; git status should show it clean.

Two upstream CMake options matter to the wrapper: -DYSE_ENABLE_PYTHON=ON (default OFF, desktop only) embeds the interpreter behind LiveCoding, and YSE_ENABLE_MIDI_DEVICE (default ON on Windows / Linux) builds the RtMidi backend behind MidiIn / MidiOut and the MIDI-device patcher objects.

Prebuilt engine (default) #

In a checkout, too, dart test and dart run use the release the build hook bundles, with no setup: clone with the submodule, dart pub get, dart test. The pin the hook reads is lib/src/engine_release.dart (tag plus one sha256 per release asset), the copy of the submodule pin that ships in the published package; test/engine_release_test.dart fails when the two drift or the digests differ from the release's own. Bump them together.

To name the engine explicitly with YSE_DLL_PATH instead, or to prepare an offline Android build, fetch the release yourself. The engine publishes libyse binaries for every release tag. tool/fetch_engine.dart reads the tag the submodule pins (git describe --tags --exact-match), downloads that release's asset for your platform from https://github.com/yvanvds/yse-soundengine/releases, checks it against the sha256 digest GitHub reports for it, and unpacks it into the gitignored .engine/<tag>/. A tag that is already there is not downloaded again (--force re-fetches). It needs only the Dart SDK, git and tar (built into Windows 10+ and Linux); no gh, MSYS2 or CMake. It fails if the pin is not a release tag.

# Windows
git clone --recurse-submodules https://github.com/yvanvds/dart-yse
cd dart-yse
dart pub get
dart run tool/fetch_engine.dart
$env:YSE_DLL_PATH = "$PWD\.engine\v3.1.0\libyse-v3.1.0-windows-x64\bin"
dart test
# Linux (x86_64). Runtime libraries only, no build toolchain:
sudo apt-get install -y libportaudio2 libsndfile1 librtmidi6

git clone --recurse-submodules https://github.com/yvanvds/dart-yse
cd dart-yse
dart pub get
dart run tool/fetch_engine.dart
export YSE_DLL_PATH="$PWD/.engine/v3.1.0/libyse-v3.1.0-linux-x64/lib"
dart test

The tool prints the exact YSE_DLL_PATH for the pinned tag as its last line. The Windows bin/ holds libyse.dll and all its runtime DLLs. The release builds use the default options: MIDI device I/O on, Python (LiveCoding) off. The C headers ffigen reads still come from the submodule.

--platform android fetches the Android asset instead (one lib/<abi>/libyse.so per ABI), for an offline Android build through the build hook's engine_dir user-define; see Android.

The loader looks for the engine in this order: YSE_DLL_PATH, the release the build hook bundled, then the submodule's source build in third_party/yse-soundengine/build/bin (only reached where the hook bundles nothing). It does not look in .engine/ by itself: that folder exists only in a dart-yse checkout, never in a published package, so using it means saying so with YSE_DLL_PATH. To try an engine that has no release yet, see Unreleased engine.

Building the engine from source #

Use this for a platform without a prebuilt asset, or to change build options such as -DYSE_ENABLE_PYTHON=ON.

Windows

Requires MSYS2 Clang64 (the same toolchain YSE itself uses) and CMake.

# Clone with the submodule.
git clone --recurse-submodules https://github.com/yvanvds/dart-yse
cd dart-yse

# Build libyse.dll once.
cmake -S third_party/yse-soundengine -B third_party/yse-soundengine/build -G Ninja `
      -DCMAKE_BUILD_TYPE=Release `
      -DCMAKE_C_COMPILER=C:/msys64/clang64/bin/clang.exe `
      -DCMAKE_CXX_COMPILER=C:/msys64/clang64/bin/c++.exe
cmake --build third_party/yse-soundengine/build --target yse

# Install the Dart dependencies.
dart pub get

At runtime point YSE_DLL_PATH at the directory containing libyse.dll; left unset, the loader takes the release the build hook bundles instead:

$env:YSE_DLL_PATH = "$PWD\third_party\yse-soundengine\build\bin"
dart run example/hello_sound.dart

(Or copy that directory's contents next to your Dart executable.)

Linux

Tested on Ubuntu 24.04. Requires CMake ≥ 3.20, Ninja, GCC or Clang, and the system development packages that YSE links against.

sudo apt-get install -y \
  build-essential cmake ninja-build pkg-config \
  portaudio19-dev libsndfile1-dev librtmidi-dev

git clone --recurse-submodules https://github.com/yvanvds/dart-yse
cd dart-yse

# Build libyse.so once.
cmake -S third_party/yse-soundengine -B third_party/yse-soundengine/build -G Ninja \
      -DCMAKE_BUILD_TYPE=Release
cmake --build third_party/yse-soundengine/build --target yse

dart pub get

At runtime point YSE_DLL_PATH at the build output; left unset, the loader takes the release the build hook bundles instead:

export YSE_DLL_PATH="$PWD/third_party/yse-soundengine/build/bin"
dart run example/hello_sound.dart

The upstream CMake build embeds $ORIGIN in the .so's RPATH, so sibling shared libraries are discovered automatically — no LD_LIBRARY_PATH setup needed.

Unreleased engine (opt-in) #

dart-yse is also where engine changes get tried before they are tagged. To do that, build your own clone of yse-soundengine (not the submodule, which stays on its release tag) and point YSE_DLL_PATH at its output:

# Windows, with the dev clone built at D:\yse-soundengine\build
$env:YSE_DLL_PATH = "D:\yse-soundengine\build\bin"
dart test
# Linux
export YSE_DLL_PATH="$HOME/src/yse-soundengine/build/bin"
dart test

This mode is only ever entered on purpose; nothing in the default flow reads or builds a dev clone. While it is active, every engine test suite prints an UNRELEASED ENGINE banner in its setup output, naming the pinned tag and the library actually loaded. The banner appears when System.version differs from the pinned tag (3.1.0 for v3.1.0) or when the library comes from anywhere other than the build hook's bundled release, .engine/<tag>/ or the submodule's own build/bin (libYSE's version string only changes at release time, so a dev build just after a tag still reports that tag). It never fails a test. Expect snapshot and drift tests to fail when the dev engine has changed what they check; CI always runs the pin.

On Android the same mode is the build hook's engine_dir user-define pointing at libyse.so files built from the dev clone (see Android). Leave it out of anything you commit.

To try new C API functions you can regenerate the bindings against the dev clone's headers (point the headers.entry-points path in ffigen.yaml at the clone's yse_all.h), but only locally: committed bindings, and ffigen.yaml itself, must stay on the pinned tag's headers in the submodule. When the engine change is released, bump the pin to the new tag, regenerate, and run dart run tool/fetch_engine.dart.

Linux validation from Windows (optional) #

tool/ci-linux/Dockerfile builds an image with the engine deps and the Dart SDK so you can reproduce the Linux build from a Windows dev box without rebooting:

docker build -t dart-yse-ci -f tool/ci-linux/Dockerfile .
docker run --rm -v ${PWD}:/workspace dart-yse-ci

The default command configures + builds libyse.so into .docker-build/yse/bin/ (a .gitignored directory inside dart-yse — keeps Docker artefacts out of the Windows build/ tree and out of the submodule, so neither toolchain trips on stale CMakeCache entries left by the other), runs dart analyze, and exercises the FFI surface via tool/linux_smoke.dart (initOffline → renderOffline; no audio device required).

0
likes
160
points
0
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Dart bindings for the YSE sound engine — 3D audio playback, mixing, DSP, MIDI, and generative music.

Homepage
Repository (GitHub)
View/report issues

Topics

#audio #sound #midi #dsp #ffi

License

EPL-2.0 (license)

Dependencies

code_assets, crypto, ffi, hooks

More

Packages that depend on yse