Music#

Musical primitives — Note, PNote, Scale, Motif — and the generative Player that builds on them. The primitives are usable on their own.

A player plays its notes on the Synth it is constructed with; attach that synth to a playing sound (Sound.fromSynth()) to hear it:

final synth = Synth()..addSineVoices(8);
final sound = Sound.fromSynth(synth)..play();
final player = Player(synth)
  ..setScale(Scale()..add(60)..add(64)..add(67))
  ..play();

The engine borrows the synth, the active scales and every added motif. The player holds Dart references to them, so garbage collection never frees one while the player can still read it, and they are released only after the player’s native destroy. Dispose the player before any of them, and let one audio block run before disposing the synth: the engine retires a destroyed player on its next block and still sends note-offs to the synth then.

Upstream: YSE::note / YSE::player.

Note#

class Note implements Finalizable

A single musical event — pitch, volume, length, MIDI channel.

The fundamental unit for the music subsystem: [Motif]s are sequences of [PNote]s (positioned [Note]s), and the [Player] generates notes within a [Scale] constraint.

Constructors#

factory Note({double pitch = 60, double volume = 1.0, double length = 0, int channel = 1})

Construct a note.

[pitch] is a MIDI pitch (60 = middle C). [volume] is velocity in [0, 1]. [length] is the duration in seconds (0 = engine default). [channel] is the MIDI channel.

Properties#

Pointer<YseNote> get handle

Internal: native handle.

double get pitch
set pitch(double value)

MIDI pitch (60 = middle C).

double get volume
set volume(double value)

Velocity in [0, 1].

double get length
set length(double value)

Duration in seconds.

int get channel
set channel(int value)

MIDI channel, in the musical layer’s 1-based numbering (1..16), as used by Synth.playNote. Not the wire-level 0..15 of MidiOut.

Methods#

void set({required double pitch, double volume = 1.0, double length = 0, int channel = 1})

Replace all four fields in one call.

void dispose()

Destroy the underlying native note and detach the finalizer.

PNote#

class PNote implements Finalizable

A [Note] with a time position — the building block of a [Motif].

Position is measured from the start of the containing motif, in seconds.

Constructors#

factory PNote({required double position, required double pitch, double volume = 1.0, double length = 0, int channel = 1})

Construct a positioned note.

Properties#

Pointer<YsePNote> get handle

Internal: native handle.

double get position
set position(double value)

Time position from the start of the motif, in seconds.

double get pitch
set pitch(double value)

MIDI pitch.

double get volume
set volume(double value)

Velocity in [0, 1].

double get length
set length(double value)

Duration in seconds.

int get channel
set channel(int value)

