| """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 |
|
|