yse 0.2.0
yse: ^0.2.0 copied to clipboard
Dart bindings for the YSE sound engine — 3D audio playback, mixing, DSP, MIDI, and generative music.
Changelog #
0.2.0 #
Binds libYSE 3.1.0. First release published to pub.dev.
Engine bumped to libYSE v3.1.0 (from v2.4.0-3); bindings regenerated
(#55). v3.1.0 is v3.0.1 plus upstream #1022 (bandPass unity gain),
#1026 (device enumeration without a stream), #1027 (auto-reconnect to the
opened device, with an outcome API) and #1028 / #1029 (device callback
fixes). The 3.0 C ABI is not compatible with 2.4, and the engine changed
several behaviours without changing a signature, so this is a breaking
release. The new docs page Upgrading to package:yse 0.2.0
(docs/source/intro/upgrading.rst, published at
https://yvanvds.github.io/dart-yse/intro/upgrading.html) gives the old and
new Dart code for every change below (#70).
The engine comes with the package #
- Native-assets build hook (#95).
hook/build.dartdownloads the prebuilt libYSE v3.1.0 release for Windows x64 and Linux x64, checks it against the sha256 digest embedded in the package (lib/src/engine_release.dart, tested against the submodule pin and the release), and bundles it as a code asset, sodart pub add ysefollowed bydart run,dart test,dart build cliorflutter runneeds no manual engine setup. On Windows the engine's runtime DLLs are bundled next tolibyse.dll. The download is cached under.dart_tool/; later builds work offline. Linux needs the PortAudio, libsndfile and RtMidi runtime packages. - Android through the build hook (#96). The hook also bundles the
engine's Android release: for an arm64-v8a or x86_64 target it reports
that ABI's
lib/<abi>/libyse.so, which Flutter packages into the APK / AAB, soflutter pub add yseis the whole Android setup (minSdk26). The library links Oboe, libsndfile and the C++ runtime statically, so nothing else is bundled. The ~58 MB Android asset is downloaded once per project into.dart_tool/hooks_runner/shared/yse/. On Android the loader resolves the engine through its code asset id (package:yse/libyse) and says which ABIs are covered when the app has none. The hook'sengine_diruser-define (hooks: user_defines: yse: engine_dir:in the app's pubspec) bundles a local release-shaped folder instead, for offline builds, unreleased engines and 32-bit ABIs. yse_flutter_libsis deprecated and not published (#96). It never reached pub.dev and is no longer needed; 0.3.0 packages nothing, so apps that still list it keep building.android/engine.propertiesand its pin test are gone.example/main.dart(#99): the smallest complete program, playing a generated tone through the bundled engine with no sound file and noYSE_DLL_PATH.- Requires Dart 3.10 (Flutter 3.38), where build hooks are stable; the
SDK constraint moved from
^3.8.0to^3.10.0. New dependencies:hooks,code_assetsandcrypto. - Loader order is now
YSE_DLL_PATH, then the bundled release, then the submodule's source build (third_party/yse-soundengine/build/bin). In a dart-yse checkout a source build is therefore only used whenYSE_DLL_PATHnames it. The unreleased-engine banner (#89) treats the bundled release as pinned.
Breaking changes #
System.init()throws when no device opens (#56). It throwsYseException(YSE_ERR_AUDIO_DEVICE) and leaves the engine closed when there is no default output device or it refuses to open or start; up to libYSE 2.4 it returned normally with nothing open. Fall back toinitOffline().System.openDevice()throws when the engine opened nothing (#56, closes #52): no output device set, a stale device (Device.id-1 or no longer resolving), a stream error (including a device that refuses the session rate), or an offline session without a backend. The running stream and speaker layout are left untouched. It used to return normally.- 48 kHz default session rate (#56, closes #53). The default is now
48 kHz (was 44.1 kHz), so offline renders that assumed 44.1 kHz change
length; choose the rate with the new
System.requestedSampleRate.DeviceSetup.sampleRatecannot change a running session's rate.DspObject.granulator's defaultpoolSizeis 5 s at 48 kHz (48000 * 5, was44100 * 5). System.setAutoReconnect(#54, #56):delayMsis wall-clock milliseconds (libYSE 2.4 compared it against a count ofupdate()ticks), with a 500 ms start-up grace. The watchdog now reopens the device of the last successfulopenDevice()(found again by name and host, with its channel count and buffer size) instead of the default output device; a session that never calledopenDevice()still reopens the current default.System.resume()reopens the same device and never falls back to another one, and is a no-op on an offline session.DspBuffersubclasses are real Dart subclasses (#55).DspBuffer.plainstays;DspBuffer.drawable/.file/.wavetableare replaced byDrawableBuffer(...),FileBuffer(...)andWavetable(...). The 3.0 C API no longer checks the subclass (itstatic_casts), so calling e.g.drawLineon a plain buffer was undefined behaviour; the type system now rules it out.- Patcher IDs (#55):
PHandle.id,PHandle.connectionTargetIdandPHandle.connectionTargetInletreturnint?, withnullfor the engine'sYSE_PATCHER_ID_NONE/YSE_PATCHER_INLET_NONEsentinels. Patcher IDs now start at 0 and are reused after a delete. Patch-file IDs are per patcher from 0, and the patcher name is saved in the JSON (#64). Patcher()andPatcher.parseJsonthrowYseExceptionon failure (#55).parseJsonadds to the existing patch (callclear()first to replace it) and rolls back a failed load.- Patcher bus slots carry the
patcher.prefix (#50, #55): a patcher namedfoopublishes its.sslots topatcher.foo.<slot>and its.rslots hearpatcher.foo.<slot>(libYSE 2.4:foo.<slot>). Obj.dOut(~out) is removed (#55); no engine object ever implemented it. Every other existingObjname is kept.PCategorygains the 11 new 3.0 registry categories (#55), so an exhaustiveswitchover it needs the new cases.PatcherObjectType.descriptionis now a tooltip-length summary (#64).Player(Synth synth)(#85). Since engine v2.3.0yse_player_createtakes the synth the player plays its notes on and returns NULL without one; the wrapper passednullptr(since #22), soPlayer()always threwYseExceptionand the class was unusable.- Late voice groups throw (#81, upstream yse-soundengine#1177).
Synth.addSineVoices/addVaVoices/addFmVoices/addSamplerVoicesandSynth.setPositionHandlerthrowStateErroroncevoiceCountis non-zero; the engine drops these calls after the voice pool is built and only logs a warning. DspObjectkind-specific accessors check the kind (#72).q, thesweep*accessors,setDelayTap/delayTime/delayGain,phaserRange,differenceAmplitudeand every granulator accessor throwYseExceptionon the wrong kind before calling native code, where they used to write through a mistyped pointer.System.close()closes bus streams (#59): it releases everyBussubscription and tap and closes their streams. Before, a tap stream went silent but stayed cached, so re-tapping the same prefix after a re-init never fired. Subscribe again after the next init.pass*with aPatcher.messageslistener (#62): whilemessageshas a listener,passBang/passInt/passFloat/passStringreturntruefor a name nothing receives.- Deprecated:
LiveCoding.busforwards to the newBus.tap(#59).BusValue/BusFramemoved tobus.dart;package:yse/yse.dartstill exports them. - Patcher behaviour changes that alter existing patches (no wrapper
change; #64):
.routestrips the selector and matches floats (.routepassis the old behaviour);.metroperiod changes apply while running and its left inlet takes bang / stop / float;.counterhonours its start value and step inlet;~sawis bipolar; multiple~dacs sum;.msends the typed message it spells;.midiouttakes byte lists;~+/~-default their right operand to 0 and~bpdefaults to Q 1;.programchange/.channelpressureno longer send an extra byte;~line stopholds. - Engine behaviour changes picked up:
bandPass(and~bp) has unity gain at its centre frequency (upstream #1022); the random generator is thread-safe, reproducible by default and draws floats in[0, 1);Channel.virtual = falseturns virtualisation off (upstream #864); doppler uses a wall clock (upstream #667); an out-of-rangeMidiOutchannel is clamped to0..15(upstream #1014); domain clocks and clips advance per rendered block, may be disposed in any order, and a clip survivesSystem.close().
Sessions and devices #
- libYSE 3 session and device semantics (#56):
- New
System.requestedSampleRate(get/set beforeinit/initOffline; survivesclose, 0 clears it). An offline session runs exactly at the requested rate. - New
System.setChannelConfiguration(layout, outputs)sets the speaker layout of a session with no device (offline, headless CI). - New
DeviceSetup.outputChannels. - Dartdoc corrected for
setAutoReconnect(delayMsis wall-clock milliseconds),resume(no-op on an offline session),missedCallbacks,devices,Device.defaultBufferSize, the MIDI device-name getters andunderwaterDepth(world units below the surface, not 0..1).
- New
- Render threads, block size and RNG seeding (#57):
- New
System.renderThreads(get/set;-1auto,0audio thread only,nworkers; set before init, survivesclose) andSystem.activeRenderThreads. - New static
System.blockSize(128):renderOffline(blocks)rendersblocks * blockSizeframes. - New static
System.randomSeed(seed)/System.randomize()seed the engine's generator (.random, granulator jitter, LFO random shapes). The engine is reproducible by default.
- New
- Sound occlusion callback (#58): new
System.occlusion, anOcclusionFn(double Function(Pos source, Pos listener)) the engine calls synchronously duringupdate()once per sound withSound.occlusionenabled; the result (0..1) ducks the sound. One callback per process;nullremoves it andSystem.close()clears it. An exception thrown by the callback counts as unoccluded for that update and is reported to the zone that installed it. - List audio devices without opening one (#51, upstream
yse-soundengine#1026):
- New
System.refreshDevices()fillsSystem.devices(anddefaultDevice/defaultHost) without opening a stream. It works beforeinit, on aninitOffline()session and on a running device session, and returnsfalsewith an empty list when the audio backend cannot start. - It also starts the backend, so
openDevice()now works on aninitOffline()session in a process that never calledinit(). - Hot-plug: with no stream open the backend restarts and picks up devices plugged in or removed since. With a stream open the list is re-read from the existing enumeration, so a new device appears only after the device is closed and the list is refreshed again.
- The refresh replaces the list; match devices by name and host, not by index.
- New
- Auto-reconnect reports the outcome (#54, upstream
yse-soundengine#1027):
- New optional
setAutoReconnectparametersfallbackToDefault(open the default device when the session's own cannot be reopened; off by default) andmaxAttempts(stop after that many consecutive failed attempts; 0 = no limit). - New
System.reconnectState(ReconnectState:none,reconnecting,restored,fellBackToDefault,failed),System.reconnectCount, andSystem.activeDevice/System.activeHostfor the device actually playing.
- New optional
Bus #
Bus: host publish and exact-address subscribe (#59):- New
Busclass:Bus.publish(address, value),Bus.subscribe(address)(exact match) andBus.tap(prefix).BusValue/BusFramemove to it. - New
Synth.busName, so a synth answers onsynth.<name>.*.
- New
- Named sounds and channels on the bus (#60):
- New
Sound.busName: a named sound answers onsound.<name>.volume,.speedand.position. - New
Channel.busName: a named channel answers onchannel.<name>.volume. It is separate fromChannel.name, the log name given toChannel.create.busNameis the name used for bus addressing on all three kinds. The value is remembered per native channel, soChannel.master.busNamereads back across wrappers.System.close()forgets it.
- New
Patcher #
- Named patchers (#50, upstream yse-soundengine#896):
- New
Patcher.name(get/set) and aname:parameter onPatcher(...). Every patcher has a name:null/''restores the auto-generatedpatcher_<N>. Names are not unique; two patchers with the same name share their slots. - A name longer than
Patcher.maxNameLength(55) throwsYseExceptionand keeps the current name. The name is saved bydumpJsonand applied byparseJsonunless one was already set. - It is named
name, notbusNamelikeSynth/Sound/Channel, because it is the patcher's own engine-side name (read back from the engine, saved with the patch), not only a bus address.
- New
- Subpatchers (#61, upstream yse-soundengine#545 / #764):
- New
Patcher.setContainer(obj, container)puts an object inside anObj.patcherobject (or back at the top level withnull); read it back withPHandle.container. The engine refuses an illegal move (not apatchercontainer, or a cycle) without a status, so the wrapper reads the containment back and throwsYseException; a handle from anotherPatcherthrowsArgumentError. - New
PHandle.subpatcherInlets/subpatcherOutlets: the pin range a subpatcher presents (.inlet/~inletshare one index space).PHandle.inputs/outputsanswer 0 for a subpatcher. connect/disconnectto a subpatcher resolve to its boundary object; deleting a subpatcher deletes its contents transitively.PHandlenow has value equality: two handles to the same engine object compare equal.
- New
- Patcher send callback (#62, upstream yse-soundengine#907):
- New
Patcher.messages: a broadcast stream ofPatcherMessage(address,BusValue) for every message sent to a name no.rin the patcher receives — a.sin the patch, or an unreceivedpass*. Lists arrive asBusString. The native callback is installed on the first listen and cleared on the last cancel, byPatcher.dispose()and bySystem.close(). - The engine calls it on the emitting thread, which is an engine worker
for timer-driven messages, so the bridge is a
NativeCallable.listenerand delivery is asynchronous. Every native message block is freed exactly once, including messages still queued at teardown.
- New
- GUI value protocol (#63, upstream yse-soundengine#551 / #846):
- New
PHandle.guiValueCount,PHandle.guiValueAt(index)(''past the count) andPHandle.guiValueIsSettable. - New
PHandle.restoreGuiValue(state)andPHandle.setGuiValueAt(index, value): thin wrappers that send the state back on inlet 0 (state, orset <index> <value>;.textedittakes the value as its whole state). They throwStateErroron a non-settable object such as.b. There is no engine setter by design. - Dartdoc on
guiValue/getGuiProperty/setGuiPropertynow spells out the contract: polls may be destructive, per-cell reads are not atomic, and GUI properties are persisted decoration, not live state. New "GUI controls" section in the patcher API docs.
- New
Objcovers the whole registry (#64): a constant for every one of the 305 registry objects (was 42), generated bytool/gen_obj_constants.dartfrom the bound registry and upstreampObjectList.hpp, with the object's summary as dartdoc. Names mirrorYSE::OBJ(G_ARRAY_INDEXOF→gArrayIndexof). NewObj.alllists them all. Subpatcher boundaries:Obj.gInlet,gOutlet,dInlet,dOutlet.- New
PatcherObjectType.requiresMidiDevice(#64): true for objects that hold a MIDI port, which exist only in builds with MIDI device support (not macOS / Android).
Buffers, DSP and reverb #
- Buffer math, copy / swap, file rate and multichannel buffers (#65,
upstream yse-soundengine#909):
- New
DspBuffer.addBuffer/subBuffer/mulBuffer/divBuffer: sample-wise math against another buffer over the shorter length; dividing by a zero sample gives 0. - New
DspBuffer.copyFrom(src, srcPos:, dstPos:, count:). An out-of-range span throwsYseException(the engine would skip it silently); a negative argument throwsRangeError; overlapping ranges within one buffer throwArgumentError. - New
DspBuffer.swap(other)exchanges samples between buffers of equal length (YseExceptionotherwise); each keeps its class and rate. - New
FileBuffer.fileSampleRate: the native rate of the last loaded file, 0 before a load, unchanged by a re-init at another rate. It isFileBuffer-only, since the C entry point trusts the subclass. - New
DspMultiBuffer(List<DspBuffer>)(copies its channels; finalized natively) withchannelCount, andSound.fromMultiBufferto play it as a stereo / surround source. FileBuffer.saveFiledartdoc states the format (mono 32-bit float WAV at the file's rate) and that it fails while a customBufferIOis active.
- New
- Underwater insert, chain walking, reverb preset values + morph (#66,
upstream yse-soundengine#909):
- New
UnderwaterDspObjectsubclass with adepthget/set (world units below the surface:<= 1transparent,1..5crossfades,>= 5fully neutral, negative clamps to 0). The inheritedimpactis not applied. Independent ofSystem.underwaterFx/underwaterDepth. - New
DspObject.next: the object linked after this one, ornull. It returns the existing Dart wrapper (via a weak native-address registry), soidentical(a.next, b)aftera.link(b)and no second owning wrapper is ever created. It reads the engine, so it followslink's insert-between semantics and a disposed object being spliced out. - New
ReverbPresetValues.fromPreset(preset)(the exact valuesReverb.presetapplies) andReverbPresetValues.lerp(a, b, t)(the engine's morph core,tclamped to[0, 1], exact at both ends). Neither needs an engine session. - New
Reverb.applyValues(values)applies a full parameter set to a zone, exactly asReverb.presetdoes for a named one.
- New
Synth, music and MIDI #
Playerworks: it plays on aSynth(#85). Attach the synth to a playingSound.fromSynthto hear the player; the newPlayer.synthreturns it. The player holds what the engine borrows: its synth, every scale the scale cross-fade may still read (the last one set without a fade and each fade target since) and its motif pool (addMotifholds,removeMotifreleases). All are released only after the player's native destroy;dispose()releases the scales and motifs at once and keeps the synth until the wrapper is collected, because the engine still sends note-offs to it when it retires the player on its next audio block (upstream yse-soundengine#1181). If you dispose the synth yourself, let one block run afterplayer.dispose()first.Player.setScale/addMotifdocs no longer claim the player copies its scale: the engine borrows both (#84).example/demo17_music.dartnow plays a player; README and docs no longer callPlayera placeholder.- DX7 banks from memory, patch lookup, Note-driven synth calls,
PNote.channel(#67, upstream yse-soundengine#909):- New
Dx7Bank.fromBytes(Uint8List)parses a bank already in memory (Flutter assets, Android APKs). The bytes are copied for the call only. - New
Dx7Bank.indexOf(name): the index of the first patch whose space-trimmed name matches exactly (case-sensitive), ornull. Pairs withSynth.setFmPatch. - New
Synth.playNote(Note)/Synth.releaseNote(Note): the note's channel is the channel, its pitch rounded to the nearest integer is the note number, and its volume is the velocity. - New
PNote.channelget/set. - Docs: MIDI channel numbering has two bases.
MidiOutand parsedMidiInmessages use wire-level0..15;Synth,ClipEvent,Note/PNoteand theMidiIn.connectSynthfilter use1..16, with0meaning omni on a synth call. Stated on each class and in a table on the MIDI docs page. The wrapper's defaults already match. - Docs: a
DomainClockand itsClipTransports may now be disposed in any order (a clip on a destroyed clock stops firing), disposing a clip releases its sounding notes, a clip survivesSystem.close(), and clocks advance per rendered block.ClipTransport.stop()was already a plain pass-through to the engine's own note-off flush, so nothing in the wrapper became redundant.
- New
MidiOut.rawfor two-byte messages and SysEx (#68, upstream yse-soundengine#748 / #903):- New
MidiOut.raw(List<int> bytes)sends one complete message exactly as given — no padding, no truncation — so program change, channel pressure, single-byte realtime messages and SysEx of any length can be sent. The bytes are copied for the call; an empty list is a no-op. The message is checked in Dart first and anArgumentErrornames the problem: a value outside0..255, a missing status byte (no running status), a data byte with the high bit set, a length that doesn't match the status, a SysEx without its closing0xF7, or an undefined status. raw3is unchanged; its docs now point atrawfor anything that isn't a three-byte message.- Docs: an out-of-range
MidiOutchannel is clamped to0..15by the engine (libYSE 3.0.1, upstream #1014) instead of corrupting the status byte, andMidiOut.openon a port that doesn't exist leaves the port unopened, so every send is a silent no-op. BufferIOdeliberately does not wrapyse_buffer_io_exists/yse_buffer_io_remove(upstream #909). They look a buffer up by the native address passed at registration, which a Dart caller never holds:addAssetregisters a copy the wrapper allocates itself, and withstoreCopy = truethe engine registers its own copy, so the address never matches anyway.exists/removeAssetcover the same need by ID.
- New
DomainClock/ClipTransport.bindreport failures throughYseExceptionwith the engine's error detail (#55).Channel.namereads through the 3.0 caller-buffer getter (#55).
Lifetime and safety fixes #
- Sounds and patcher inserts keep their borrowed sources alive (#65,
#80).
Sound.fromBuffer,Sound.fromMultiBuffer,Sound.fromPatcherandSound.fromSynthhold a Dart reference to their source, andSound.dspholds the assigned chain head, asChannel.dspalready did.DspObject.patcherInsertholds itsPatcher. Before, dropping the caller's only reference let garbage collection run the source's native finalizer while the engine still used it: a dropped buffer orSynthwas freed under its playing sound, and a droppedPatcherlost its control surface (the engine keeps rendering a graph it has taken over). Explicitly disposing a source before its sound is still unsupported; the docs now say so for every loader. - Every borrowed object stays alive under its consumer (#84):
DspObject.linkchains: each effect holds the one linked after it, following the engine's own rewiring (linkinserts between,unlinkdetaches,disposesplices the successor onto the predecessor). A chain whose head is held — by aSound, aChannelor your own variable — keeps every link alive.Channel.dspholds its insert per native channel, soChannel.master.dsp = fx(and the other singleton getters, which return a fresh wrapper each time) keepsfxalive, andChannel.master.dspreads it back. Released on reassignment,Channel.disposeandSystem.close.ClipTransport.connectSynth/connectMidiOut,MidiFile.connectSynthandMidiIn.connectSynthhold each sink until it is disconnected or the owner is disposed.Motif.setFirstPitchholds itsScale.- Finalizer order: when an owner and what it borrows become unreachable
in the same collection, their native finalizers used to run in
unspecified order, so a sound's buffer or synth could be freed before
the sound. Borrowed objects are now released only after the owner's
native destroy has run (they are the token of a Dart
Finalizeron the owner, released when its callback is delivered).
Log.messagesandMidiIn.rawMessagesno longer leak queued messages at teardown (#76). Both used to close theirNativeCallable.listenerstraight after clearing the native callback. That dropped the calls still queued on its port, so the engine-malloc'd string or byte buffer of every message emitted just before the last cancel (orMidiIn.dispose) was never freed. They now use the drain-marker teardown fromPatcher.messages(#62): clear the callback, post a NULL marker through the same port, and close the callable only when the marker arrives. Every buffer is freed exactly once, and messages still queued are dropped rather than delivered.MidiIn.disposenow destroys the port before it retires the callbacks. The parsed MIDI stream carries no native buffers and is unchanged.DspObjectkind checks (#72) and late-voice-group checks (#81): see Breaking changes. The mix-grade modules (Compressor,ParametricEq, ...) were already safe because they are typed subclasses.System.devicesguards a NULL device pointer, andDevice.iddocuments -1 (#55).
Docs, examples and tooling #
- Upgrade guide and new pages (#70):
intro/upgrading(0.1.0 → 0.2.0, old and new code per change),intro/sessions_and_devices(sessions, sample rates, offline rendering, device selection,refreshDevices, auto-reconnect), a clocks-and-clips tutorial (tutorials/06_clocks_and_clips), a per-callback threading table inintro/mental_model, and refreshed devices / patcher / synth / install pages. The:yse-cpp:links on the API pages had title and target swapped (so they pointed at.../YSE::deviceand the like); they now point at the upstream libYSE pages. - Examples (#70): new
example/demo19_bus.dart(publish / subscribe / tap, named synth, sound, channel and patcher) andexample/demo20_clocks_clips.dart(DomainClock+ClipTransport+Synth).demo06_devices.dartlists devices withrefreshDevices()on an offline session instead of opening the default device;hello_sound.dartreports a failedinit();demo07_dsp_buffer.dartusesDrawableBuffer/FileBuffer(#55);demo08_dsp_modules.dartsizes the granulator pool for 48 kHz;demo17_music.dartplays aPlayer(#85). The Android sample setsrenderThreads = 0. - Patcher object reference covers all 305 objects (#69). The docs
site's object reference listed the 42 objects of a stale upstream 2.x
snapshot. It is now an index plus one page per category (templates
ported from upstream libYSE), with each object's 3.x summary, its
Objconstant, and a MIDI device only flag for the objects that are not available on macOS / Android. The snapshot (docs/source/_data/patcher_objects.json) is now generated from the bound registry bytool/gen_patcher_snapshot.dart, andtest/gen_patcher_snapshot_test.dartfails when it drifts. The page moved fromapi/patcher_objects.htmltoapi/patcher_objects/. - Patcher object reference shows the long-form notes (#78). Under
each object's summary the reference now renders libYSE's longer notes
(message formats, edge cases, examples) for the 251 objects that have
them.
tool/port_patcher_notes.dartcopies them from the engine submodule intodocs/source/_data/patcher_notes/, turning links into the upstream patcher guide into links to the libYSE site and C++ API names (YSE::Randomize(),CreateObject, ...) into their Dart equivalents;test/port_patcher_notes_test.dartfails when the copy drifts. - API generator (#75, and the #56–#59 docs fix):
tool/emit_api_rst.dartemits pages for typedefs (BusFrame,OcclusionFn), exits 1 when a re-exported name has no page, and translates dartdoc's fenced code blocks and pipe tables into RST so the Sphinx build stays warning-free. - Prebuilt engine for the pinned tag (#88): new
tool/fetch_engine.dartreads the release tag the engine submodule pins, downloads that tag's Windows x64 or Linux x64 release asset from the public yse-soundengine release URL (noghneeded), checks it against the sha256 digest GitHub reports, and unpacks it into the gitignored.engine/<tag>/. Running the tests and examples no longer needs MSYS2 or CMake: pointYSE_DLL_PATHat the path it prints. It fails when the pin is not a release tag. Building from source stays documented as the alternative and is what CI still does. - Unreleased-engine mode, made visible (#89): testing against an
engine that has no release yet is opt-in, by pointing
YSE_DLL_PATHat a build of a separate yse-soundengine clone (README, Unreleased engine (opt-in)). Every engine test suite now prints anUNRELEASED ENGINEbanner whenSystem.versiondiffers from the pinned tag (3.1.0vsv3.1.0) or the library is loaded from outside.engine/<tag>/and the submodule'sbuild/bin; it never fails a test. Committed bindings must come from the pinned tag. The pin helpers moved fromtool/fetch_engine.dartto the sharedtool/engine_pin.dart. The loader still does not search.engine/on its own. ffigen.yamldocumentsdart run ffigen --config ffigen.yaml(ffigen 20 no longer finds the config on its own) (#55).yse_flutter_libsis bumped to 0.2.0: it packages the same pinned libYSE v3.1.0. (Both this and the next entry are superseded by #96: the build hook now bundles the Android engine and the plugin is deprecated; see The engine comes with the package above.)yse_flutter_libspackages the engine's Android release (#91) instead of compiling libYSE with the NDK per ABI. The plugin'sandroid/engine.propertiesnames the tag and the sha256 oflibyse-<tag>-android.tar.gz; Gradle downloads it once into the Gradle user home, checks the digest and addsarm64-v8a/x86_64asjniLibs, so consumers (including pub.dev and git dependencies) need neither the NDK nor the engine sources.tool/fetch_engine.dart --platform androidunpacks the same asset into.engine/<tag>/, which the plugin uses in a checkout; the Gradle propertyyse.engineDircovers offline builds, andyse.buildFromSource=truekeeps the NDK source build as an opt-in for unreleased engines.test/flutter_libs_engine_test.dartfails whenengine.propertiesdrifts from the submodule pin.- A relative
YSE_DLL_PATHloads on Windows (#92). The loader now resolvesYSE_DLL_PATHagainst the working directory before loading. Before, a relative path failed with error 126:SetDllDirectoryWtakes the working directory out of the DLL search order, andLoadLibrarylooks a relative path up along that order, solibyse.dllitself was not found. A failed load now throws aYseExceptionthat names the resolved path and the likely cause (a missing dependency library).test/library_load_test.dartloads the engine through a relative path in a fresh process.
Known upstream issues #
Open in yvanvds/yse-soundengine, found while wrapping v3.1.0; details and workarounds are on the upgrade page.
- #1033: an offline session with no
requestedSampleRateinherits the previous session's rate (48 kHz only in a fresh process), andsetChannelConfigurationtakes effect only on a block that mixes a sound (channel layout applies only while a sound is alive). - #1175: disposing a never-attached synth with VA or FM voices crashes the next synth that is attached to a sound.
- #1176: clearing a MIDI-in callback does not wait for a dispatch already in progress, so one in-flight message can race the wrapper's teardown.
- #1177: a synth's setup job can build it before its voices are added; the synth then keeps 0 voices with no error. The wrapper throws when it can detect a late voice group; it cannot detect an empty build.
- #1181: a disposed
Playerdereferences its synth on the next audio block; let one block run before disposing the synth.
Added since 0.1.0 on the libYSE 2.x engine #
These landed between 0.1.0 and the 3.x upgrade and ship for the first time in 0.2.0:
- Platforms. Android support:
library.dartresolveslibyse.soon Android via the Android linker (no path lookup — the.sois bundled into the host APK / AAB underlib/<abi>/). A new sibling Flutter pluginpackages/yse_flutter_libs/cross-compiles the engine for each ABI (defaultarm64-v8a,x86_64) by externalNativeBuild'ing the upstream CMakeLists with NDK 27 / minSdk 26. Consumers add bothyseandyse_flutter_libsto theirpubspec.yaml; Gradle picks up the resultinglibyse.soautomatically. Oboe is the audio backend (AAudio on API 26+ with OpenSL ES fallback). A minimal end-to-end sample lives atexample/android_sample/. README has a parallel Android section covering the plugin model, ABI selection, permissions, and threading notes. - Linux support.
library.dartresolveslibyse.soon Linux in addition tolibyse.dllon Windows. RPATH is$ORIGIN, so sibling shared libs are discovered withoutLD_LIBRARY_PATH. Tested against Ubuntu 24.04 (the GitHub-Actions runner baseline). README has parallel Windows / Linux build sections.tools/ci-linux/Dockerfilebuilds an image with the engine deps plus the Dart SDK so Windows contributors can reproduce a Linux build locally without rebooting.tool/linux_smoke.dartexercises FFI load +initOffline/renderOfflinefor headless validation in Docker and CI. A GitHub Actions CI job (Ubuntu 24.04) builds the engine and runsdart analyze/dart test/ the headless smoke on every push and PR, with a strictdart formatgate (#46). - API reference site (closes #6). New
docs/tree builds a Sphinx site that mirrors the upstream libYSE docs site (sphinx-book-theme, same intro / tutorials / API layout) so contributors who already know the C++ docs find their way.tool/emit_api_rst.dartwalks the classes re-exported fromlib/yse.dartand emits one RST page per class straight from each class's dartdoc comments — comments stay the canonical source of truth. A.github/workflows/docs.ymlworkflow builds the site on every push tomainand publishes togh-pages. MidiIn: broadcast streams of raw and parsed input messages, with the native callback installed on first subscription and bridged from RtMidi's input thread.- Channel peak metering (pre/post-volume, linear/dB, per output) and
the live device getters
System.activeSampleRate/activeBufferSize/activeOutputLatency. System.sessionSampleRate: the session rate, constant frominittocloseacross pause / resume.LiveCoding(#14): submit Python scripts to the engine's embedded interpreter and receive tracebacks onLiveCoding.errors.DomainClockandClipTransport(#21): named, rampable beat clocks and engine-timed MIDI clip playback;ClipTransport.stop()flushes note-offs for exactly the notes that clip has sounding (#40).Synth(#23): polyphonic voice pools (sine, virtual-analog, FM from DX7 banks, SFZ sampler) behindSound.fromSynth, with per-note 3D positions (PositionHandler); channel and key-range parameters on the VA / FM / sampler voice groups (#41).- Channel aux sends, return buses and insert DSP (#24):
Channel.createWithSends/createReturn, per-slot sends, andChannel.dsp. - Mix-grade DSP modules (#25):
Compressor,ParametricEq,Chorus,PlateReverb,FeedbackDelay. - Patcher object metadata registry (#26):
PatcherRegistry,PatcherObjectTypeand its inlet / outlet / parameter descriptions. - libYSE 2.3.1 surface (#38):
MorphingReverb,ReverbPresetValues,DspObject.patcherInsert(Patcher), andMidiFile.connectSynth/MidiIn.connectSynth. DspObject.link(null)detaches the forward edge (#42).- Host bus tap (#43):
LiveCoding.bus(prefix)as a broadcast stream of(address, value)frames (superseded byBus.tap, #59).
0.1.0 #
Initial Windows release.
First public release. Wraps the YSE sound engine
(github.com/yvanvds/yse-soundengine)
via a hand-authored C ABI bridge that lives upstream (YseEngine/c_api/)
plus ffigen-generated low-level bindings + hand-written idiomatic Dart
wrappers in this package.
Wrapped subsystems:
System— lifecycle, devices, MIDI device enumeration, global reverb, underwater FX, update loop, version, CPU loadListener— position, velocity, orientationSound— file / buffer / patcher sources, transport, 3D, mixing, DSP-chain attachmentChannel— pre-built singletons (ChannelMaster / FX / Music / Ambient / Voice / Gui), user-created channels, volume, reverb attachReverb— positioned + global, 11 presets, full parameter surfaceDevice+DeviceSetup— descriptor enumeration + open-device configDspBuffer— plain / drawable / file / wavetable subclasses, bulk Float32List I/O, draw_line, file load/save, band-limited wavetablesDspObject— 11 effects (lowpass, highpass, bandpass, sweep, basicDelay + lp/hp variants, phaser, ringModulator, difference, granulator) plus the inherited bypass / impact / LFO surfacePatcher+PHandle+Objconstants — Max/MSP-style graph with JSON round-tripMidiFile,MidiOut— file playback + device outputNote,PNote,Scale,Motif— music-theory primitivesLog— broadcastStream<String>of engine messages viaNativeCallable.listenerwith ownership-transfer marshallingBufferIO— registerUint8Listassets under string IDs
Known limitations:
- Android (and Linux) landed in the next release — see 0.2.0 above.
X but its upstream
player::create(synth&)factory is commented out (synth subsystem deferred upstream) — every method call crashes the process. Don't use until YSE restores the synth. (Fixed in 0.2.0:Player(Synth).) - The callback-based
YSE::ioVFS isn't wrapped —BufferIOcovers the common asset-pack use case. - Custom
dspSourceObjectsubclassing isn't wrapped — buffer-source sounds viaSound.fromBufferare the procedural-audio path for now. - No native-assets build hook yet: consumers point
YSE_DLL_PATHat a locally-builtlibyse.dll. See README for the manual setup.