User Script Examples - II

And that’s one area where Falcon wins. Please give us mid i out!

3 Likes

MIDI Out in combination with Lua scripting would offer endless possibilities for MIDI generation and manipulation!

2 Likes

Hi all, can someone explain how to use the .halmod presets please? I’ve got a decent handle on HALion 7, Macro editing also. My understanding is these halmod presets are for the midi modules somehow? i cant work out how to use them…i can’t import them into the Lua Script module. Any help much appreciated

I have vibe coded a recursive program information dumper. With this, you can just create a program manually and dump it to see what the values, parameter ranges etc. are.

-- Script to Dump Full Program Structure & Detailed Parameters (Subtype Aware)

-- Pretty print a parameter value through its ParameterDefinition
local function prettyVal(element, paramName, value)
    if not element or not element.getParameterDefinition then
        return tostring(value)
    end

    local okDef, def = pcall(element.getParameterDefinition, element, paramName)
    if okDef and def and def.getDisplayString then
        local okStr, str = pcall(def.getDisplayString, def, value)
        if okStr and type(str) == "string" and str ~= "" then
            return string.format("%s(%d)", str, value)
        end
    end
    return tostring(value)   -- fallback
end


-- Helper Function: Recursively Dumps Table Contents (Values)
function dumpTableContents(tbl, indent, maxDepth, currentDepth)
    currentDepth = currentDepth or 1
    if currentDepth > maxDepth then print(indent .. "[Max recursion depth reached]"); return end
    local entryCount = 0; local pairs_success = pcall(function() for _ in pairs(tbl) do entryCount = entryCount + 1 end end)
    if not pairs_success then print(indent .. "(Error iterating table)"); return end
    if entryCount == 0 then print(indent .. "(Table is empty)"); return end
    print(indent .. "Table Content:")
    local kvIndent = indent .. "  "
    for k, v in pairs(tbl) do
        local keyStr = tostring(k); local valueStr = "[Unknown Type]"
        if type(v) == "number" then local ok, fv = pcall(string.format, "%.4f", v); valueStr = ok and fv or tostring(v)
        elseif type(v) == "string" then valueStr = string.format("%q", v)
        elseif type(v) == "boolean" then valueStr = tostring(v)
        elseif type(v) == "table" then print(string.format("%sKey: %s => Value: [Nested Table]", kvIndent, keyStr)); dumpTableContents(v, kvIndent .. "  ", maxDepth, currentDepth + 1); valueStr = nil
        elseif type(v) == "userdata" then valueStr = "[Userdata Object]"
        elseif v == nil then valueStr = "nil"
        else valueStr = tostring(v) end
        if valueStr then print(string.format("%sKey: %s => Value: %s", kvIndent, keyStr, valueStr)) end
    end
end

