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