How to change MIDI mappings with LUA script?

Hi

I’m building a drum kit in Halion and on the Macro page I would like to have one dropdown menu for each drum element in order to select which midi note will trigger that respective drum element. Let me explain a little more: when I create this drum kit in Halion I use the standard GM drum mapping (i.e.: C1 triggers the bassdrum, D1 the snare, F#1 the closed HiHat, A#1 the open HiHat, and so on) but I want to be able to modify this default mapping from the Macro page by using a dropdown menu for each drum element so I can select a different midi note to trigger each element.

Any ideas for how I can accomplish this?

I would create a parameter to select the midi note for each drum sound and use a table to keep track of the midi mapping. I would use the selected midi note as index and the note the sound is mapped to as value. Update the table each time any of the mapping parameters changes.

Thank you very much for the help @misohoza ! :hugs:

Unfortunately it’s not clear to me how to implement your suggestion. I’m not sure how to “create a parameter to select the midi note”. You mean inserting a dropdown menu element on the macro page? I also don’t know how to “use the selected midi note as index and the note the sound is mapped to as value” and also how to create and update the table. I’m guessing that’s something I have to do in the Lua script but unfortunately I don’t know how to do that. :worried:

Can you please explain a little bit?

Cheers!

I meant something like this:

local mapping = {}
for i = 0, 127 do
	mapping[i] = i
end

local notes = {[0] = "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}

local noteNames = {}
for i = 0, 127 do
	local note = notes[i % 12]
	local octave = math.floor(i / 12) - 2
	noteNames[i] = note .. octave
	print(note .. octave)
end

local lowestNote = 36
local highestNote = 72

for i = lowestNote, highestNote do
	defineParameter("N"..i, nil, i, noteNames, function() mapping[_G["N"..i]] = i end)
end

for i = lowestNote, highestNote do
	mapping[_G["N"..i]] = i
end

function onNote(e)
	playNote(mapping[e.note], e.velocity)
end
1 Like

Thank you thank you thank you @misohoza!! :hugs: You’re a lifesaver! :heart_eyes:

This works great and it’s exactly what I needed! There’s just one problem: after remapping the drum elements to new notes, the “old” notes (the default mapping) still trigger the respective elements, so it’s like having two midi mappings at the same time. For example, after changing the trigger note for the bassdrum from C1 to B2, the note C1 still triggers the bassdrum, so the bassdrum is now triggered by both C1 and B2.

Do you know how I can solve this?

Yes, you are right. And there’s another issue if you pick a note that’s already used.
Maybe try like this:

local mapping = {}
local lastMapping = {}
for i = 0, 127 do
	mapping[i] = {i}
	lastMapping[i] = i
end

local notes = {[0] = "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}

local noteNames = {}
for i = 0, 127 do
	local note = notes[i % 12]
	local octave = math.floor(i / 12) - 2
	noteNames[i] = note .. octave
	print(note .. octave)
end

local lowestNote = 36
local highestNote = 72

function mappingChanged(i)
	local oldNote = lastMapping[i]
	local newNote = _G["N"..i]
	
	for j, note in ipairs(mapping[oldNote]) do
		if note == i then
			table.remove(mapping[oldNote], j)
			break
		end
	end
	
	table.insert(mapping[newNote], i)
	lastMapping[i] = newNote
end


for i = lowestNote, highestNote do
	defineParameter("N"..i, nil, i, noteNames, function() mappingChanged(i) end)
end

for i = lowestNote, highestNote do
	mappingChanged(i)
end

function onNote(e)
	for i, note in ipairs(mapping[e.note]) do
		playNote(note, e.velocity)
	end	
end
1 Like

Yeessss! Now it works perfectly! :star_struck: :partying_face:
Thank you soooooooo much! :hugs: :face_blowing_a_kiss:

Do you think it would be possible to somehow save the custom mapping made by the user and then be used by default for all the presets in the library, or is it way too complicated? In other words if the user wants to have the ride cymbal always triggered by G#3 for example it would be very nice if this setting could be saved as the default mapping and then used for all the presets in the library (without the need for the user to redo the mapping for each preset).

@misohoza Unfortunately there seems to be a problem with that script. When I reopen the program the new mappings are ignored and it reverts back to the default mappings. To be more clear: I set the new custom mappings (for example A#2 for bassdrum, E5 for snare, F#3 for ride) using the dropdown menus for each element and they work just fine. I then save the program and close it. But when I reload that program, even though the new mappings (A#2, E5, F#3) appear in the dropdown menus they are ignored and the mapping is back to default (C1 for bassdrum, D1 for snare, D#2 for ride). What could be the reason for this?

As a note, I’m still on Halion 6 but I doubt this has something to do with it.

I moved the callback that updates the mapping to onLoad
Hopefully it will solve the issue.

I have also added a parameter to save the default mapping. It should create a mapping.txt file in your Documents/Steinberg folder. I haven’t tested it thoroughly yet. You can give it a try. Ideally the settings should persists when switching presets.

local mapping = {}
local lastMapping = {}
local initMapping = {}

for i = 0, 127 do
	mapping[i] = {i}
	lastMapping[i] = i
end

fileLocation = getUserSubPresetPath()
posStart, posEnd = string.find(getUserSubPresetPath(), "Steinberg/")
fileLocation = string.sub(fileLocation, 1, posEnd) .. "mapping.txt"

print(fileLocation)

f, err = io.open(fileLocation, "r")

if f then
	data = f:read("*all")
	for w in string.gmatch(data, "%d+") do
		table.insert(initMapping, tonumber(w))
	end
	f:close()
end


function saveMapping()
	if SaveMapping then
		local f, err = io.open(fileLocation, "w")
		if f then
			f:write(table.concat(lastMapping, " "))
			f:close()
		end
		wait(250)
		SaveMapping = false
	end
end


defineParameter("SaveMapping", nil, false, saveMapping)

local notes = {[0] = "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}

local noteNames = {}
for i = 0, 127 do
	local note = notes[i % 12]
	local octave = math.floor(i / 12) - 2
	noteNames[i] = note .. octave
--	print(note .. octave)
end

local lowestNote = 36
local highestNote = 72

function mappingChanged(i)
	local oldNote = lastMapping[i]
	local newNote = _G["N"..i]

	for j, note in ipairs(mapping[oldNote]) do
		if note == i then
			table.remove(mapping[oldNote], j)
			break
		end
	end

	table.insert(mapping[newNote], i)
	lastMapping[i] = newNote
end


for i = lowestNote, highestNote do
	defineParameter("N"..i, nil, i, noteNames, function() mappingChanged(i) end)
	if initMapping[i] then
		_G["N"..i] = initMapping[i]
	end
end

function onLoad()
	for i = lowestNote, highestNote do
		mappingChanged(i)
	end
end

function onNote(e)
	for i, note in ipairs(mapping[e.note]) do
		playNote(note, e.velocity)
	end	
end
1 Like

Thank you so much!
Unfortunately I get the following error:

Error: Line 10: attempt to call global ‘getUserSubPresetPath’ (a nil value): fileLocation = getUserSubPresetPath()

:thinking:

Yes, that requires Halion 7.

This should hopefully work in Halion 6

local mapping = {}
local lastMapping = {}

for i = 0, 127 do
	mapping[i] = {i}
	lastMapping[i] = i
end


local notes = {[0] = "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}

local noteNames = {}
for i = 0, 127 do
	local note = notes[i % 12]
	local octave = math.floor(i / 12) - 2
	noteNames[i] = note .. octave
--	print(note .. octave)
end

local lowestNote = 36
local highestNote = 72

function mappingChanged(i)
	local oldNote = lastMapping[i]
	local newNote = _G["N"..i]

	for j, note in ipairs(mapping[oldNote]) do
		if note == i then
			table.remove(mapping[oldNote], j)
			break
		end
	end

	table.insert(mapping[newNote], i)
	lastMapping[i] = newNote
end


for i = lowestNote, highestNote do
	defineParameter("N"..i, nil, i, noteNames, function() mappingChanged(i) end)	
end

function onLoad()
	for i = lowestNote, highestNote do
		mappingChanged(i)
	end
end

function onNote(e)
	for i, note in ipairs(mapping[e.note]) do
		playNote(note, e.velocity)
	end	
end
1 Like

Yeeess! It’s working now! :partying_face:

Ah, I see. That’s not a problem as I’m gonna upgrade to Halion 7 pretty soon so I’ll be able to use that feature as well.

Again, thank you so much @misohoza ! You’ve been suuuper helpful and I really appreciate you took the time to help me here! :hugs: