Midi2lua ((link)) ✰
From MIDI to Code: Unleashing Real-Time Music with midi2lua
If you’ve ever wanted to bring dynamic, interactive music into a Lua-based environment—think Roblox, LÖVE2D, or Defold—you’ve likely run into a frustrating wall. Sure, you can play a pre-rendered MP3, but how do you let the player control the drum beat? How do you sync a boss fight’s health bar to the intensity of a synth solo?
Enter the unsung hero of interactive audio: midi2lua.
This isn’t just a converter. It’s a bridge between standard music production (MIDI) and live, programmable game logic.
5. Runtime Usage in Lua
Load and play the generated script:
local song = require("mysong")local tick = 0 local current_track = 1 local event_idx = 1 local events = song.tracks[current_track].events
function love.update(dt) tick = tick + dt * (song.ticks_per_beat / 60) -- assumes 60 BPM default while event_idx <= #events and events[event_idx].tick <= tick do local e = events[event_idx] if e.type == "note" then play_sound(e.pitch, e.velocity) elseif e.type == "tempo" then update_playback_speed(e.bpm) end event_idx = event_idx + 1 end endmidi2lua
1. Overview
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.
The Concept
A MIDI file is essentially a timeline of events. To use it in Lua, you need to:
- Parse the binary MIDI header and tracks.
- Convert the delta time stamps (ticks) into absolute timestamps (milliseconds or seconds).
- Store the events in a Lua table structure.
Advanced Features and Optimizations
The basic note list is useful, but the best midi2lua converters offer sophisticated features. From MIDI to Code: Unleashing Real-Time Music with
The Magic: Real-Time Interaction
Here’s where it gets fun. Because your music is just data, you can manipulate it on the fly.
Example 1: The "Health" Synth
-- Play a synth lead, but filter the notes based on player HP
function updateMusic(playerHealth)
for _, note in ipairs(song.tracks[2].notes) do
if playerHealth > 50 or note.pitch < 60 then
playNote(note)
end
end
end
Example 2: Rhythm-Based Powerups
-- Check if the player presses "A" within 50ms of a Kick drum
function onPlayerAction(inputTime)
for _, note in ipairs(song.tracks[1].notes) do
if note.pitch == 36 and math.abs(inputTime - note.time) < 0.05 then
grantBonus("Rhythm Strike!")
end
end
end
Getting Started (5 Minutes)
- Export your track as a Standard MIDI File (.mid) from any DAW.
- Run the converter:
midi2lua my_song.mid -o music.lua - Require the file in your Lua project:
local music = require("music") - Write a scheduler that iterates through
music.tracks[1].notesusingos.clock()or your game’s update loop.
4. Implementation Walkthrough (Python Example)
A minimal but complete midi2lua converter can be written in Python using mido. Below is a reference implementation.
#!/usr/bin/env python3 # midi2lua.py - Convert MIDI file to Lua note tableimport mido from mido import MidiFile, tick2second import sys Parse the binary MIDI header and tracks
def midi_to_lua(midi_path, lua_path): mid = MidiFile(midi_path) tempo = 500000 # default microseconds per quarter (120 BPM) ticks_per_beat = mid.ticks_per_beat tracks_data = []
for track in mid.tracks: track_notes = [] absolute_ticks = 0 open_notes = {} # (note, channel) -> (start_tick, velocity) for msg in track: absolute_ticks += msg.time if msg.type == 'set_tempo': tempo = msg.tempo elif msg.type == 'note_on' and msg.velocity > 0: open_notes[(msg.note, msg.channel)] = (absolute_ticks, msg.velocity) elif (msg.type == 'note_off') or (msg.type == 'note_on' and msg.velocity == 0): key = (msg.note, msg.channel) if key in open_notes: start_tick, vel = open_notes.pop(key) duration = absolute_ticks - start_tick if duration > 0: track_notes.append( 'start': start_tick, 'duration': duration, 'pitch': msg.note, 'velocity': vel ) # Close any dangling notes (end of track) for (pitch, ch), (start_tick, vel) in open_notes.items(): duration = absolute_ticks - start_tick if duration > 0: track_notes.append( 'start': start_tick, 'duration': duration, 'pitch': pitch, 'velocity': vel ) if track_notes: tracks_data.append(track_notes) # Write Lua file with open(lua_path, 'w') as f: f.write("-- Generated by midi2lua\n") f.write("return \n") f.write(f" tempo = int(60_000_000 / tempo),\n") # BPM f.write(f" resolution = ticks_per_beat,\n") f.write(" tracks = \n") for track_notes in tracks_data: f.write(" \n") f.write(" notes = \n") for n in track_notes: f.write(f" start = n['start'], duration = n['duration'], pitch = n['pitch'], velocity = n['velocity'] ,\n") f.write(" ,\n") f.write(" ,\n") f.write(" \n") f.write("\n")
if name == "main": if len(sys.argv) != 3: print("Usage: midi2lua.py input.mid output.lua") sys.exit(1) midi_to_lua(sys.argv[1], sys.argv[2])
Run:
pip install mido
python midi2lua.py song.mid song.lua
8.3 Time Signature Awareness
Incorporate metrical structure for loop points or measure‑based callbacks.

