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 end midi2lua

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:

  1. Parse the binary MIDI header and tracks.
  2. Convert the delta time stamps (ticks) into absolute timestamps (milliseconds or seconds).
  3. 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)

  1. Export your track as a Standard MIDI File (.mid) from any DAW.
  2. Run the converter:
    midi2lua my_song.mid -o music.lua
    
  3. Require the file in your Lua project:
    local music = require("music")
    
  4. Write a scheduler that iterates through music.tracks[1].notes using os.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 table

import 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.