How to Add Mono Legato¶
Add single-voice legato detection to your product so that overlapping notes are linked as source/destination pairs, enabling crossfade transitions between them.
Prerequisites¶
- A working IVLS product with at least one flow and a PlayEvent node
- Familiarity with flows, voice ownership, and the fades system
Steps¶
1. Include the legato bundle¶
Add Stl.Legato to your node list. If your product already includes Ivls.STL, the legato nodes (Stl.LegatoBase, Stl.MonoLegato, Stl.LegatoSwitch) are already registered.
If assembling nodes manually:
define IVLS_NODES += Stl.Legato2. Understand what MonoLegato does¶
Stl.MonoLegato tracks a per-thread voice stack. When a new note arrives while a previous note is held, it links the two voices as a legato pair — the old voice becomes the source, the new voice becomes the destination. It then deliberately hangs the source voice: alive but ownerless, waiting for you to handle it.
This is the central ownership contract: MonoLegato assigns the link, you handle the transition.
3. Create your legato playback node¶
This node runs after Stl.MonoLegato in the flow. It checks legato.found() and takes ownership of the hanging source voice — typically by fading it out. If no legato link exists, it passes the voice through normally.
node MyProduct.Play.Legato:
cb NoteOn:
if legato.found(self)
{ Take ownership of the source voice by fading it out }
declare src_vo := legato.get_src_vo(self)
declare src_fade := fades.curve_out(src_vo, 80, -30, TRUE)
ivls.play(src_fade)
{ Play the destination voice with a fade in }
declare dest_vo := ivls.new_formal_voice(self)
declare dest_fade := fades.curve_in(dest_vo, 60, -20)
ivls.play(dest_fade)
ivls.play(dest_vo)
else
{ No legato — play normally }
ivls.pass()
end if
end nodeThe kill_on_end = TRUE parameter on fades.curve_out means the fade will release the source voice when it completes. This closes the ownership gap — the source voice's lifetime is managed from the moment you create the fade until the fade ends.
If you don't handle the source voice here, it hangs indefinitely. There is no safety net.
4. Register nodes in the flow¶
Register Stl.MonoLegato followed by your playback node. Do NOT include Stl.LegatoSwitch — that's a stock response for simple switching behavior. Your playback node replaces it.
node MyProduct.Flows:
cb Flows:
define FLOWS += my.legato_flow, my.sound_flow
{ Legato flow: detect, then handle }
ivls.register_node(my.legato_flow, Stl.MonoLegato)
ivls.register_node(my.legato_flow, MyProduct.Play.Legato)
{ Sound output }
ivls.register_node(my.sound_flow, MyProduct.PlayEvent)
{ Legato voices end up in the sound flow for final playback }
ivls.register_move(my.legato_flow, my.sound_flow)
end nodeStl.MonoLegato assigns the links. Your node reads them and handles the musical transition. Voices that pass through continue into the sound flow for playback.
5. Route voices into the legato flow¶
Use a Divert node to send voices into the legato flow when legato is enabled:
node MyProduct.Divert.PlayMode:
cb NotePass:
if my.legato_enabled = TRUE
ivls.reflow_voice(self, my.legato_flow)
else
ivls.reflow_voice(self, my.sound_flow)
end if
end node6. (Optional) Use LegatoSwitch for quick prototyping¶
If you want basic switch-style legato without writing a custom playback node, you can use Stl.LegatoSwitch as a drop-in response. It releases the source voice immediately (hard switch, no crossfade) and unlinks the pair:
ivls.register_node(my.legato_flow, Stl.MonoLegato)
ivls.register_node(my.legato_flow, Stl.LegatoSwitch)This is useful for prototyping or for instruments where a hard switch is musically appropriate. For production instruments that need crossfades, replace LegatoSwitch with your own playback node as shown in step 3.
Legato API¶
| Function | Returns | Purpose |
|---|---|---|
legato.found(self) |
boolean | TRUE if this voice has an active legato link |
legato.get_src_vo(self) |
integer | Voice index of the previous (source) voice |
legato.get_dest_vo(self) |
integer | Voice index of the current (destination) voice |
legato.get_interval(self) |
integer | Signed semitone distance between source and destination |
legato.unlink(self) |
— | Clears the legato source link on this voice |
Verify¶
Play two overlapping notes on your keyboard:
- The first note should trigger normally
- The second note, played while the first is held, should trigger a crossfade (source fades out, destination fades in)
- Releasing the second note while still holding the first should restore the first note (mono legato voice stack behavior)
Further reading¶
- Guide: Legato — conceptual details on mono vs. poly legato and the ownership pattern
- Guide: Ownership — the three ownership models and how to prevent hung voices
- Guide: Fades — the
fades.curve_in/fades.curve_outAPI