Save this as midi2lua_patched.py:
#!/usr/bin/env python3 """ midi2lua_patched.py Converts MIDI file to Lua table for LÖVE2D/FNF. Patched features: tempo mapping, channel filtering, note grouping. """import sys import struct import math
def read_var_length(f): value = 0 while True: byte = f.read(1) if not byte: break byte = byte[0] value = (value << 7) | (byte & 0x7F) if not (byte & 0x80): break return value
def parse_midi(filename, track_idx=0, channel_include=None): with open(filename, 'rb') as f: if f.read(4) != b'MThd': raise ValueError("Not a MIDI file") header_len = struct.unpack('>I', f.read(4))[0] format_type = struct.unpack('>H', f.read(2))[0] num_tracks = struct.unpack('>H', f.read(2))[0] division = struct.unpack('>H', f.read(2))[0] ticks_per_beat = division & 0x7FFF
tracks = [] for _ in range(num_tracks): if f.read(4) != b'MTrk': raise ValueError("Bad track chunk") track_len = struct.unpack('>I', f.read(4))[0] track_data = f.read(track_len) tracks.append(track_data) # Parse selected track data = tracks[track_idx] pos = 0 tick = 0 events = [] tempo = 500000 # default microseconds per quarter bpm = 120 time_sig_num = 4 time_sig_denom = 4 while pos < len(data): delta = read_var_length(bytearray([data[pos]])) if isinstance(data, bytes) else read_var_length(bytearray([data[pos]])) # Actually parse delta correctly delta_bytes = 0 delta_val = 0 while True: b = data[pos] delta_val = (delta_val << 7) | (b & 0x7F) pos += 1 if not (b & 0x80): break tick += delta_val if pos >= len(data): break cmd = data[pos] pos += 1 if cmd == 0xFF: # meta event meta_type = data[pos] pos += 1 length = read_var_length(bytearray([data[pos]])) if isinstance(data, bytes) else read_var_length(bytearray([data[pos]])) # Actually read length len_val = 0 while True: b = data[pos] len_val = (len_val << 7) | (b & 0x7F) pos += 1 if not (b & 0x80): break meta_data = data[pos:pos+len_val] pos += len_val if meta_type == 0x51: # set tempo tempo = int.from_bytes(meta_data, byteorder='big') bpm = 60000000 / tempo elif meta_type == 0x58: # time signature time_sig_num = meta_data[0] time_sig_denom = 2 ** meta_data[1] continue elif cmd & 0xF0 == 0x90: # note on channel = cmd & 0x0F if channel_include is not None and channel not in channel_include: continue note = data[pos] velocity = data[pos+1] pos += 2 if velocity > 0: events.append(('note_on', tick, channel, note, velocity, tempo, ticks_per_beat)) elif cmd & 0xF0 == 0x80: # note off channel = cmd & 0x0F if channel_include is not None and channel not in channel_include: continue note = data[pos] pos += 2 events.append(('note_off', tick, channel, note)) else: # skip other events if cmd & 0xF0 in [0xC0, 0xD0]: pos += 1 else: pos += 2 return events, ticks_per_beat, bpm, time_sig_num, time_sig_denomdef tick_to_seconds(tick, tempo_us, ticks_per_beat): return (tick * tempo_us) / (ticks_per_beat * 1_000_000)
def generate_lua(events, ticks_per_beat, bpm, filename_out, sample_rate=44100, ppq=480): notes = [] active = {} midi2lua patched
for ev in events: if ev[0] == 'note_on': _, tick, ch, note, vel, tempo, tpb = ev start_sec = tick_to_seconds(tick, tempo, tpb) active[(ch, note)] = start_sec elif ev[0] == 'note_off': _, tick, ch, note = ev if (ch, note) in active: start_sec = active.pop((ch, note)) # find end tempo (simplified: use last tempo) end_sec = tick_to_seconds(tick, tempo, tpb) if 'tempo' in locals() else start_sec + 0.5 duration = end_sec - start_sec if duration > 0: notes.append( 'pitch': note, 'start': start_sec, 'duration': duration, 'channel': ch, 'velocity': 100 ) # Generate Lua lua_code = f"""-- Auto-generated by midi2lua_patched-- BPM: bpm:.2f PPQ: ticks_per_beat
local notes = { """ for n in notes: lua_code += f" pitch = n['pitch'], start = n['start']:.6f, duration = n['duration']:.6f, channel = n['channel'] ,\n" lua_code += """
function play_sequence(source) for _, note in ipairs(notes) do local timer = love.timer.getTime() local delay = note.start - timer if delay < 0 then delay = 0 end love.timer.after(delay, function() local frequency = 440 * 2 ^ ((note.pitch - 69) / 12) local sound = love.audio.newSource(love.sound.newSoundData(1, 44100)) -- actual synth logic here end) end end
return notes """ with open(filename_out, 'w') as f: f.write(lua_code) print(f"✅ Generated filename_out with len(notes) notes")
if name == 'main': if len(sys.argv) < 3: print("Usage: midi2lua_patched.py input.mid output.lua [channel1,channel2]") sys.exit(1) midi_file = sys.argv[1] lua_file = sys.argv[2] channels = None if len(sys.argv) > 3: channels = [int(c) for c in sys.argv[3].split(',')] events, tpb, bpm, _, _ = parse_midi(midi_file, track_idx=0, channel_include=channels) generate_lua(events, tpb, bpm, lua_file)Save this as midi2lua_patched
A patched version implies community‑ or developer‑driven modifications that fix limitations, add features, or adapt the tool to newer environments. Common patches include:
MIDI2Lua Patched does not contain any Nintendo copyrighted code. It is a transformative tool that converts open-standard MIDI files into Lua tables. However, using it to distribute full game ROMs with copyrighted soundtracks (e.g., replacing Mario music with a pop song) may violate fair use. Most modders use it for original compositions or public domain MIDIs.
The Problem: Standard MIDI-to-Lua converters often output a repetitive, line-by-line stream of function calls. This results in huge file sizes and poor runtime performance because the Lua interpreter has to process thousands of individual function calls for simple chord strikes or parameter changes.
The Solution: A patch that detects simultaneous events (like a chord) or rapid parameter changes and "batches" them into single table-driven function calls, optionally quantizing the timing to clean up sloppy performances. -- BPM: bpm:
MIDI clips from a DAW are transformed into Lua sequences that send DMX values over UDP – perfect for open‑source lighting consoles.
First, let’s break down the name. MIDI (Musical Instrument Digital Interface) is the universal format for sequenced music. Lua is a lightweight scripting language used extensively in Nintendo’s proprietary engines (like the LunchPack engine for 3DS/Wii U) and in homebrew frameworks such as LÖVE.
The original midi2lua tool was a command-line utility that parsed a MIDI file and outputted a .lua file containing large arrays of note events, durations, and velocities. The game engine would then iterate through these tables to play custom music.
The problem? The original version was alpha-quality at best.
Frustrated modders began sharing unofficial edits. Several forks appeared, but one rose above the rest: the MIDI2Lua Patched build (often version 1.2.3p or higher), maintained by a collective of German and Japanese ROM hackers.
Open your MIDI in a DAW like Reaper or FL Studio. Ensure: