midmid3 / midmid /midi_writer.py
markury's picture
Initial commit
d171350
"""MIDI serialization: write ChartData to a GH-format .mid file."""
import mido
from midmid.datatypes import ChartData, NoteEvent
DIFFICULTY_OFFSETS = {"easy": 60, "medium": 72, "hard": 84, "expert": 96}
HOPO_NOTE = {"easy": 65, "medium": 77, "hard": 89, "expert": 101}
NOTE_VELOCITY = 100
def write_midi(chart: ChartData, output_path: str) -> None:
mid = mido.MidiFile(ticks_per_beat=chart.resolution)
mid.tracks.append(_build_tempo_track(chart))
mid.tracks.append(_build_events_track(chart))
mid.tracks.append(_build_guitar_track(chart))
if chart.beats:
mid.tracks.append(_build_beat_track(chart))
mid.save(output_path)
def _build_tempo_track(chart):
track = mido.MidiTrack()
events = []
for tick, bpm in chart.tempo_events:
events.append((tick, mido.MetaMessage(
"set_tempo", tempo=mido.bpm2tempo(bpm), time=0)))
for tick, num, den in chart.time_signatures:
events.append((tick, mido.MetaMessage(
"time_signature", numerator=num, denominator=den, time=0)))
_write_sorted_events(track, events)
return track
def _build_events_track(chart):
track = mido.MidiTrack()
track.append(mido.MetaMessage("track_name", name="EVENTS", time=0))
events = []
for tick, label in chart.sections:
events.append((tick, mido.MetaMessage(
"text", text=f"[section {label}]", time=0)))
_write_sorted_events(track, events)
return track
def _build_guitar_track(chart):
track = mido.MidiTrack()
track.append(mido.MetaMessage("track_name", name="PART GUITAR", time=0))
events = []
for difficulty, offset in DIFFICULTY_OFFSETS.items():
if difficulty not in chart.notes:
continue
for note in chart.notes[difficulty]:
for fret in note.fret_set:
midi_note = offset + fret
events.append((note.tick, mido.Message(
"note_on", note=midi_note, velocity=NOTE_VELOCITY, time=0)))
off_tick = note.tick + max(note.sustain_ticks, 1)
events.append((off_tick, mido.Message(
"note_off", note=midi_note, velocity=0, time=0)))
if note.is_hopo:
hopo_note = HOPO_NOTE[difficulty]
events.append((note.tick, mido.Message(
"note_on", note=hopo_note, velocity=NOTE_VELOCITY, time=0)))
events.append((note.tick + 1, mido.Message(
"note_off", note=hopo_note, velocity=0, time=0)))
_write_sorted_events(track, events)
return track
def _build_beat_track(chart):
track = mido.MidiTrack()
track.append(mido.MetaMessage("track_name", name="BEAT", time=0))
events = []
for tick, is_downbeat in chart.beats:
midi_note = 12 if is_downbeat else 13
events.append((tick, mido.Message(
"note_on", note=midi_note, velocity=NOTE_VELOCITY, time=0)))
events.append((tick + 1, mido.Message(
"note_off", note=midi_note, velocity=0, time=0)))
_write_sorted_events(track, events)
return track
def _write_sorted_events(track, events):
events.sort(key=lambda e: e[0])
prev_tick = 0
for abs_tick, msg in events:
msg.time = abs_tick - prev_tick
track.append(msg)
prev_tick = abs_tick