And that’s one area where Falcon wins. Please give us mid i out!
MIDI Out in combination with Lua scripting would offer endless possibilities for MIDI generation and manipulation!
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
I especially love the comment that envelopes are too complex ![]()
-- 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.