Skip to content

How to Add Portamento

Add pitch glide between notes so that when a legato transition is detected, the sounding voice smoothly slides from the source pitch to the destination pitch.

Prerequisites

  • A working IVLS product with at least one flow and Stl.PlayEvent
  • Legato detection already set up (Stl.MonoLegato registered in a flow) -- see How to Add Legato
  • Stl.Legato included via Ivls.STL (part of the standard library bundle)
  • Familiarity with portamento concepts and Modbits

Steps

1. Add the portamento core node to IVLS_NODES

Stl.Portamento.Core declares the per-voice portamento fields (porta.init_note, porta.init_vo, porta.modbit, porta.run_owner) and the helper functions. Include it in your product's node list:

define IVLS_NODES := ..., Stl.Portamento.Core, ...

2. Create a portamento playback node from the template

Inherit from Stl.Portamento.Template and implement two virtual callbacks: AllowInitPorta (controls whether a non-legato note starts a portamento glide) and PortaFound (runs the actual pitch glide when a legato transition is detected).

node MyProduct.Play.Portamento from Stl.Portamento.Template:
    cb AllowInitPorta:
        { Set porta_on_init := TRUE to glide even on the first note (auto mode) }
        { Set porta_on_init := FALSE for fingered mode (glide only on overlapping notes) }
        porta_on_init := FALSE

    cb PortaFound:
        { Seize tuning control for this voice }
        portamento.seize_tuning_for(init_vo, self)

        { Calculate the target tune offset in millicents }
        declare target_tune := (new_note - init_note) * 100000

        { Glide over time (in ms) }
        declare glide_time := my.porta_time
        if glide_time < 2
            { Instant snap }
            Modbit.set(mb, target_tune)
        else
            { Timed glide: ramp the modbit from current to target }
            declare current_tune := Modbit[mb].value
            declare stride := (target_tune - current_tune) / glide_time
            declare t

            for t := 0 to glide_time
                if path_on and portamento.still_owned_by(init_vo, self) = TRUE
                    Modbit.set(mb, current_tune + stride * t)
                    ivls.wait_ms(1)
                else
                    t := glide_time
                end if
            end for

            { Snap to exact target }
            if path_on and portamento.still_owned_by(init_vo, self) = TRUE
                Modbit.set(mb, target_tune)
            end if
        end if
end node

3. Register the portamento node in a flow

Create a flow that chains Stl.MonoLegato (for legato detection) followed by your portamento node, then moves to your sound output flow:

node MyProduct.Flows:
    cb Flows:
        define FLOWS += my.porta_flow, my.sound_flow

        { Portamento flow: detect legato, then apply pitch glide }
        ivls.register_node(my.porta_flow, Stl.MonoLegato)
        ivls.register_node(my.porta_flow, MyProduct.Play.Portamento)
        ivls.register_move(my.porta_flow, my.sound_flow)

        { Sound output flow }
        ivls.register_node(my.sound_flow, Stl.PlayEvent)
end node

4. Route voices with a Divert node

Use a Divert node to send voices to either the portamento flow or the normal sound flow based on a portamento on/off parameter:

node MyProduct.Divert.PlayMode:
    cb NotePass:
        if my.portamento_enabled = TRUE
            ivls.reflow_voice(self, my.porta_flow)
        else
            ivls.reflow_voice(self, my.sound_flow)
        end if
end node

Register the divert node in your base flow so it runs before the sound path:

ivls.register_node(my.base_flow, MyProduct.Divert.PlayMode)

Key variables available in PortaFound

The template makes these variables available inside your cb PortaFound:

Variable Type Purpose
init_vo integer The voice that holds the initial pitch modbit
init_note integer The MIDI note the glide starts from
new_note integer The MIDI note the glide targets
mb integer The Modbit handle controlling the pitch offset
self integer The current voice triggering the legato
src integer The source (previous) voice in the legato chain
dest integer The destination voice in the legato chain

Key portamento functions

Function Purpose
portamento.set_init_voice(vo) Creates the initial pitch modbit and assigns it to the voice
portamento.connect_data(src, dest) Copies portamento fields from source to destination voice
portamento.seize_tuning_for(init_vo, vo) Takes ownership of the pitch modbit so this voice controls the glide
portamento.still_owned_by(init_vo, vo) Returns TRUE if this voice still owns the pitch modbit

Verify

  1. Enable portamento and play two overlapping notes -- the second note should glide from the first note's pitch
  2. Play the same two notes without overlapping -- no glide should occur (in fingered mode)
  3. If using auto mode (porta_on_init := TRUE), the first note of a new phrase should also glide from the last played pitch
  4. Adjust glide time -- shorter values should produce snappier transitions, longer values a slower slide

Further reading

  • Guide: Portamento for conceptual details on fingered vs. auto portamento
  • How to: Add Legato for setting up the legato detection that portamento depends on
  • Guide: Modbits for understanding how Modbit.set() drives real-time pitch changes