Tables
Lua has exactly one way to keep several values together, and it is called a table. A list of note names is a table. A description of a device, with its name, port and channel, is a table. The thirty-two DX7 algorithms, each a row of thirty numbers, are a table of tables. The midiInput that the controller hands to every MIDI callback is a table too. Learn this one structure and you have learned all of Lua's data structures at once.
A table as a list
A table is written with curly braces. When you simply list values inside, Lua numbers them for you, starting at one:
local NOTE_NAMES = { "C", "C#", "D", "D#", "E", "F",
"F#", "G", "G#", "A", "A#", "B" }
print(NOTE_NAMES[1])
print(NOTE_NAMES[12])
print(#NOTE_NAMES)The log shows C, B and 12. The square brackets pick an entry by its position, and # gives the number of entries, as it did for a string.
Starting at one is the thing to remember. Most languages count from zero, and MIDI certainly does: note 60 is the first C of its octave when you divide by twelve and keep the remainder. That remainder runs from 0 to 11, and the table runs from 1 to 12, so a note name formatter has a + 1 in it:
local note = 61
print(NOTE_NAMES[(note % 12) + 1] .. (note // 12 - 2))This prints C#3. Nearly every off-by-one mistake in a Lua script is this + 1 forgotten or added twice, so when a table lookup gives you the neighbour of what you wanted, look here first.
A table as a record
Entries can have names instead of positions. Then the table describes one thing with several properties, the way a device has a name, a port and a channel:
local synth = {
name = "Yamaha DX7",
port = PORT_1,
channel = 1
}
print(synth.name)
print(synth["channel"])The dot and the square brackets with a string do the same thing. The dot is shorter and is what you will write nearly all the time; the brackets are there for when the name is itself in a variable.
This is also what the controller gives you when it calls your script. A MIDI callback receives a midiInput table with interface and port in it, and you read it exactly like synth above:
function midi.onNoteOn(midiInput, channel, note, velocity)
print("note " .. note .. " on port " .. midiInput.port)
endSend this and play a note into the controller, and the log tells you where it came from. This callback has to be defined at the top level of the script, which is where you have been putting everything so far.
Any key you like
A list is numbered from one and a record has names, but a table does not insist on either. You can use any number as a key, which is handy when the numbers mean something, such as which group of controls belongs to which value of a selector:
local controlGroups = {
[0] = { 20, 21, 22 },
[1] = { 26, 27, 28 },
[2] = { 32, 33 }
}
print(#controlGroups[1])
print(controlGroups[2][1])The first line prints 3, the number of controls in group 1. The second prints 32: the second group, its first control. Two pairs of brackets, one to reach the inner table and one to reach into it. This is the shape of the DX7 algorithm table in the Drawing a Custom Control tutorial, where each of the thirty-two rows is itself a table of numbers.
Changing a table
A table is not fixed when it is created. Assign to a key that does not exist and it appears; assign nil and it disappears:
local held = {}
held[60] = true
held[64] = true
print(held[60])
held[60] = nil
print(held[60])The first print says true and the second nil. This is a tiny model of how a script keeps track of which notes are currently held, which is the heart of an arpeggiator: a Note On adds the key, a Note Off removes it, and a loop with pairs walks over whatever is left.
Adding to the end of a list has an idiom of its own, which you have met in the Editing a Patch Name tutorial:
local characters = {}
for i = 1, 3 do
characters[#characters + 1] = string.char(64 + i)
end
print(table.concat(characters))#characters + 1 is always the next free position, so the line appends. table.concat glues the entries of a list into one string, giving ABC here. That pair, append in a loop and concatenate at the end, is how a name is read out of a patch dump.
Tables are shared, not copied
One thing about tables that catches everyone once. When you assign a table to a second variable, you do not get a second table. Both names refer to the same one:
local a = { 1, 2, 3 }
local b = a
b[1] = 99
print(a[1])The log says 99, even though we changed b. Think of a table as a thing and of variables as labels stuck to it; two labels, one thing. When you really need a copy, you make it with a loop, which is a good exercise now that you know both.
What comes next
We have been writing function since the chapter on conditions and reading past it. It is time to give functions the attention they deserve, because the controller runs your script almost entirely by calling them.