Skip to content

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 instruments

Stl.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 src

legato.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_src and vo_dest and call ivls.pass_formal(). They perform no crossfading. Playback nodes read the link fields and implement the transition.
  • ivls.pass_formal() in Stl.MonoLegato creates a formal child voice. The legato fields are written on the sent formal voice, not on self.
  • Stl.LegatoSwitch is 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.
  • stl-portamento — reads legato.found() and the vo_src/vo_dest fields
  • 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