Skip to content

Functions

IVLS-KSP supports two function types: standard functions (inlined) and task functions (coroutines with a real call stack).

Standard Function Syntax

function name(param1, param2, ...)
    // function body
end function
function ivls._fire_voice(vo)
    ivls._refurbish_voice(vo)
    Voice[vo].input := ivls.input_type.VOICE_ON
    Voice[vo].note_on_time := ENGINE_UPTIME
    inc(Voice[vo].stage)
    ivls._process_voice(vo)
end function

Inlining Semantics

Standard functions are inlined via textual substitution. There is no call stack and no recursion. At compile time, each function call is replaced with the function body, and parameter names are substituted with the caller's arguments.

Because of inlining, modifying a parameter inside a function directly modifies the caller's variable -- this is a natural consequence of textual substitution, not a separate "pass by reference" mechanism.

function math.set_nth_bit(B, N, val)
    B := BOOL.CHOOSE(val, ...
                    sh_left(1, N) .or. B, ...
                    (math.ALLMASK - sh_left(1, N)) .and. B ...
                )
end function

Here, assigning to B modifies the caller's variable because B is textually replaced with whatever was passed in.

function math.rand_seeded(min, max, seed) -> return
    seed := 8088405 * seed + 1
    return := (sh_right(seed, 8) .and. 0xFFFFFF) mod (max - min + 1) + min
end function

When called as math.rand_seeded(0, 100, my_seed), the seed parameter is the caller's my_seed variable after inlining, so my_seed is updated.

Return Values

Functions return values using the -> return syntax. The return variable is assignable within the function body:

function math.amp_to_db(a) -> return
    return := (20.0 * math.log10(a))
end function

Named Return Variables

Return variables can have descriptive names:

function math.equal_power(in) -> ratio
    ratio := sqrt(sin(0.5 * NI_MATH_PI * in))
end function
function ivls.RoundRobins.query_rr(vo) -> rr_pos
    declare rr_idx := ivls.RoundRobins.query_rr_idx(vo)
    declare max := ivls.RoundRobins.hooks.get_max_rr(vo)

    if not in_range(rr_idx, 0, rr.MAX_IDX)
        message("RoundRobins.query_rr(): Invalid rr_idx found: " & rr_idx)
        rr_pos := -1
    else if max = 0
        rr_pos := 0
    else
        rr_pos := rr.main_seed[max - 1, rr.counter[rr_idx]]
    end if
end function

Local Variables

Local variables are declared within a function using declare:

function ivls._refurbish_voice(v)
    declare i
    for i := 0 to num_elements(Voice.RESET_ON_NEW) - 1
        Voice[v].access[Voice.RESET_ON_NEW[i]] := Voice.INIT[Voice.RESET_ON_NEW[i]]
    end for
end function

Float locals use the ~ prefix:

function math.get_timestretched_offset(root_note, result_note, ideal_transient, extra_offset) -> result
    declare ~stretch_factor := pow(2.0, float(root_note - result_note) / 12.0)
    declare ~perceptual_transient := ideal_transient * ~stretch_factor
    declare ~converted_offset := extra_offset / ~stretch_factor
    declare ~corrective_offset := (perceptual_transient - ideal_transient) / ~stretch_factor
    result := corrective_offset + converted_offset
end function

Task Functions (Coroutines)

Task functions have a real TCM call stack, support var/out parameter modifiers, and can recurse. They can yield execution using wait operations.

taskfunc name(params)
    // function body with potential wait operations
end taskfunc

The var Keyword

Task functions use var to explicitly mark by-reference parameters:

taskfunc ivls.send_voice(var self, var self_invalid, vo_input, nenv)
    declare vo_to_send
    declare vo_as_parent
    NodeEnv[nenv].sentVoice := -1

    if vo_input = self
        message('Calling ivls.play() on \'self\' not supported in IVLS!')
        self_invalid := TRUE
    end if

    if not check_ref(Voice, self)
        vo_as_parent := ivls.omni_voice
        vo_to_send := vo_input
    else
        vo_as_parent := self
        if vo_input = self
            vo_to_send := ivls.new_voice(self)
        else
            vo_to_send := vo_input
        end if
    end if

    if not check_ref(Voice, vo_to_send)
        if vo_to_send = self
            message('Node ' & ivls.node_name(nenv) & ' tried to play passive input after release!')
        else
            message('Node ' & ivls.node_name(nenv) & ' tried to play invalid Voice!')
            cluster.Dump(Voice, self)
        end if
    else if self_invalid = TRUE
        message('IVLS has been halted at node ' & ivls.node_name(nenv))
        cluster.Dump(Voice, self)
        Voice.delete(vo_to_send)
    else if NodeEnv[nenv].info.path_cancelled = TRUE and not (Voice[vo_to_send].duration > 0)
        if vo_input = self
            Voice.delete(vo_to_send)
        end if
    else
        if Voice[vo_to_send].stage < ivls.flow_lengths[Voice[vo_to_send].flow] - 1
            ivls._fire_new_child(vo_to_send, vo_as_parent)
            NodeEnv[nenv].sentVoice := vo_to_send
        else
            Voice.delete(vo_to_send)
        end if
    end if
end taskfunc
  • var self and var self_invalid are passed by reference (changes reflect in the caller)
  • vo_input and nenv are passed by value

Taskfunc with Waiting

taskfunc #name#.process_label_revert(#name#.DIMS)
    #name#.par_label_in_use[#name#.DIMS] := TRUE
    tcm.wait(UI.PARAM_DISP_DELAY_MS * 1000)
    if #name#.par_label_in_use[#name#.DIMS] = TRUE
        #name#.par_ctrl_label[#name#.DIMS] -> text := #name#.par_label_original_text[#name#.DIMS]
        #name#.par_label_in_use[#name#.DIMS] := FALSE
    end if
end taskfunc

Calling Conventions

// Basic calls
ivls._fire_voice(vo)
math.rand_reseed()

// Capturing return values
declare result := math.clamp(value, 0, 100)
declare db := math.amp_to_db(0.5)

// Return values in expressions
Voice[vo].rr := ivls.RoundRobins.query_rr(vo)
return := math.min(math.max(num, min), max)

Function Namespacing

IVLS-KSP uses dotted names for logical namespaces. At compilation, dots become double underscores:

math.amp_to_db(a)           // Math library
ivls.add_child_to_parent()  // IVLS voice operations
ivls.RoundRobins.query_rr() // Round-robin functions

Type-associated functions follow the pattern Type.function_name:

function Voice.new() -> result
function Voice.delete(ref)
function Voice.copy(src) -> result

Properties with Functions

Properties use functions for getters and setters, enabling the bracket accessor syntax:

property #type#.#field#
    function get(pointer) -> result
        result := #type#.pool[pointer * #type#.BLOCK + #type#.field.#field#]
    end function

    function set(pointer, value)
        #type#.pool[pointer * #type#.BLOCK + #type#.field.#field#] := value
    end function
end property

Usage: Voice[ref].note calls get(ref), Voice[ref].note := 60 calls set(ref, 60).

Hook Functions

Hook functions provide overridable extension points:

// Default implementation
function ivls.RoundRobins.hooks.get_max_rr(vo) -> rr_max
    rr_max := 0
end function

// Product code override
function ivls.RoundRobins.hooks.get_max_rr(vo) -> max
    max := get_sample_rr_count(Voice[vo].note, Voice[vo].dyn_layer)
end function

See Also

  • Macros - Compile-time code generation
  • Control Flow - Loops and conditional statements
  • Callbacks - Event-driven function execution
  • Types - Type-associated functions and properties