Skip to content

Portamento


Legato connects two notes. Portamento makes the connection audible as a continuous pitch glide -- the kind of slide you hear from a fretless bass, a trombone, or a vocalist bending between pitches. Implementing this in a sampler means manipulating the tuning of a live voice in real time, millisecond by millisecond, while handling ownership correctly when new notes arrive mid-glide.

IVLS provides Stl.Portamento.Template, a template node that handles the glide infrastructure. You inherit from it and fill in two virtual callbacks to control when and how glides occur.

The Template and Its Virtuals


Stl.Portamento.Template is a NoteOn/NoteOff node that you extend with from:

node MyPortamento from Stl.Portamento.Template:
    cb AllowInitPorta:
        { Decide if a non-legato note should glide from the last pitch }

    cb PortaFound:
        { Perform the actual glide }
end node

The template handles the structural work: detecting legato links, setting up modbit tuning offsets, tracking the last pitch per thread. Your product fills in two virtual callbacks:

AllowInitPorta -- called on non-legato notes. Set porta_on_init := TRUE if the instrument should glide even from a fresh note (using the last played pitch on that thread). Set it to FALSE (or leave it) for fingered-only portamento where glides only happen on overlapping notes.

PortaFound -- called when a glide should occur (either legato was detected, or AllowInitPorta returned TRUE). This is where you perform the actual pitch change. The template provides several local variables for your use: init_vo, init_note, new_note, and mb (the modbit handle for tuning).

Ownership: Seize and Check


When a new note arrives mid-glide, who owns the tuning modbit? The old glide loop is still running, and the new note wants to start its own glide on the same voice. Without coordination, they would fight over the same tuning parameter.

The portamento system solves this with an ownership protocol:

portamento.seize_tuning_for(init_vo, self) -- the new note claims ownership of the tuning modbit on the init voice. After this call, the new note's voice is the registered owner.

portamento.still_owned_by(init_vo, self) -- checked inside the glide loop. If another voice has seized ownership, this returns FALSE and the loop exits cleanly.

{ Inside PortaFound }
portamento.seize_tuning_for(init_vo, self)

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

This pattern -- seize, then loop while still owned -- is the standard way to handle overlapping portamento glides. When a third note arrives, it seizes ownership, and the second note's loop exits on the next iteration.

The Modbit Glide Loop


The actual pitch glide is a loop that adjusts a modbit tuning offset every millisecond. A modbit is a per-voice modulation value attached to a specific parameter (in this case, tuning). The template creates one via Modbit.local_modbit() and adds it to the destination voice.

The loop runs at one-millisecond resolution using ivls.wait_ms(1):

declare current_tune := Modbit[mb].value
declare distance := target_tune_offset - current_tune
declare stride := distance / time

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

The path_on check ensures the loop stops if the voice is released. The still_owned_by check ensures it stops if a new note takes over. Together, they make the glide loop safe against both user input and system cleanup.

Tuning values are in millicents -- 100,000 per semitone. A glide from C3 to E3 (four semitones) targets an offset of 400,000 millicents.

Production Variants


The Polycore product demonstrates three different portamento behaviors, all built from the same template:

Polycore.Play.MonoSwitch -- no glide at all. When legato is detected, it releases the old voice and starts a fresh one. This gives a "switch" legato where notes connect without pitch bending.

Polycore.Play.PortaInstant -- instant portamento. The tuning offset snaps to the target in a single step. No loop, no glide time -- just an immediate pitch shift.

Polycore.Play.PortaTimed -- timed portamento with a configurable glide duration. Supports both constant-time mode (all glides take the same duration regardless of interval) and constant-rate mode (glide speed is fixed, so larger intervals take longer). Also supports fingered vs. non-fingered portamento via the AllowInitPorta callback.


Portamento builds on top of the legato system. Stl.MonoLegato handles the voice assignment. Stl.Portamento.Template handles the tuning manipulation infrastructure. Your product's node -- inheriting from the template -- decides the musical character of the glide: how fast, whether from fresh notes, and what to do when notes overlap during a glide.