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 json

def 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