-- == Recursive Function: Dumps Element Details & DETAILED Parameters ==
function dumpElementDetailed(element, indent)
    if not element then return end
    indent = indent or ""

    -- Get basic info & attempt subtype detection (Unchanged)
    local nameStr = element.name or "[Unnamed]"
    local typeStr = element.type or "[Unknown Type]"; if typeStr == "" then typeStr = "[Empty Type]" end
    local subTypeStr = ""
    -- ... (Logic to set subTypeStr using .moduleType/.isAuxBus - Unchanged) ...
    if typeStr == "Effect" or typeStr == "MidiModule" then
        local mt_success, mt_value = pcall(function() return element.moduleType end)
        if mt_success and mt_value and mt_value ~= "" then subTypeStr = mt_value end
    elseif typeStr == "Bus" then
         local ab_success, ab_value = pcall(function() return element.isAuxBus end)
         if ab_success then subTypeStr = ab_value and "Aux Bus" or "Main/Group Bus" end
    end

    -- Print hierarchy line (Unchanged)
    if subTypeStr ~= "" then print(string.format("%s- Name: '%s', Type: %s (%s)", indent, nameStr, typeStr, subTypeStr))
    else print(string.format("%s- Name: '%s', Type: %s", indent, nameStr, typeStr)) end
    local nextIndent = indent .. "  "

    -- === Dump ALL Parameters for THIS element with DETAILS ===
    local pcall_success_defs, paramDefs = pcall(function() return element.parameterDefinitions end)
    if pcall_success_defs and paramDefs and type(paramDefs) == "table" and #paramDefs > 0 then
        print(string.format("%sParameters (%d):", nextIndent, #paramDefs))
        local paramIndent = nextIndent .. "  "
        for i, def in ipairs(paramDefs) do
             -- *** MODIFIED PART: Extract and Format Definition Details ***
             local paramName = def.name or "[No Name]"
             local paramLongName = def.longName or ""
             local paramId = def.id or -1
             local paramType = def.type or "[No Type]"
             -- Safely format Min/Max/Default based on type
             local minValStr, maxValStr, defaultValStr = "N/A", "N/A", "N/A"
             if def.min ~= nil then if type(def.min)=="number" then minValStr=string.format("%.4f",def.min) elseif type(def.min)=="boolean" then minValStr=tostring(def.min) else minValStr="[?]" end end
             if def.max ~= nil then if type(def.max)=="number" then maxValStr=string.format("%.4f",def.max) elseif type(def.max)=="boolean" then maxValStr=tostring(def.max) else maxValStr="[?]" end end
             if def.default ~= nil then
                 if type(def.default)=="number" then defaultValStr=string.format("%.4f",def.default)
                 elseif type(def.default)=="boolean" then defaultValStr=tostring(def.default)
                 elseif type(def.default)=="string" then defaultValStr=string.format("%q",def.default)
                 else defaultValStr = "[Other Type]" end
             end
             local unitStr = (def.unit ~= nil and def.unit ~= "") and string.format(" Unit:%q", def.unit) or ""
             -- *** End Definition Detail Formatting ***

             -- Get Current Value
             local getSuccess, value = pcall(element.getParameter, element, def.id)
             local valueStr = "[Read Error]"
             local valueIsTable = false
             if getSuccess then
                  valueIsTable = (type(value) == "table")
                  -- Format value for display
                  if valueIsTable then valueStr = string.format("[Table (%d entries)]", #value) -- Use simple length for summary
                  elseif type(value) == "number" then local ok, fv = pcall(string.format, "%.4f", value); valueStr = ok and fv or tostring(value)
                  elseif type(value) == "string" then valueStr = string.format("%q", value)
                  elseif type(value) == "userdata" then valueStr = "[Userdata Object]"
                  elseif type(value) == "boolean" then valueStr = tostring(value)
                  elseif value == nil then valueStr = "nil"
                  else valueStr = tostring(value) end
             else
                 valueStr = string.format("[Error: %s]", tostring(value))
             end

             -- Print combined parameter definition and value line
             print(string.format("%s%d: '%s' {ID:%d, Type:%s, Range:[%s]-[%s], Default:%s%s} = %s",
                                 paramIndent, i, paramName, paramId, paramType,
                                 minValStr, maxValStr, defaultValStr, unitStr,
                                 valueStr))
             -- Optionally print long name if different
             -- if paramLongName ~= "" and paramLongName ~= paramName then print(string.format("%s    Long Name: '%s'", paramIndent, paramLongName)) end

             -- If the value was a table, call the helper to dump its contents AFTER the main line
             if getSuccess and valueIsTable then
                  dumpTableContents(value, paramIndent .. "  ", 3) -- Indent table contents further
             end
        end
    else
         print(string.format("%s(No parameter definitions listed or accessible)", nextIndent))
    end
	
	-- Print output routing if configured
	if typeStr == "Layer" or typeStr == "Zone" then
		local okBus, bus = pcall(element.getOutputBus, element)
		if okBus and bus then
			print(string.format("%s  -> Output -> Bus '%s'", indent, bus.name or "[Unnamed]"))
		end
	end

    -- Print sample
    if typeStr == "Zone" then
        local okF, filePath = pcall(element.getParameter, element, "SampleOsc.Filename")
        if okF and type(filePath) == "string" and filePath ~= "" then
            print(string.format("%sSample File : %s", indent, filePath))
        end
    end
	
    -- Mapping information that lives in attributes, not parameters
    if typeStr == "Zone" then
        local function attr(name, default)
            local ok, v = pcall(function() return element[name] end)
            return (ok and v ~= nil) and v or default
        end

        local kLow  = attr("keyLow" , 0)
        local kHigh = attr("keyHigh", 127)
        local root  = attr("rootKey", math.floor((kLow + kHigh) / 2))  -- mid key
        local vLow  = attr("velLow" , 0)
        local vHigh = attr("velHigh", 127)

        print(string.format(
            "%sMapping     : Keys %d-%d  (Root %d)   Vel %d-%d",
            indent, kLow, kHigh, root, vLow, vHigh
        ))
    end

    -- print modulation matrix
	if typeStr == "Zone" then
		dumpZoneModMatrix(element, nextIndent)
	end

    -- === Find Children and Recurse (Logic unchanged) ===
    local children = nil
    local childrenDescription = "(Leaf Node or Child Type Not Parsed)"
    -- ... (child finding logic - same as previous script) ...
     if typeStr == "Program" then
         local success_c, result_c = pcall(element.findChildren, element, false); if success_c then children = result_c end
         if children and #children > 0 then childrenDescription = string.format("Direct Children (%d):", #children) else childrenDescription = "(No direct children found)" end
    elseif typeStr == "Layer" then
         children = {}
         local zones_success, zones = pcall(element.findZones, element, false); local others_success, others = pcall(element.findChildren, element, false)
         if zones_success and zones then for _, z in ipairs(zones) do table.insert(children, z) end end
         if others_success and others then for _, o in ipairs(others) do local isZone = false; if zones_success and zones then for _, z in ipairs(zones) do if o == z then isZone = true; break end end end; if not isZone then table.insert(children, o) end end end
         if #children > 0 then childrenDescription = string.format("Children (%d):", #children) else childrenDescription = "(No children found in Layer)" end
    elseif typeStr == "Bus" then
         local effects_success, effects = pcall(element.findEffects, element); if effects_success then children = effects end
         if children and #children > 0 then childrenDescription = string.format("Child Effects (%d):", #children) else childrenDescription = "(No Effects found on this Bus)" end
    end

    print(string.format("%s%s", nextIndent, childrenDescription))
    if children and #children > 0 then
         local childIndent = nextIndent .. "  "
         for i, child in ipairs(children) do
             dumpElementDetailed(child, childIndent) -- Recursive call
         end
    end
end

-- === Helper: dump the assigned Mod‑Matrix rows of a Zone =============
-- Works in the Controller thread (getModulationMatrixRow itself is allowed here).
local function safeNumber(v)
    -- convert nil, strings, userdata, etc. → 0
    return (type(v) == "number") and v or 0
end

------------------------------------------------------------------
--  Helper: dump the Modulation‑Matrix of a Zone
------------------------------------------------------------------
local function safeNum(v) return (type(v) == "number") and v or 0 end

function dumpZoneModMatrix(zone, indent)
    local headerPrinted = false

    for rowIndex = 1, 32 do
        local okRow, row = pcall(zone.getModulationMatrixRow, zone, rowIndex)
        if okRow and row then
            -- fetch numeric parameters
            local dest   = safeNum(select(2, pcall(row.getParameter, row, "Destination.Destination")))
            local src1   = safeNum(select(2, pcall(row.getParameter, row, "Source1.Source")))
            local src2   = safeNum(select(2, pcall(row.getParameter, row, "Source2.Source")))
            local amount = safeNum(select(2, pcall(row.getParameter, row, "Amount")))

            local inUse = (dest ~= 0) or (src1 ~= 0) or (src2 ~= 0) or (amount ~= 0)
            if inUse then
                if not headerPrinted then
                    print(indent .. "Modulation Matrix Rows:")
                    headerPrinted = true
                end

                ----------------------------------------------------------------
                -- Decode Src2 more deeply (Src1 would require processor thread)
                ----------------------------------------------------------------
                local src2Info1, src2Info2
                local okS2, s2a, s2b, s2c = pcall(row.getSource2, row)
                if okS2 then src2Info1, src2Info2 = s2b, s2c end

                -- build readable strings
				local destStr = prettyVal(row, "Destination.Destination", dest)
				local s1Str   = prettyVal(row, "Source1.Source",        src1)
				local s2Str   = prettyVal(row, "Source2.Source",        src2)

                if src2 == ModulationSource.midiControl and src2Info1 then
                    s2Str = s2Str .. " (CC#" .. tostring(src2Info1) .. ")"
                elseif src2 == ModulationSource.quickControl and src2Info1 then
                    local layName = src2Info1.name or "Layer"
                    s2Str = s2Str .. string.format(" (%s, QC %d)", layName, src2Info2 or 0)
                elseif src2 == ModulationSource.modulationModule and src2Info1 then
                    local mmName = src2Info1.name or "MIDI Mod"
                    s2Str = s2Str .. string.format(" (%s, Out %d)", mmName, src2Info2 or 0)
                elseif src2 == ModulationSource.sampleAndHold and src2Info1 then
                    s2Str = s2Str .. " (S&H ".. tostring(src2Info1) ..")"
                end

                -- print final line
                print(string.format(
                    "%s  * Row %02d  Dest=%-24s  Src1=%-24s  Src2=%-24s  Amt=%.3f",
                    indent, rowIndex, destStr, s1Str, s2Str, amount
                ))
            end
        end
    end

    if not headerPrinted then
        print(indent .. "(No modulation rows assigned)")
    end
end



-- Main function to start the detailed dump (Single Pass)
function dumpProgramDetailedFullDefs()
    print("--- Dumping Full Program Structure & Detailed Parameters (v6) ---")
    local prog = this.program
    if not prog then print("Error: Cannot get program!") return end
    dumpElementDetailed(prog, "") -- Start dumping from the program root
    print("\n--- Detailed Dump Finished ---")
end

-- Run
dumpProgramDetailedFullDefs()

Just for some laughs. This is my attempt to feed the parameter knowledge into the LLM and tell it to generate a brass patch using the lua script. There is still some teaching that needs to take place for that to work :wink: I especially love the comment that envelopes are too complex :slight_smile:

-- HALion Script: Create Basic Synth Brass Patch

-- =====================================
--  Find/Create Helpers
-- =====================================
-- Finds the first existing Zone in the program, or creates Layer/Zone if none exist.
function findOrCreateFirstSynthZone()
    local prog = this.program
    if not prog then
        print("Error: Cannot access this.program")
        return nil
    end

    local zones = prog:findZones(true) -- Find zones recursively

    if zones and #zones > 0 then
        print("Found existing zone: ", zones[1].name)
        -- Optional: Ensure it's a Synth Zone
        if zones[1]:getParameter(1) ~= 0 then
             print("Warning: First zone found is not Type 0 (Synth). Setting it anyway.")
             zones[1]:setParameter(1, 0) -- Attempt to force Synth type
        end
        return zones[1]
    else
        -- No zones found, create a Layer and a Synth Zone
        print("No zones found. Creating new Layer and Synth Zone...")
        local layer = Layer()
        if not layer then print("Error: Failed to create Layer object.") return nil end
        layer:setName("Synth Brass Layer")
        prog:appendLayer(layer)
        print("Created Layer: ", layer.name)

        local zone = Zone()
        if not zone then print("Error: Failed to create Zone object.") return nil end
        zone:setName("Synth Brass Zone")
        zone:setParameter(1, 0) -- Set ZoneType to Synth (0)
        layer:appendZone(zone)
        print("Created Synth Zone: ", zone.name)
        return zone
    end
end

-- Syntactic sugar for setParameter
local function p(z, name, value)
    -- Use pcall for safety, especially with parameter names/ranges
    local success, err = pcall(z.setParameter, z, name, value)
    if not success then
         print(string.format("Warning: Failed to set parameter '%s' to %s. Error: %s", name, tostring(value), tostring(err)))
    end
end

-- =====================================
--  Brass voicing recipe
-- =====================================
function voicingBrass(zone)
    if not zone then return end
    print("Applying brass voicing to zone: ", zone.name)

    -- ------- Oscillators -------
    print("Setting Oscillators...")
    local sawOscType = 2 -- Correct value for Sawtooth
    -- Osc 1
    p(zone, "Osc 1.On", true)
    p(zone, "Osc 1.Type", sawOscType) -- Use correct type for Saw
    p(zone, "Osc 1.Mix", 70.0)       -- Assuming 0-100 range

    -- Osc 2
    p(zone, "Osc 2.On", true)
    p(zone, "Osc 2.Type", sawOscType) -- Use correct type for Saw
    p(zone, "Osc 2.Mix", 60.0)       -- Assuming 0-100 range
    p(zone, "Osc 2.Fine", 5.0)        -- Detune (+/- 50 range assumed?)

    -- Osc 3
    p(zone, "Osc 3.On", false)        -- Ensure Osc 3 is off

    -- Sub Osc
    p(zone, "SubOscOn", true)
    p(zone, "SubOscWave", 2)          -- Assuming 0=Sine, 1=Tri, 2=Saw? Default was 0 (Sine). User request used 2 (Tri). Let's try 1 for Triangle.
    -- Correcting SubOscWave based on common assignments (0=Sine, 1=Triangle, 2=Saw, 3=Square)
    p(zone, "SubOscWave", 1)          -- Set to Triangle
    p(zone, "SubOscMix", 30.0)        -- Assuming 0-100 range

    -- ------- Noise (breath) -------
    print("Setting Noise...")
    p(zone, "Noise.On", true)
    p(zone, "Noise.Type", 0)          -- Assuming 0=White
    p(zone, "Noise.Level", 3.0)       -- Assuming 0-100 range, subtle

    -- ------- Filter -------
    print("Setting Filter...")
    -- Filter.Type = 1 was used in user script. Let's assume 0=LP24, 1=LP12? Use 0 for stronger effect.
    p(zone, "Filter.Type", 0)         -- Try LP 24dB
    p(zone, "Filter.Cutoff", 30.0)    -- Start Low (0-100 range assumed). TUNE!
    p(zone, "Filter.Resonance", 20.0) -- Moderate Resonance (0-100 range assumed)
    p(zone, "Filter.Keytrack", 75.0)  -- Key Tracking (0-100 range assumed)
    p(zone, "Filter.EnvAmount", 60.0) -- Apply Filter Env Amount (0-100 range assumed)

    -- ------- Envelopes -------
    print("Skipping custom envelope settings (using defaults). API requires complex table manipulation.")
    -- Setting simple ADSR values directly via setParameter seems unsupported.
    -- The script previously attempted to set EnvelopePoints tables, which might work but is complex to verify/debug here.
    -- Relying on default envelopes + Filter.EnvAmount for now.
    -- Applying velocity sensitivity based on previous findings:
    p(zone, "Amp Env.VelocityToLevel", 60.0)
    p(zone, "Filter Env.VelocityToLevel", 50.0)

    -- ------- Vibrato (Mod Wheel) -------
    print("Skipping Mod Matrix setup for Vibrato (requires complex API calls).")
    -- Setting LFO params but not routing:
    p(zone, "LFO 1.WaveForm", 0)      -- Assuming 0 = Sine for LFO
    p(zone, "LFO 1.Rate", 5.8)        -- ~6 Hz
    p(zone, "LFO 1.Trigger", 1)       -- Retrigger mode? (Sync/Free?)
    p(zone, "LFO 1.SharedPhase", true)-- Seems unlikely for Vibrato? Default is false. Let's try false.
    p(zone, "LFO 1.SharedPhase", false)
    -- Routing requires getModulationMatrixRow and setting row properties.

    -- ------- Global Level & Pan -------
    print("Setting Amp Level/Pan...")
    p(zone, "Amp.Level", -6.0) -- Assuming dB range
    -- Check if Amp.Pan exists or if it's just Pan global to the zone? Dump has "Amp.Pan" ID 3276820
    p(zone, "Amp.Pan", -5.0)   -- Assuming -100 to +100 range?
end

-- =====================================
--  Entry point - Use onLoadIntoSlot
-- =====================================
function onLoadIntoSlot() -- Changed from onLoad
    print("Brass Patch Generator: Executing onLoadIntoSlot...")
    local z = findOrCreateFirstSynthZone() -- Use robust find/create function
    if not z then
        print("Brass Patch Generator: Failed to find or create a zone.")
        return
    end
    voicingBrass(z) -- Apply voicing to the found/created zone
    print("Brass Patch Generator: Zone voicing attempted.")
end

-- Immediately call the entry point for testing in editor
onLoadIntoSlot()

Does anyone know how to programmatically set the algorithm (either pre-configured algorithm like “DX7 22” or custom algorithm configurations) for an FM zone in LUA? It doesn’t seem to be in the parameters if I’m not mistaken.

You can try this:

Aaah, thanks a lot. That I have missed.