Stl.MonoLegato and Stl.PolyLegato¶
IVLS separates legato into two concerns: assignment (which voices form a legato pair) and playback (what happens when a pair is detected). The STL provides two assignment nodes — Stl.MonoLegato for monophonic instruments and Stl.PolyLegato for polyphonic ones — and a companion Stl.LegatoSwitch node. Playback is always product-defined.
Why the Split Architecture¶
Combining legato detection and playback in one node means any change to the transition style (crossfade length, new articulation, different curve) requires modifying the detection code. Separating them means playback nodes can be swapped, extended, or replaced without touching the detection algorithm. It also enables the lookahead engine to inject legato.vo_src retrospectively — the playback node sees an already-established link without modification.
Key Concepts¶
Legato Voice Fields¶
Stl.LegatoBase injects four fields into every voice:
| Field | Default | Purpose |
|---|---|---|
legato.vo_src |
-1 |
Voice index of the source (previous) note |
legato.vo_dest |
-1 |
Voice index of the destination (new) note |
legato.is_current |
FALSE |
Whether this voice is the active sounding note |
legato.registered |
FALSE |
Whether this voice has been paired as a legato destination |
Two predicates are defined as macros:
define legato.found(#vo#) := (Voice[#vo#].legato.vo_src # -1 and Voice[#vo#].legato.vo_dest # -1)
define legato.check_current(#vo#) := (Voice[Voice[#vo#].legato.vo_dest].legato.is_current = TRUE)Bundle Composition¶
Products include the subset they need:
define Stl.Legato += Stl.LegatoBase
define Stl.Legato += Stl.MonoLegato, Stl.LegatoSwitch // mono instruments
define Stl.Legato += Stl.PolyLegato // poly instrumentsStl.MonoLegato¶
Stl.MonoLegato maintains a per-thread voice stack (adt.Create2DList(mono.VoiceBuffer, ivls.THREADS, 128)) and a mono.previous_vo[thread] pointer to the most recently sounding voice.
NoteOn: Appends to the voice stack. If a previous voice exists, links Voice[self].legato.vo_src := mono.previous_vo[thread]. Then calls ivls.pass_formal() — not ivls.pass(). The difference: pass_formal creates a new formal child voice that travels the rest of the flow independently, recording its handle in NodeEnv[nenv].sentVoice. Legato link fields are written onto that sent voice.
NoteOff: If the released note was the topmost in the stack, the previous voice is cloned via ivls.clone_voice(old_vo, nenv). This re-runs the downstream flow from the previous voice's state, effectively re-triggering the note held underneath — classic mono legato re-trigger behavior. no_children_release is set so the engine does not auto-release child voices while notes remain held.
Stl.LegatoSwitch¶
A companion to Stl.MonoLegato for switch-style legato (no crossfade hold). On NoteOn, it checks whether the incoming voice has a legato source and, if so, releases it before passing:
cb NoteOn:
if legato.found(self)
ivls.release_voice(legato.get_src_vo(self))
legato.unlink(self)
end if
ivls.pass()This prevents voice accumulation in instruments where the previous note should cut off as soon as the new one begins.
Stl.PolyLegato¶
Stl.PolyLegato collects simultaneous NoteOn and NoteOff events within a configurable latency window (default 100 ms), then runs a matching algorithm.
Matching algorithm:
- 1 press, 1 release: direct legato pair
- N presses = N releases: matched in ascending pitch order
- Unequal counts: tests all C(larger, smaller) combinations and selects the pairing that minimizes the maximum semitone interval (minimax criterion)
Results land in two parallel ADT lists. After the algorithm, each NoteOn voice searches the result list and writes legato.vo_src to the corresponding source voice's child.
PathCancellation safety: Stl.LegatoBase implements cb PathCancellation to release the source voice if the path is cancelled mid-flow (e.g., voice limit enforcement).
Legato Predicates in Practice¶
// Gate: does this voice have a legato transition?
if legato.found(self)
// run transition logic
// Gate: is the destination still the active legato head?
if legato.check_current(dest)
// safe to release srclegato.found is the primary branch condition. legato.check_current checks whether the destination is still the active head — important for continuous portamento chains where earlier links should not be re-released.
Tokyo Scoring Layered Crossfade¶
Tokyo Scoring builds on these STL nodes with speed profiles (4 speeds), equal-power crossfade (sine-based curves that vary by dynamic layer and vibrato state), layered transitions (up to 2 layers per articulation with independent fade curves), run-to-legato fallback, and voice limit enforcement that walks back four levels of the legato source chain.
Connections to Other Parts of IVLS¶
The legato system is a prerequisite for stl-portamento (Stl.Portamento.Template reads legato.found() to detect transitions). The lookahead engine injects legato.vo_src retrospectively before playback nodes run. Crossfades use stl-modulation-fades fades.curve_out / fades.curve_in on source and destination voices.
Patterns and Caveats¶
- Assignment nodes set
vo_srcandvo_destand callivls.pass_formal(). They perform no crossfading. Playback nodes read the link fields and implement the transition. ivls.pass_formal()inStl.MonoLegatocreates a formal child voice. The legato fields are written on the sent formal voice, not onself.Stl.LegatoSwitchis typically placed within the legato playback flow, not at the assignment stage. Tokyo Scoring calls it after the crossfade voice is launched, not as part of the detection chain.
Related¶
- stl-portamento — reads
legato.found()and thevo_src/vo_destfields - stl-play-event — fires after legato assignment; EventGroups uses the assigned articulation
- stl-modulation-fades — crossfades use fades.curve_out / fades.curve_in
- lookahead — injects legato.vo_src retrospectively