oxihuman_export/
midi_export.rs1#![allow(dead_code)]
4
5#[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#[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#[allow(dead_code)]
39pub fn add_note(midi: &mut MidiExport, note: MidiNote) {
40 midi.notes.push(note);
41}
42
43#[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#[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#[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#[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#[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#[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#[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 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}