Like this lua script that I asked for in Copilot? It opened a window into Chat GPT and came up with this.
scale assistant lua script for halion
--[[
HALion Scale Assistant 1.04
Generated via Chat GPT on 9/9/2026
Forces incoming MIDI notes into a selected scale.
--]]
-- Define available scales (intervals in semitones from root)
local scales = {
["Major"] = {0, 2, 4, 5, 7, 9, 11},
["Natural Minor"] = {0, 2, 3, 5, 7, 8, 10},
["Harmonic Minor"]= {0, 2, 3, 5, 7, 8, 11},
["Pentatonic"] = {0, 2, 4, 7, 9},
["Blues"] = {0, 3, 5, 6, 7, 10}
}
-- Build sorted list of scale names for UI
local scaleNames = {}
for name, _ in pairs(scales) do
table.insert(scaleNames, name)
end
table.sort(scaleNames)
-- Parameters exposed to HALion UI
defineParameter("rootNote", "Root Note", 0, 0, 11, function() end) -- 0 = C, 1 = C#, etc.
defineParameter("scaleName", "Scale", 0, 0, #scaleNames - 1, function() end)
-- Mapping: originalNote -> mappedNote
local noteMap = {}
-- Counter: mappedNote -> active count
local noteCount = {}
-- Helper: force note into scale
local function forceToScale(note, root, scaleIntervals)
local noteInOctave = (note - root) % 12
local closest = scaleIntervals[1]
local minDiff = 12
for _, interval in ipairs(scaleIntervals) do
local diff = (noteInOctave - interval) % 12
if diff < minDiff then
minDiff = diff
closest = interval
end
end
return note - noteInOctave + closest
end
-- Handle Note On
function onNote(event)
local selectedScale = scales[scaleNames[scaleName + 1]]
if not selectedScale then
postEvent(event)
return
end
local mappedNote = forceToScale(event.note, rootNote, selectedScale)
-- Store mapping for this original note
noteMap[event.note] = mappedNote
-- Increment count for mapped note
noteCount[mappedNote] = (noteCount[mappedNote] or 0) + 1
-- Send Note On for mapped note
local newEvent = clone(event)
newEvent.note = mappedNote
postEvent(newEvent)
end
-- Handle Note Off
function onRelease(event)
local mappedNote = noteMap[event.note]
if mappedNote then
-- Decrement count for mapped note
noteCount[mappedNote] = (noteCount[mappedNote] or 0) - 1
-- Only send Note Off when no more originals are holding it
if noteCount[mappedNote] <= 0 then
noteCount[mappedNote] = nil
local newEvent = clone(event)
newEvent.note = mappedNote
postEvent(newEvent)
end
-- Remove mapping for this original note
noteMap[event.note] = nil
else
-- Pass through if no mapping
postEvent(event)
end
end
function onInit()
print("Scale Assistant Script Loaded (Chord-Safe)")
print("Available Scales: " .. table.concat(scaleNames, ", "))
end
So remember, you don’t need to be a scripting/coding wizard to build things anymore. It took less than 10 minutes to get something that seems to be usable, test it in HALion, and post it here. All I did was enter plain English prompts into an AI (free access in this case, I got to it through a Copilot query).
I just gave it a try with rootNote as the default of 0 (C), and again at 5 (F), and it seems to work. This script is set up to play the closest note down if something out of scale is pressed. One could ask GPT to change that behavior, or to put in different options (play higher, play nothing, play a random note still in scale, etc.).
Since I have no macro established I found the parameters to tweak by opening a parameter list:
On scaleName value:
0 = Pentatonic
1 = Harmonic Minor
2 = Major
3 = Natural Harmonic
4 = Blues
I’m not sure if this script got the naming part right so scale names instead of a number would show up in macro pages. It seems like it does set up a table for that but I’m wondering if it could be done in the same pass as defining the parameter and having it ready to show up in a column there? It’s been a while since I personally made a script that set up a name field like that, so I can’t remember how to check that as I sit here.
If it got it wrong it shouldn’t be too hard to correct with a couple of prompts into the AI, or just tweak it to whatever your Macro editor needs.