MIDI channel, 1-based like [Note.channel] (issue #67).

Methods#

void dispose()

Destroy the underlying native pNote and detach the finalizer.

Scale#

class Scale implements Finalizable

A set of allowed pitches.

Drives the [Player] so its generated notes stay in key, and constrains the transpositions a [Motif] can take.

Constructors#

factory Scale()

Construct an empty scale.

Properties#

Pointer<YseScale> get handle

Internal: native handle.

int get size

Number of pitches in the scale.

Methods#

void add(double pitch, {double octaveStep = 12})

Add a pitch to the scale.

[octaveStep] controls automatic octave replication: 12 (default) replicates the pitch at every octave; values <= 0 add only the exact pitch.

void remove(double pitch, {double octaveStep = 12})

Remove a pitch (with optional octave replication).

bool has(double pitch)

Whether [pitch] is a member of the scale.

double nearest(double pitch)

Nearest in-scale pitch to [pitch].

void clear()

Remove every pitch.

void dispose()

Destroy the underlying native scale and detach the finalizer.

Motif#

class Motif implements Finalizable

A re-usable phrase or pattern — a sequence of [PNote]s.

Hand it to a [Player] which will trigger it at appropriate moments.

Constructors#

factory Motif()

Construct an empty motif.

Properties#

Pointer<YseMotif> get handle

Internal: native handle.

set length(double value)
double get length

Set the motif length explicitly, in seconds.

bool get isEmpty

Whether the motif contains any notes.

int get size

Number of notes.

Methods#

void add(PNote note)

Append a positioned note.

void clear()

Remove every note.

void autoSetLength()

Set the motif length automatically to the end of the last note.

void transpose(double pitch)

Transpose every note by [pitch] semitones.

void setFirstPitch(Scale validPitches)

Restrict legal starting pitches.

When the [Player] picks a transposition for this motif it picks one whose starting note belongs to [validPitches].

The engine borrows [validPitches] (it keeps a pointer, not a copy), so later changes to the scale apply. The motif holds a Dart reference to it until the next call or [dispose], so garbage collection never frees it under the motif; don’t .dispose() the scale first.

void dispose()

Destroy the underlying native motif and detach the finalizer.

Player#

class Player implements Finalizable

Generative note sequencer.

Plays random notes within configurable pitch / velocity / gap / length ranges, optionally constrained to a [Scale], optionally drawing from one or more weighted [Motif]s instead of pure randomness. Numeric setters accept an optional [fade] parameter that interpolates from the current value to the target over that many seconds.

A player has no sound of its own: it plays its notes on the [Synth] it is constructed with. Attach that synth to a playing sound (Sound.fromSynth) to hear them. The engine advances players once per rendered audio block.

The engine borrows the synth, the active [Scale]s and every added [Motif] — it keeps pointers to them, not copies. The player holds Dart references to each, so garbage collection never frees one while the player can still read it (#85); they are released only after the player’s own native destroy. Don’t .dispose() any of them while the player is alive.

Constructors#

factory Player(Synth synth)

Construct a player that plays its notes on [synth].

[synth] must be live (not disposed). The player holds it until the player is disposed or collected. Throws [YseException] if the engine refuses to create the player.

Properties#

Synth get synth

The synth this player plays its notes on.

bool get isPlaying

Whether the player is currently producing notes.

Methods#

void play()

Start producing notes.

void stop()

Stop producing notes.

void setMinimumPitch(double target, {Duration fade = Duration.zero})

Set the lowest pitch the player may produce (0..126). [fade] interpolates.

void setMaximumPitch(double target, {Duration fade = Duration.zero})

Set the highest pitch the player may produce (1..127). [fade] interpolates.

void setMinimumVelocity(double target, {Duration fade = Duration.zero})

Set the lowest velocity (0..0.999999). [fade] interpolates.

void setMaximumVelocity(double target, {Duration fade = Duration.zero})

Set the highest velocity (0.000001..1). [fade] interpolates.

void setMinimumGap(double target, {Duration fade = Duration.zero})

Set the minimum gap between successive notes / motifs, in seconds.

void setMaximumGap(double target, {Duration fade = Duration.zero})

Set the maximum gap between successive notes / motifs, in seconds.

void setMinimumLength(double target, {Duration fade = Duration.zero})

Set the minimum note length, in seconds (used when no motif is active).

void setMaximumLength(double target, {Duration fade = Duration.zero})

Set the maximum note length, in seconds (used when no motif is active).

void setVoices(int target, {Duration fade = Duration.zero})

Number of simultaneous voices.

void setScale(Scale scale, {Duration fade = Duration.zero})

Constrain generated pitches to [scale].

With a [fade], the player cross-fades from the current scale to [scale] over that time (the first call always applies at once).

The engine borrows [scale] (it keeps a pointer, not a copy), so later changes to it apply. The player holds a Dart reference to every scale the cross-fade may still read: the last scale set without a fade and each fade target set since. Don’t .dispose() them while the player is alive.

void addMotif(Motif motif, {int weight = 1})

Add a [motif] to the player’s pool. Picked weighted by [weight].

The engine borrows [motif] (it keeps a pointer, not a copy). The player holds a Dart reference to it until [removeMotif] or [dispose]; don’t .dispose() the motif before then.

void removeMotif(Motif motif)

Remove a previously added motif, and stop holding it.

void adjustMotifWeight(Motif motif, int weight)

Adjust the selection weight of an already-added motif.

void playPartialMotifs(double target, {Duration fade = Duration.zero})

Probability that the player plays only part of a motif (0..1).

void playMotifs(double target, {Duration fade = Duration.zero})

Probability that the player draws notes from a motif vs. random (0..1).

void fitMotifsToScale(double target, {Duration fade = Duration.zero})

Probability that motif notes are quantised to the active scale (0..1).

void dispose()

Destroy the underlying native player and detach the finalizer.

The scales and motifs are released at once. The synth stays held until this wrapper is collected: the engine retires a destroyed player on its next audio block, and that step still sends note-offs to the synth (upstream yvanvds/yse-soundengine#1181). If you dispose the synth yourself, let at least one audio block run after this call first (in an offline session, render one) — and before System.close.