Synth#
A polyphonic synthesiser voice pool rendered behind one Sound. A
Synth owns polyphony, voice allocation, voice stealing and full
keyboard/pedal state; you build its voice pool from a built-in voice type
(sine, virtual-analog, FM/DX7, or SFZ sampler), attach it behind a positioned
sound, then drive it with noteOn / noteOff and controller / pedal
events — or with playNote / releaseNote from a Note. Synth
channels are 1-based (1..16, 0 = omni); see the channel-numbering
table on the MIDI page.
The FM voice is patched from a Dx7Bank imported from a DX7 .syx
file (Dx7Bank.load) or from bytes already in memory such as a bundled
asset (Dx7Bank.fromBytes); indexOf finds a patch by name. The sampler voice renders an SfzInstrument. Per-note 3D
positioning — “the swarm” — is configured with a PositionHandler.
Two engine issues in libYSE v3.1.0 shape how a synth is set up and torn down (see Upgrading to package:yse 0.2.0 (libYSE 3.1) for the full list):
Add every voice group right after
Synth(). The engine’s setup job can build a new synth before its voices arrive and leave it at 0 voices with no error (upstream #1177); voice groups added after the pool is built throwStateError.Attach a synth with VA or FM voices to a sound before disposing it. Disposing one that was never attached crashes the next synth that is attached to a sound (upstream #1175). Sine-only and voice-less synths are not affected.
Upstream: YSE::synth.
Synth#
class Synth implements Finalizable
A polyphonic synthesiser voice pool rendered behind one [Sound] (issue #23 / engine issues #145–#149).
A synth owns polyphony, voice allocation, voice stealing and full keyboard/pedal state; you drive it with note, controller and pedal events. The usual flow is:
Build a voice pool from a built-in voice type — [addSineVoices], [addVaVoices], [addFmVoices] or [addSamplerVoices].
Wrap it in a positioned sound with Sound.fromSynth (the synth must outlive that sound).
Play notes with [noteOn] / [noteOff].
Voice cloning happens off the audio thread on the engine’s setup pool, so a synth becomes playable a short moment after an addVoices call returns — poll [voiceCount] for the cloned count, exactly as a file-backed [Sound] is not playable until its buffer finishes loading. Voices must be added before the synth is attached/played; once [voiceCount] is non-zero the add*Voices methods and [setPositionHandler] throw [StateError].
Add every voice group right after `Synth()` (issue #81). In libYSE v3.1.0 the engine’s setup job can build a new synth before its voices arrive: the audio thread queues a synth setup job while any synth waits to be built (plus one spare job right after another synth finishes), and that job builds every synth it finds. A synth built that way keeps zero voices for good — the engine drops the later groups with only a log warning, and [voiceCount] stays 0 with no error raised. Measured on Windows x64, adding the voices immediately after Synth() still loses the race in roughly 1–10 % of cases when another synth was just built; leave 5 ms between the two and it is lost almost every time. The wrapper cannot detect this case (the C API reports success and has no status to query), so if [voiceCount] stays 0 well after the synth’s sound is playing, dispose both and build a new synth. The fix belongs in the engine: yvanvds/yse-soundengine#1177.
Channels use the musical layer’s 1-based numbering: 1..16 address a specific MIDI channel and 0 means omni (all channels) — 0 is never channel 1. This is the same base as [ClipEvent.channel], [Note.channel] and the MidiIn.connectSynth filter, but not the wire-level 0..15 used by MidiOut and the parsed MidiIn messages; add one when routing a raw MIDI channel into a synth. Velocity, controller and aftertouch values are normalised to [0, 1]; the pitch wheel to [-1, 1].
Threading (CLAUDE.md): construct and drive this from the [System] isolate. The engine’s audio-thread note-rewrite hook (yse_synth_set_note_callback) is intentionally not surfaced — it fires on the audio thread, which cannot safely re-enter Dart, the same reason custom voices and position handlers stay unwrapped.
Constructors#
factory Synth()
Create and register a synth, ready to receive voices and note events.
Throws [YseException] if the native synth cannot be allocated.
Properties#
bool get isValid
Whether the synth has a live native implementation.
int get voiceCount
Total number of allocated (cloned) voices across every group. Zero until the setup pool finishes cloning — poll this to know the synth is playable.
Pointer<YseSynth> get handle
Raw native handle, for sibling wrappers within lib/src/ (e.g. ClipTransport.connectSynth). Not part of the public API surface.
String? get busName
set busName(String? name)
The name this synth answers to on the global bus, or null when it is anonymous (the default).
Once named foo, the engine subscribes the synth to synth.foo.note, .off, .cc, .bend, .aftertouch and .alloff, so a Bus.publish (or a script’s yse.send, or a patcher .s) drives it with no host round-trip. synth.foo.note takes a BusList of [channel, note, velocity]; see the bus page for the other payloads.
Setting null or ‘’ clears the name and its subscriptions; renaming re-subscribes. Names are unique per synth — a duplicate is rejected and logged by the engine, with no error here. Only effective while the engine is between init and close. There is no native getter: this returns the last value set through this wrapper.
Methods#
void addSineVoices(int count, {int channel = 0, int lowestNote = 0, int highestNote = 127, double attack = 0.01, double decay = 0.1, double sustain = 0.8, double release = 0.2})
Add a group of [count] built-in sine voices (a sine oscillator shaped by an ADSR envelope) responding to note numbers in [lowestNote, highestNote] on [channel] (0 = omni). May be called several times to build layered or split keyboards. [attack], [decay] and [release] are in seconds; [sustain] is a level in [0, 1].
Must be called right after Synth(), before the synth is played (see the class notes on issue #81). Throws [StateError] if the voice pool is already built, and [YseException] if the group is rejected.
void addSamplerVoices(SfzInstrument instrument, int count, {int channel = 0, int lowestNote = 0, int highestNote = 127})
Add a group of [count] SFZ sampler voices rendering [instrument], responding to note numbers in [lowestNote, highestNote] on [channel] (0 = omni). Filtering by [channel] lets a sampler pool respond only to events broadcast on its MIDI channel — e.g. per-voice routing from a [ClipTransport].
The instrument’s region table and PCM are shared with the voice group, which retains its own reference — so [instrument] may be disposed right after this returns. Like [addSineVoices], call it right after Synth(); throws [StateError] if the voice pool is already built, and [YseException] if the group is rejected.
void addVaVoices(int count, {int channel = 0, int lowestNote = 0, int highestNote = 127})
Add a group of [count] virtual-analog + wavetable voices with a fresh default patch, responding to note numbers in [lowestNote, highestNote] on [channel] (0 = omni). Filtering by [channel] lets a VA pool respond only to events broadcast on its MIDI channel — e.g. per-voice routing from a [ClipTransport].
Establishes the synth’s VA patch, which the setVa* setters steer. Call once per synth, right after Synth() (see [addSineVoices]). Throws [StateError] if the voice pool is already built, and [YseException] on failure.
void addFmVoices(int count, {int channel = 0, int lowestNote = 0, int highestNote = 127})
Add a group of [count] DX7-class 6-operator FM voices with the built-in sine test patch, responding to note numbers in [lowestNote, highestNote] on [channel] (0 = omni). Filtering by [channel] lets an FM pool respond only to events broadcast on its MIDI channel — e.g. per-voice routing from a [ClipTransport].
Establishes the synth’s FM patch — select a DX7 voice into it with [setFmPatch], or dial the headline params with the setFm* setters. Call once per synth, right after Synth() (see [addSineVoices]). Throws [StateError] if the voice pool is already built, and [YseException] on failure.
void noteOn(int noteNumber, {int channel = 1, double velocity = 1.0})
Start a note. [velocity] is normalised to [0, 1].
void noteOff(int noteNumber, {int channel = 1, double velocity = 0.0})
Release a note. [velocity] is the release velocity, normalised to [0, 1].
void playNote(Note note)
Start the note described by [note] (issue #67).
The note’s [Note.channel] is the channel (1-based, 0 = omni), its [Note.pitch] rounded to the nearest integer is the note number, and its [Note.volume] is the velocity. [Note.length] is ignored — release the note with [releaseNote]. The note is read, not retained.
void releaseNote(Note note)
Release the note described by [note] (issue #67) — the counterpart of [playNote], mapped the same way (so [Note.volume] becomes the release velocity). Pass the same note, or one with the same channel and rounded pitch, that started it.
void allNotesOff({int channel = 0})
Release every held note on [channel] (0 = all channels). Voices enter their normal release; they are not cut.
void pitchWheel(double value, {int channel = 1})
Bend every voice on [channel]. [value] is normalised to [-1, 1] (0 = centre).
void controller(int number, double value, {int channel = 1})
Send a control-change on [channel]. [value] is normalised to [0, 1]. CC 64 / 66 / 67 act as the sustain / sostenuto / soft pedals; other CC numbers are stored as the channel’s last controller value.
void aftertouch(double value, {int channel = 1, int noteNumber = -1})
Apply aftertouch pressure, normalised to [0, 1]. A [noteNumber] of -1 (the default) is channel-wide; otherwise only the voice(s) sounding that note receive it.
void sustain(bool down, {int channel = 1})
Set the sustain pedal (CC 64) on [channel].
void sostenuto(bool down, {int channel = 1})
Set the sostenuto pedal (CC 66) on [channel].
void softPedal(bool down, {int channel = 1})
Set the soft pedal (CC 67) on [channel].
void setVaOscWave(int osc, VaWaveform wave)
Set oscillator [osc]’s waveform.
void setVaOscDetune(int osc, double semitones)
Set oscillator [osc]’s detune in semitones.
void setVaOscLevel(int osc, double level)
Set oscillator [osc]’s output level.
void setVaOscPulseWidth(int osc, double width)
Set oscillator [osc]’s pulse width (for [VaWaveform.pulse]).
void setVaWavetablePosition(double position)
Set the wavetable morph position (for [VaWaveform.wavetable]).
void setVaCutoff(double hz)
Set the filter cutoff in Hz.
void setVaResonance(double resonance)
Set the filter resonance.
void setVaKeyTracking(double amount)
Set the filter key-tracking amount.
void setVaFilterEnvAmount(double octaves)
Set the filter-envelope depth in octaves.
void setVaFilterVelAmount(double octaves)
Set the filter velocity depth in octaves.
void setVaAmpAttack(double seconds)
Set the amplitude-envelope attack in seconds.
void setVaAmpDecay(double seconds)
Set the amplitude-envelope decay in seconds.
void setVaAmpSustain(double level)
Set the amplitude-envelope sustain level in [0, 1].
void setVaAmpRelease(double seconds)
Set the amplitude-envelope release in seconds.
void setVaAmpVelAmount(double amount)
Set the amplitude velocity-sensitivity amount.
void setVaFilterAttack(double seconds)
Set the filter-envelope attack in seconds.
void setVaFilterDecay(double seconds)
Set the filter-envelope decay in seconds.
void setVaFilterSustain(double level)
Set the filter-envelope sustain level in [0, 1].
void setVaFilterRelease(double seconds)
Set the filter-envelope release in seconds.
void setVaLfoType(LfoType type)
Set the LFO shape.
void setVaLfoRate(double hz)
Set the LFO rate in Hz.
void setVaLfoToPitch(double semitones)
Set the LFO-to-pitch depth in semitones.
void setVaLfoToCutoff(double octaves)
Set the LFO-to-cutoff depth in octaves.
void setVaLfoToWavetable(double amount)
Set the LFO-to-wavetable morph depth.
void setVaGain(double gain)
Set the VA voice’s output gain.
void loadVaWavetable(int slot, List<double> cycle)
Install a single-cycle waveform into the VA wavetable morph bank at [slot]. [cycle] holds one period of normalised samples.
Setup-thread only — this reshapes table storage, so call it before the synth is played, not while voices render.
void setFmPatch(Dx7Bank bank, int index)
Copy patch [index] from a DX7 [bank] into the synth’s FM patch — the way to reach the full 155-parameter DX7 voice. The patch is copied, so [bank] may be disposed afterwards. Throws [YseException] on failure.
void setFmAlgorithm(int algorithm)
Set the FM algorithm (0..31).
void setFmFeedback(int feedback)
Set the global feedback amount (0..7).
void setFmTranspose(int transpose)
Set the transpose (0..48, 24 = none).
void setFmLfoSpeed(int speed)
Set the LFO speed (0..99).
void setFmLfoDelay(int delay)
Set the LFO delay (0..99).
void setFmLfoWaveform(int waveform)
Set the LFO waveform (0..5).
void setFmLfoPitchModDepth(int depth)
Set the LFO pitch-modulation depth (0..99).
void setFmLfoAmpModDepth(int depth)
Set the LFO amplitude-modulation depth (0..99).
void setFmPitchModSens(int sensitivity)
Set the pitch-modulation sensitivity (0..7).
void setFmOpOutputLevel(int op, int level)
Set operator [op]’s output level (0..99).
void setFmOpFreqCoarse(int op, int coarse)
Set operator [op]’s coarse frequency (0..31).
void setFmOpFreqFine(int op, int fine)
Set operator [op]’s fine frequency (0..99).
void setFmOpDetune(int op, int detune)
Set operator [op]’s detune (0..14, 7 = centre).
void setFmOpOscMode(int op, int mode)
Set operator [op]’s oscillator mode (0 = ratio, 1 = fixed).
void setFmOpEnabled(int op, bool enabled)
Enable or disable operator [op].
void setPositionHandler(PositionHandler handler)
Attach one of the built-in per-note position [handler]s, giving every voice its own 3D position and movement.
Must be called before the synth is attached/played — like the add*Voices methods, the engine rejects a handler swap once the voice pool is built (it logs a warning and keeps the existing handler). Throws [StateError] once [voiceCount] shows the pool is built, and [YseException] for an unknown handler kind.
void setHandlerCenter(double x, double y, double z)
Move the shared centre (x, y, z) that the spread / orbit handlers read.
A bounded, allocation-free message safe to call every control tick; all of the synth’s live handlers pick up the new centre on the next audio block.
void setNotePosition(int noteNumber, double x, double y, double z, {int channel = 1})
Imperatively place the voice(s) sounding [noteNumber] on [channel] at (x, y, z) — for app-driven trajectories. Primarily useful when no position handler is attached (a handler re-steers the voice next block).
Pos getVoicePosition(int noteNumber, {int channel = 1})
Best-effort snapshot of the current position of a voice sounding [noteNumber] on [channel]. Returns [Pos.zero] if none is sounding. A single snapshot intended for tests / metering, not a readback stream.
void dispose()
Destroy the underlying native synth and detach the finalizer.
Idempotent. Dispose any [Sound] rendering this synth before the synth.
SfzInstrument#
class SfzInstrument implements Finalizable
A loadable SFZ sampler instrument (issue #23 / engine issue #174).
A shareable, engine-lifetime-independent asset: a parsed SFZ region table plus the resident PCM it references. Load one from disk with [SfzInstrument.load], or synthesise a single-region instrument around one sample file with [SfzInstrument.fromSample]. Hand it to a synth’s sampler voice pool via [Synth.addSamplerVoices].
The handle is reference-counted: a voice group that renders the instrument retains its own share, so it is safe to [dispose] this handle right after [Synth.addSamplerVoices] returns, and safe to hold or dispose it across [System] close. Loading decodes samples on the calling isolate (never the audio thread), so construct these off any latency-sensitive path.
Constructors#
factory SfzInstrument.load(String path)
Load and preload an .sfz file, decoding every unique sample into RAM.
Throws [YseException] if the file is unreadable, empty, or has no playable region.
factory SfzInstrument.fromSample(String file, {String? name, int root = 60, int low = 0, int high = 127, double attack = 0.0, double release = 0.1, double maxLength = 0.0})
Build a one-region instrument around a single sample [file] without an .sfz text file.
[root] is the key that plays the sample untransposed; [low] and [high] bound the playable key range. [attack] and [release] are the amplitude envelope times in seconds; [maxLength] caps a non-looping one-shot in seconds. [name] is an optional label used only for identification.
Throws [YseException] if the sample is missing or unreadable.
Properties#
bool get isValid
Whether the instrument is playable (a valid region table with at least one resident sample).
Pointer<YseSfzInstrument> get handle
Raw native handle, for sibling wrappers within lib/src/ (e.g. [Synth.addSamplerVoices]). Not part of the public API surface.
Methods#
void dispose()
Release the instrument. Idempotent; a double free is a logged no-op.
Dx7Bank#
class Dx7Bank implements Finalizable
A parsed DX7 SysEx bank (issue #23 / engine issue #177).
A list of FM patches (1 for a single-voice dump, 32 for a packed bulk dump) imported from a .syx file. Select a patch into a synth’s FM voice pool with [Synth.setFmPatch] — the patch is copied into the synth, so this bank may be [dispose]d afterwards.
Constructors#
factory Dx7Bank.load(String path)
Load and parse a DX7 .syx file into a bank.
Throws [YseException] if the file is missing, unreadable, has a bad header, the wrong length, or a checksum mismatch.
factory Dx7Bank.fromBytes(Uint8List data)
Parse a DX7 SysEx image already in memory (issue #67) — for hosts that ship banks as bundled assets (Flutter assets, Android APKs) where there is no filesystem path.
Accepts the same formats as [Dx7Bank.load]. [data] is copied into a native buffer for the duration of the call only; neither the copy nor [data] is retained, so a transient buffer is fine.
Throws [YseException] on empty input, a bad header, the wrong length, or a checksum mismatch.
Properties#
int get patchCount
Number of patches in the bank (1 or 32).
Pointer<YseDx7Bank> get handle
Raw native handle, for sibling wrappers within lib/src/ (e.g. [Synth.setFmPatch]). Not part of the public API surface.
Methods#
String patchName(int index)
The space-trimmed name of patch [index], or an empty string for an out-of-range index.
int? indexOf(String name)
Index of the first patch whose space-trimmed name equals [name] exactly, or null when no patch matches (issue #67). The match is case-sensitive and compares against [patchName]’s trimmed form, so pass the name without the DX7’s trailing space padding.
Pairs with [Synth.setFmPatch]: synth.setFmPatch(bank, bank.indexOf(‘E.PIANO 1’)!).
void dispose()
Release the bank. Idempotent; a double free is a logged no-op.
PositionHandler#
class PositionHandler
Which built-in per-note position handler a [Synth] attaches, and its configuration (issue #23 / engine issue #171 — “the swarm”).
A handler gives every voice its own 3D position and movement. Build one with a factory constructor and pass it to [Synth.setPositionHandler] before the synth is attached/played. All positions are in the same coordinate frame as a [Sound] position; the shared centre of the spread / orbit handlers can be steered at runtime with [Synth.setHandlerCenter].
Constructors#
PositionHandler.fixed(double x, double y, double z)
Every voice sounds from one fixed position (x, y, z).
PositionHandler.randomSpread({double radius = 1.0, int seed = 0})
Each voice is scattered to a random point within [radius] of the centre.
A given [seed] reproduces the same scatter.
PositionHandler.orbit({double radius = 1.0, double velocityRadius = 0.0, double aftertouchWiden = 0.0, double rate = 1.0, double height = 0.0, double releaseSlow = 1.0})
Each voice orbits the centre — the “swarm”.
[radius] is the base orbit radius; [velocityRadius] adds extra radius at full velocity; [aftertouchWiden] is the fraction of that extra radius reached at full aftertouch. [rate] is the angular speed in radians per second, [height] the vertical offset of the orbit plane, and [releaseSlow] a rate multiplier applied once a note is released.