Skip to content

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.Legato

2. 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 node

The 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 node

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

6. (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_out API