Midi2lua
This function reads a MIDI file and extracts the tempo and a list of note events.
-- A minimal MIDI to Lua Table parser
-- Supports: Type 0 (single track) or first track of Type 1
-- Returns: A table containing 'tempo' and 'notes' (pitch, start_time, duration)
local function parseMIDI(filepath)
local file = io.open(filepath, "rb")
if not file then error("Could not open MIDI file: " .. filepath) end
-- Helper: Read Variable Length Quantity (VLQ)
local function readVLQ()
local value = 0
local b
repeat
b = file:read(1):byte()
value = (value << 7) | (b & 0x7F)
until (b & 0x80) == 0
return value
end
-- Helper: Read 16/32-bit Big Endian
local function read16() local b1, b2 = file:read(2):byte(1,2); return (b1 << 8) | b2 end
local function read32() local b1, b2, b3, b4 = file:read(4):byte(1,2,3,4); return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4 end
-- 1. Parse Header
local header = file:read(4)
if header ~= "MThd" then error("Not a valid MIDI file") end
read32() -- Header length (always 6)
local format = read16()
local nTracks = read16()
local division = read16()
local ticksPerBeat = division & 0x7FFF
-- 2. Parse First Track (Simplified for demonstration)
-- Skip to first 'MTrk'
while file:read(4) ~= "MTrk" do end
local trackLength = read32()
local trackEnd = file:seek() + trackLength
local notes = {}
local activeNotes = {} -- Stores start_tick for currently playing notes
local currentTick = 0
local tempo = 500000 -- Default: 120 BPM (microseconds per beat)
-- Temporarily store events to calculate duration later
local eventList = {}
while file:seek() < trackEnd do
local delta = readVLQ()
currentTick = currentTick + delta
local status = file:read(1):byte()
-- Handle running status
if status < 0x80 then
-- If data byte, we are continuing previous status
-- (Implementation skipped for brevity in this snippet)
-- In a full parser, you would handle the running status byte logic here.
end
local eventType = status >> 4
-- Note On (0x9) or Note Off (0x8)
if eventType == 0x9 or eventType == 0x8 then
local data1 = file:read(1):byte()
local data2 = file:read(1):byte()
local pitch = data1
local velocity = data2
-- Note On with velocity 0 acts as Note Off
if eventType == 0x9 and velocity > 0 then
table.insert(eventList,
type = "on",
tick = currentTick,
pitch = pitch,
velocity = velocity
)
else
table.insert(eventList,
type = "off",
tick = currentTick,
pitch = pitch
)
end
-- Meta Event (0xFF)
elseif status == 0xFF then
local typeByte = file:read(1):byte()
local len = readVLQ()
local data = file:read(len)
-- Tempo Change
if typeByte == 0x51 then
tempo = (data:byte(1) << 16) | (data:byte(2) << 8) | data:byte(3)
end
-- Other events (Control Change, Program Change, etc)
else
-- Skip data bytes for unsupported events
if eventType == 0xC or eventType == 0xD then
file:read(1) -- 1 byte
else
file:read(2) -- 2 bytes
end
end
end
file:close()
-- 3. Process eventList to calculate duration and time
-- This pairs Note Ons with corresponding Note Offs
for _, event in ipairs(eventList) do
if event.type == "on" then
activeNotes[event.pitch] = event.tick
elseif event.type == "off" then
local startTick = activeNotes[event.pitch]
if startTick then
-- Convert Ticks to Seconds: (Ticks / PPQ) * (Tempo / 1,000,000)
local startTime = (startTick / ticksPerBeat) * (tempo / 1000000)
local endTime = (event.tick / ticksPerBeat) * (tempo / 1000000)
table.insert(notes,
pitch = event.pitch,
time = startTime,
duration = endTime - startTime
)
activeNotes[event.pitch] = nil
end
end
end
return
ticksPerBeat = ticksPerBeat,
tempo = tempo,
notes = notes
end
-- Usage Example:
-- local midiData = parseMIDI("song.mid")
-- for _, note in ipairs(midiData.notes) do
-- print(string.format("Note: %d, Start: %.2fs, Duration: %.2fs", note.pitch, note.time, note.duration))
-- end
midi2lua is a utility (or script) that parses a standard MIDI file (.mid) and outputs a Lua table representation of its musical data. This allows developers to embed procedural music playback, note-accurate event triggering, or rhythm-based game logic directly into Lua environments (e.g., LÖVE2D, Roblox, Defold, PICO-8, or custom embedded systems).
The output is self-contained Lua code – no external MIDI parser or real-time MIDI playback required at runtime. midi2lua
If existing tools don't fit your Lua dialect (e.g., you need vs Lua tables, or you need GMod specific syntax), writing your own midi2lua is surprisingly simple using the mido library.
import mido import jsondef midi_to_lua(midi_path, output_path): mid = mido.MidiFile(midi_path) lua_table = "return \n tracks = \n" This function reads a MIDI file and extracts
for track in mid.tracks: lua_table += " \n events = \n" abs_time = 0 for msg in track: abs_time += msg.time if msg.type == 'note_on' and msg.velocity > 0: lua_table += f" time = abs_time, note = msg.note, vel = msg.velocity ,\n" elif msg.type == 'note_off' or (msg.type == 'note_on' and msg.velocity == 0): lua_table += f" time = abs_time, note_off = msg.note ,\n" lua_table += " \n ,\n" lua_table += " \n" with open(output_path, 'w') as f: f.write(lua_table)
midi_to_lua("input.mid", "output.lua")
The basic note list is useful, but the best midi2lua converters offer sophisticated features.
$ midi2lua song.mid --format milliseconds --quantize 16 > song.lua
$ lua game_main.lua # uses song.lua
Artists using LÖVE2D to build interactive projections often use MIDI keyboards to trigger visuals. By pre-converting a MIDI score via midi2lua, the installation can run autonomously without needing a live MIDI input device, synchronizing visual effects to the exact timestamp of a musical score. midi2lua is a utility (or script) that parses