Skip to main content

oxihuman_export/
midi_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! MIDI file export (Type 0, single track, note events).
6
7/// A MIDI note event.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct MidiNote {
11    pub tick_on: u32,
12    pub tick_off: u32,
13    pub channel: u8,
14    pub pitch: u8,
15    pub velocity: u8,
16}
17
18/// A MIDI export container.
19#[allow(dead_code)]
20pub struct MidiExport {
21    pub tempo_bpm: u32,
22    pub ticks_per_beat: u16,
23    pub notes: Vec<MidiNote>,
24}
25
26impl MidiExport {
27    #[allow(dead_code)]
28    pub fn new(tempo_bpm: u32, ticks_per_beat: u16) -> Self {
29        Self {
30            tempo_bpm,
31            ticks_per_beat,
32            notes: Vec::new(),
33        }
34    }
35}
36
37/// Add a note to the MIDI export.
38#[allow(dead_code)]
39pub fn add_note(midi: &mut MidiExport, note: MidiNote) {
40    midi.notes.push(note);
41}
42
43/// Encode a variable-length quantity (VLQ).
44#[allow(dead_code)]
45pub fn encode_vlq(value: u32) -> Vec<u8> {
46    let mut out = Vec::new();
47    let mut v = value;
48    let first = true;
49    let mut bytes = Vec::new();
50    loop {
51        bytes.push((v & 0x7F) as u8);
52        v >>= 7;
53        if v == 0 {
54            break;
55        }
56    }
57    bytes.reverse();
58    for (i, &b) in bytes.iter().enumerate() {
59        if i < bytes.len() - 1 {
60            out.push(b | 0x80);
61        } else {
62            out.push(b);
63        }
64    }
65    if out.is_empty() && first {
66        out.push(0);
67    }
68    let _ = first;
69    out
70}
71
72/// Build MIDI file header chunk.
73#[allow(dead_code)]
74pub fn build_midi_header(midi: &MidiExport) -> Vec<u8> {
75    let mut hdr = Vec::new();
76    hdr.extend_from_slice(b"MThd");
77    hdr.extend_from_slice(&6u32.to_be_bytes());
78    hdr.extend_from_slice(&0u16.to_be_bytes());
79    hdr.extend_from_slice(&1u16.to_be_bytes());
80    hdr.extend_from_slice(&midi.ticks_per_beat.to_be_bytes());
81    hdr
82}
83
84/// Build MIDI track chunk from notes.
85#[allow(dead_code)]
86pub fn build_midi_track(midi: &MidiExport) -> Vec<u8> {
87    let mut events: Vec<(u32, Vec<u8>)> = Vec::new();
88    let us_per_beat = 60_000_000 / midi.tempo_bpm.max(1);
89    let tempo_event: Vec<u8> = {
90        let mut e = Vec::new();
91        e.extend_from_slice(&encode_vlq(0));
92        e.push(0xFF);
93        e.push(0x51);
94        e.push(0x03);
95        e.extend_from_slice(&us_per_beat.to_be_bytes()[1..]);
96        e
97    };
98    events.push((0, tempo_event));
99    let mut sorted = midi.notes.clone();
100    sorted.sort_by_key(|n| n.tick_on);
101    for note in &sorted {
102        let on_ev = vec![
103            0x90 | (note.channel & 0x0F),
104            note.pitch & 0x7F,
105            note.velocity & 0x7F,
106        ];
107        let off_ev = vec![0x80 | (note.channel & 0x0F), note.pitch & 0x7F, 0x40];
108        events.push((note.tick_on, on_ev));
109        events.push((note.tick_off, off_ev));
110    }
111    events.sort_by_key(|e| e.0);
112    let mut track_data = Vec::new();
113    let mut prev_tick = 0u32;
114    for (tick, ev) in &events {
115        let delta = tick.saturating_sub(prev_tick);
116        track_data.extend_from_slice(&encode_vlq(delta));
117        track_data.extend_from_slice(ev);
118        prev_tick = *tick;
119    }
120    track_data.extend_from_slice(&encode_vlq(0));
121    track_data.extend_from_slice(&[0xFF, 0x2F, 0x00]);
122    let mut chunk = Vec::new();
123    chunk.extend_from_slice(b"MTrk");
124    chunk.extend_from_slice(&(track_data.len() as u32).to_be_bytes());
125    chunk.extend_from_slice(&track_data);
126    chunk
127}
128
129/// Export MIDI to bytes.
130#[allow(dead_code)]
131pub fn export_midi(midi: &MidiExport) -> Vec<u8> {
132    let mut out = build_midi_header(midi);
133    out.extend_from_slice(&build_midi_track(midi));
134    out
135}
136
137/// Duration in ticks.
138#[allow(dead_code)]
139pub fn midi_duration_ticks(midi: &MidiExport) -> u32 {
140    midi.notes.iter().map(|n| n.tick_off).max().unwrap_or(0)
141}
142
143/// Duration in seconds.
144#[allow(dead_code)]
145pub fn midi_duration_secs(midi: &MidiExport) -> f32 {
146    let ticks = midi_duration_ticks(midi) as f32;
147    let beats_per_sec = midi.tempo_bpm as f32 / 60.0;
148    ticks / (midi.ticks_per_beat as f32 * beats_per_sec)
149}
150
151/// Note count.
152#[allow(dead_code)]
153pub fn midi_note_count(midi: &MidiExport) -> usize {
154    midi.notes.len()
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    fn simple_midi() -> MidiExport {
162        let mut m = MidiExport::new(120, 480);
163        add_note(
164            &mut m,
165            MidiNote {
166                tick_on: 0,
167                tick_off: 480,
168                channel: 0,
169                pitch: 60,
170                velocity: 100,
171            },
172        );
173        add_note(
174            &mut m,
175            MidiNote {
176                tick_on: 480,
177                tick_off: 960,
178                channel: 0,
179                pitch: 64,
180                velocity: 80,
181            },
182        );
183        m
184    }
185
186    #[test]
187    fn header_starts_with_mthd() {
188        let m = simple_midi();
189        let hdr = build_midi_header(&m);
190        assert_eq!(&hdr[0..4], b"MThd");
191    }
192
193    #[test]
194    fn header_length_14() {
195        let m = simple_midi();
196        let hdr = build_midi_header(&m);
197        // MThd(4) + chunk_len(4) + format(2) + num_tracks(2) + ticks_per_beat(2) = 14
198        assert_eq!(hdr.len(), 14);
199    }
200
201    #[test]
202    fn track_starts_with_mtrk() {
203        let m = simple_midi();
204        let trk = build_midi_track(&m);
205        assert_eq!(&trk[0..4], b"MTrk");
206    }
207
208    #[test]
209    fn export_midi_nonempty() {
210        let m = simple_midi();
211        let out = export_midi(&m);
212        assert!(out.len() > 10);
213    }
214
215    #[test]
216    fn midi_note_count_correct() {
217        let m = simple_midi();
218        assert_eq!(midi_note_count(&m), 2);
219    }
220
221    #[test]
222    fn midi_duration_ticks_correct() {
223        let m = simple_midi();
224        assert_eq!(midi_duration_ticks(&m), 960);
225    }
226
227    #[test]
228    fn midi_duration_secs_positive() {
229        let m = simple_midi();
230        let d = midi_duration_secs(&m);
231        assert!(d > 0.0);
232    }
233
234    #[test]
235    fn vlq_zero() {
236        let v = encode_vlq(0);
237        assert_eq!(v, vec![0]);
238    }
239
240    #[test]
241    fn vlq_128() {
242        let v = encode_vlq(128);
243        assert_eq!(v, vec![0x81, 0x00]);
244    }
245
246    #[test]
247    fn empty_midi_exports() {
248        let m = MidiExport::new(120, 480);
249        let out = export_midi(&m);
250        assert!(out.len() >= 10);
251    }
252}