Okay, it’s now fully functional and ready for prime time. It’s stackable so all 64 pads can be used simultaneously as well (instructions on this are now baked into the plugin description).
Here’s the latest screenshot:
And here’s the latest code that goes with it:
ardour {
["type"] = "dsp",
name = "Launchpad (4x4) Drum Map",
category = "Utility",
license = "MIT",
author = "Ardour Community",
description = [[ A Novation Launchpad X/Pro Mk3 to Standard GDM MIDI Drum Mapper. This plugin expects to operate on top of any 4x4 drum pad layout of similar implementation. Each plugin instance represents a 4x4 drum map with 1x1 as the top left of the grid and 4x4 as the bottom right (the numbers in parenthesis are the respective MIDI note values). Generally, these grids start at some MIDI note value like (C3, 48) and increment up 15 more notes from left-to-right, bottom-to-top...making the bottom-left pad the proper MIDI note value to use for the "Starting Note" parameter.
For the Launchpad rhythm pads in note/sequencer mode, the default starting note is C2 (36) and as such, this is the plugin default.
However, this plugin can be stacked multiple times (for example, with the Launchpad put into 'Factory Custom Mode 3')... --BUT-- take care as one instance can remap to a note in range that may unintentionally be remapped again further down the chain! Generally, stacking them in order from lowest to highest starting note should allow this to work okay. For this custom mode specifically, this means: first bottom-left (with starting note 48), then top-left (64), then bottom-right (80) and finally top-right (96).]]
}
-- Global Constants
MIDI_MAX = 127
MIDI_INVALID = -1
OFF_NOTE = MIDI_INVALID
PAD_COUNT = 16 -- 4x4 Grid
NON_PAD_PARAM_COUNT = 2 -- Should be equal to the number of named parameters defined immediately below
-- Named Parameter Indexes
PASSTHROUGH = 1
STARTING_NOTE = 2
function dsp_ioconfig ()
return { { midi_in = 1, midi_out = 1, audio_in = 0, audio_out = 0}, }
end
function dsp_params ()
-- Constants
local DEFAULT_STARTING_NOTE = 36 -- C2
-- Default Launchpad Grid/Note Map
--
-- Note range (default) is C3 (bottom left) to D#4 (top right), left half first, bottom
-- to top, then right side bottom to top.
--
-- The '#x#' here represents physical grid locations: 1x1 is the top left,
-- 1x4 is the top right, 4x4 is the bottom right, etc.
--
-- With that in mind, this is the Launchpad order, from lowest note to highest.
local LAUNCHPAD_MAP = {
[1] = "4x1",
[2] = "4x2",
[3] = "4x3",
[4] = "4x4",
[5] = "3x1",
[6] = "3x2",
[7] = "3x3",
[8] = "3x4",
[9] = "2x1",
[10] = "2x2",
[11] = "2x3",
[12] = "2x4",
[13] = "1x1",
[14] = "1x2",
[15] = "1x3",
[16] = "1x4"
}
-- Default GDM1 MIDI Drum Map
-- Constants
local GDM_DRUM_START = 35
local GDM_DRUM_END = 81
local GDM_DRUM_MAP = {
[35] = "Acoustic Bass Drum",
[36] = "Bass Drum 1",
[37] = "Side Stick",
[38] = "Acoustic Snare",
[39] = "Hand Clap",
[40] = "Electric Snare",
[41] = "Low Floor Tom",
[42] = "Closed Hi-Hat",
[43] = "High Floor Tom",
[44] = "Pedal Hi-Hat",
[45] = "Low Tom",
[46] = "Open Hi-Hat",
[47] = "Low-Mid Tom",
[48] = "High-Mid Tom",
[49] = "Crash Cymbal 1",
[50] = "High Tom",
[51] = "Ride Cymbal 1",
[52] = "Chinese Cymbal",
[53] = "Ride Bell",
[54] = "Tambourine",
[55] = "Splash Cymbal",
[56] = "Cowbell",
[57] = "Crash Cymbal 2",
[58] = "Vibraslap",
[59] = "Ride Cymbal 2",
[60] = "High Bongo",
[61] = "Low Bongo",
[62] = "Mute High Conga",
[63] = "Open High Conga",
[64] = "Low Conga",
[65] = "High Timbale",
[66] = "Low Timbale",
[67] = "High Agogo",
[68] = "Low Agogo",
[69] = "Cabasa",
[70] = "Maracas",
[71] = "Short Whistle",
[72] = "Long Whistle",
[73] = "Short Guiro",
[74] = "Long Guiro",
[75] = "Claves",
[76] = "High Wood Block",
[77] = "Low Wood Block",
[78] = "Mute Cuica",
[79] = "Open Cuica",
[80] = "Mute Triangle",
[81] = "Open Triangle"
}
-- Default "Finger Drumming" Remap
--
-- Launchpad 'Default Custom 3' Notes -> Standard GDM Drums
--
-- A big thanks to Robert Mathijs and 'The Quest For Groove' finger-drumming
-- guidelines for providing a robust starting point and further inspiration
-- to create this plugin.
local DEFAULT_REMAP = {
[13] = 41, --1x1, Low Floor Tom
[14] = 47, --1x2, Low-Mid Tom
[15] = 48, --1x3, High-Mid Tom
[16] = 49, --1x4, Crash Cymbal 1
[9] = 42, --2x1, Closed Hi-Hat
[10] = 46, --2x2, Open Hi-Hat
[11] = 42, --2x3, Closed Hi-Hat
[12] = 51, --2x4, Ride Cymbal 1
[5] = 37, --3x1, Side Stick
[6] = 38, --3x2, Acoustic Snare
[7] = 38, --3x3, Acoustic Snare
[8] = 37, --3x4, Side Stick
[1] = 57, --4x1, Crash Cymbal 2
[2] = 35, --4x2, Acoustic Bass Drum
[3] = 35, --4x3, Acoustic Bass Drum
[4] = 53, --4x4, Ride Bell
}
local SCALEPOINTS_MAP = {}
for note=0,(MIDI_MAX - PAD_COUNT) do
local name = ARDOUR.ParameterDescriptor.midi_note_name(note)
SCALEPOINTS_MAP[string.format("(%03d) %s", note, name)] = note
end
local DRUM_SCALEPOINTS_MAP = {}
DRUM_SCALEPOINTS_MAP["None"] = OFF_NOTE
for note=GDM_DRUM_START,GDM_DRUM_END do
local name = string.format("(%03d) %s", note, GDM_DRUM_MAP[note])
DRUM_SCALEPOINTS_MAP[name] = note
end
-- Pad parameters to be sorted first, then added to map_params
local pad_params = {}
local i = 1
for pad, name in pairs(LAUNCHPAD_MAP) do
pad_params[i] = {
["type"] = "input",
name = string.format("Pad %s", name),
min = MIDI_INVALID,
max = MIDI_MAX,
default = DEFAULT_REMAP[pad],
integer = true,
enum = true,
scalepoints = DRUM_SCALEPOINTS_MAP,
doc = "Set this pad's drum output."
}
i = i + 1
end
table.sort(pad_params, function (a, b)
return string.upper(a.name) < string.upper(b.name)
end)
-- The actual, returned parameter data
local map_params = {}
-- Named Parameter Definitions
map_params[PASSTHROUGH] = {
["type"] = "input",
name = "'None' pass-through",
min = 0,
max = 1,
default = 0,
toggled = true,
doc = "Allow notes mapped to 'None' to pass through instead of being supressed."
}
map_params[STARTING_NOTE] = {
["type"] = "input",
name = "Starting Note",
min = MIDI_INVALID,
max = MIDI_MAX - PAD_COUNT,
default = DEFAULT_STARTING_NOTE,
integer = true,
enum = true,
scalepoints = SCALEPOINTS_MAP,
doc = "Sets the starting note of a 4x4 drum pad. This should be set to the lowest note, usually triggered by the bottom-left pad."
}
-- Pad Parameter Definitions
i = NON_PAD_PARAM_COUNT + 1
for _, cur_pad in pairs(pad_params) do
map_params[i] = cur_pad
i = i + 1
end
return map_params
end
function dsp_run (_, _, n_samples)
assert (type(midiin) == "table")
assert (type(midiout) == "table")
local cnt = 1;
function tx_midi (time, data)
midiout[cnt] = {}
midiout[cnt]["time"] = time;
midiout[cnt]["data"] = data;
cnt = cnt + 1;
end
-- build translation table
local translation_table = {}
local ctrl = CtrlPorts:array()
local passthrough = ctrl[PASSTHROUGH]
local starting_note = ctrl[STARTING_NOTE]
-- Due to the nature of the pad layout and the decidedly finite contents it's just
-- a little more straightforward and cleaner to do this manually.
translation_table[starting_note + 0] = ctrl[NON_PAD_PARAM_COUNT + 13] -- 4x1
translation_table[starting_note + 1] = ctrl[NON_PAD_PARAM_COUNT + 14] -- 4x2
translation_table[starting_note + 2] = ctrl[NON_PAD_PARAM_COUNT + 15] -- 4x3
translation_table[starting_note + 3] = ctrl[NON_PAD_PARAM_COUNT + 16] -- 4x4
translation_table[starting_note + 4] = ctrl[NON_PAD_PARAM_COUNT + 9] -- 3x1
translation_table[starting_note + 5] = ctrl[NON_PAD_PARAM_COUNT + 10] -- 3x2
translation_table[starting_note + 6] = ctrl[NON_PAD_PARAM_COUNT + 11] -- 3x3
translation_table[starting_note + 7] = ctrl[NON_PAD_PARAM_COUNT + 12] -- 3x4
translation_table[starting_note + 8] = ctrl[NON_PAD_PARAM_COUNT + 5] -- 2x1
translation_table[starting_note + 9] = ctrl[NON_PAD_PARAM_COUNT + 6] -- 2x1
translation_table[starting_note + 10] = ctrl[NON_PAD_PARAM_COUNT + 7] -- 2x3
translation_table[starting_note + 11] = ctrl[NON_PAD_PARAM_COUNT + 8] -- 2x4
translation_table[starting_note + 12] = ctrl[NON_PAD_PARAM_COUNT + 1] -- 1x1
translation_table[starting_note + 13] = ctrl[NON_PAD_PARAM_COUNT + 2] -- 1x2
translation_table[starting_note + 14] = ctrl[NON_PAD_PARAM_COUNT + 3] -- 1x3
translation_table[starting_note + 15] = ctrl[NON_PAD_PARAM_COUNT + 4] -- 1x4
-- for each incoming midi event
for _,b in pairs (midiin) do
local t = b["time"] -- t = [ 1 .. n_samples ]
local d = b["data"] -- midi-event data
local event_type
if #d == 0 then event_type = -1 else event_type = d[1] >> 4 end
if (#d == 3) and (event_type == 9 or event_type == 8 or event_type == 10) then
-- Do the mapping - 2 is note byte for these types
if passthrough > 0 then
-- 'None' lets the notes pass though
local new_note = translation_table[d[2]]
if new_note ~= nil and new_note ~= OFF_NOTE then
d[2] = new_note
end
tx_midi (t, d)
else
-- 'None' effectively turns this note off
d[2] = translation_table[d[2]] or OFF_NOTE
if not (d[2] == OFF_NOTE) then
tx_midi (t, d)
end
end
else
tx_midi (t, d)
end
end
end
Let me know if you run into any issues or generate any feature requests. To get my full 64 pads going I’m simply stacking these as described in the instructions and then saving the individual presets per quadrant to get a fully mappable drum pad.
And to the dev team: Is something like this worthy of a pull request? I can generate one, or feel free to just copy and include if that’s easier for something this small. Just let me know!
Edit: After playing with it a little bit, it may be worth changing the behavior back to muting “None” selections rather than allowing pass-through. The recent changes have made the pass-through decision considerably less important.
Edit 2: The code has been updated to turn the pass-through behavior into a proper setting. It’s now a toggle. Some tool tip help has also been added for a little easier time understanding, even without reading the description. The screenshot has also been updated to reflect the current changes too.
Edit 3: And in what I hope will be the final edit of this cycle, after testing with the sequencer it starts at C2 instead of C3 like the custom mode…so this is the new default note. Instructions remain for the custom 3 layout, but this should now provide a decent drum layout out of the box for most any place you happen to be drumming in the Launchpad modes. Some of the internal documentation has been updated to reflect these changes too, along with a few errors being corrected along the way.
Edit 4: Bringing this back into a real project and attaching it to DrumGizmo works perfectly as well. I believe I am personally satisfied now, but as always, if there are any comments/suggestions/etc. I’m more than happy to flesh it out further.