1use crate::engine::audio::events::AudioEvent;
2use crate::language::syntax::ast::Value;
3use anyhow::{Result, anyhow};
5use std::path::Path;
6
7#[cfg(any(feature = "cli", feature = "wasm"))]
8use midly::{
9 Format, Header, MetaMessage, MidiMessage, Smf, Timing, Track, TrackEvent, TrackEventKind,
10};
11
12#[cfg(all(target_arch = "wasm32", not(any(feature = "cli", feature = "wasm"))))]
13use crate::midly::{
14 Format, Header, MetaMessage, MidiMessage, Smf, Timing, Track, TrackEvent, TrackEventKind,
15};
16
17#[cfg(feature = "cli")]
18use std::collections::HashMap;
19
20#[cfg(feature = "cli")]
22pub fn load_midi_file(path: &Path) -> Result<Value> {
23 let bytes = std::fs::read(path)
24 .map_err(|e| anyhow!("Failed to read MIDI file {}: {}", path.display(), e))?;
25
26 let smf = Smf::parse(&bytes)
27 .map_err(|e| anyhow!("Failed to parse MIDI file {}: {}", path.display(), e))?;
28
29 let mut midi_map = HashMap::new();
31
32 let mut bpm = 120.0f32;
34 let mut tempo_us_per_quarter: u32 = 500_000; let mut notes: Vec<Value> = Vec::new();
36
37 let ticks_per_beat: u32 = match smf.header.timing {
39 Timing::Metrical(t) => t.as_int() as u32,
40 _ => 480u32,
41 };
42
43 let mut active: std::collections::HashMap<(usize, u8, u8), Vec<usize>> =
45 std::collections::HashMap::new();
46
47 for (track_idx, track) in smf.tracks.iter().enumerate() {
49 let mut current_ticks: u32 = 0;
50
51 for event in track {
52 current_ticks = current_ticks.wrapping_add(event.delta.as_int() as u32);
53
54 match event.kind {
55 TrackEventKind::Midi { channel, message } => {
56 let chan = channel.as_int();
57 match message {
58 MidiMessage::NoteOn { key, vel } => {
59 if vel.as_int() > 0 {
60 let time_ms = (current_ticks as f32)
62 * (tempo_us_per_quarter as f32)
63 / (ticks_per_beat as f32)
64 / 1000.0;
65
66 let mut note_map = HashMap::new();
67 note_map.insert(
68 "tick".to_string(),
69 Value::Number(current_ticks as f32),
70 );
71 note_map.insert("time".to_string(), Value::Number(time_ms));
72 let beats = time_ms * (bpm as f32) / 60000.0; note_map.insert("beat".to_string(), Value::Number(beats));
75 note_map
76 .insert("note".to_string(), Value::Number(key.as_int() as f32));
77 note_map.insert(
78 "velocity".to_string(),
79 Value::Number(vel.as_int() as f32),
80 );
81 note_map
82 .insert("track".to_string(), Value::Number(track_idx as f32));
83 note_map.insert("channel".to_string(), Value::Number(chan as f32));
84
85 notes.push(Value::Map(note_map));
86
87 let idx = notes.len() - 1;
89 active
90 .entry((track_idx, chan as u8, key.as_int() as u8))
91 .or_default()
92 .push(idx);
93 }
94 }
95 MidiMessage::NoteOff { key, .. } => {
96 let key_tuple = (track_idx, channel.as_int() as u8, key.as_int() as u8);
98 if let Some(vec_idxs) = active.get_mut(&key_tuple) {
99 if let Some(on_idx) = vec_idxs.pop() {
100 if let Some(Value::Map(on_map)) = notes.get_mut(on_idx) {
103 if let Some(Value::Number(on_tick)) = on_map.get("tick") {
104 let onset_ticks = *on_tick as u32;
105 let dur_ticks =
106 current_ticks.saturating_sub(onset_ticks);
107 let duration_ms = (dur_ticks as f32)
108 * (tempo_us_per_quarter as f32)
109 / (ticks_per_beat as f32)
110 / 1000.0;
111 on_map.insert(
112 "duration".to_string(),
113 Value::Number(duration_ms),
114 );
115 let duration_beats =
117 duration_ms * (bpm as f32) / 60000.0;
118 on_map.insert(
119 "duration_beats".to_string(),
120 Value::Number(duration_beats),
121 );
122 }
123 }
124 }
125 }
126 }
127 _ => {}
128 }
129 }
130 TrackEventKind::Meta(meta) => {
131 if let MetaMessage::Tempo(t) = meta {
133 tempo_us_per_quarter = t.as_int();
134 bpm = 60_000_000.0f32 / tempo_us_per_quarter as f32;
135 }
136 }
137 _ => {}
138 }
139 }
140 }
141
142 for (_key, vec_idxs) in active.iter() {
144 for &idx in vec_idxs.iter() {
145 if let Some(Value::Map(m)) = notes.get_mut(idx) {
146 if !m.contains_key("duration") {
147 m.insert("duration".to_string(), Value::Number(500.0));
148 let default_beats = 500.0 * (bpm as f32) / 60000.0;
150 m.insert("duration_beats".to_string(), Value::Number(default_beats));
151 }
152 }
153 }
154 }
155
156 midi_map.insert("bpm".to_string(), Value::Number(bpm));
158 midi_map.insert(
159 "ticks_per_beat".to_string(),
160 Value::Number(ticks_per_beat as f32),
161 );
162 midi_map.insert("notes".to_string(), Value::Array(notes));
163 midi_map.insert("type".to_string(), Value::String("midi".to_string()));
164
165 Ok(Value::Map(midi_map))
166}
167
168#[cfg(not(feature = "cli"))]
169pub fn load_midi_file(_path: &Path) -> Result<Value> {
170 Err(anyhow!("MIDI loading not available without 'cli' feature"))
171}
172
173pub fn events_to_midi_bytes(events: &[AudioEvent], bpm: f32) -> Result<Vec<u8>> {
179 if events.is_empty() {
180 return Err(anyhow!("No events to export"));
181 }
182 let bpm = validate_bpm(bpm)?;
183
184 let ticks_per_beat = 480; let header = Header::new(Format::SingleTrack, Timing::Metrical(ticks_per_beat.into()));
187
188 let mut track_events = Vec::new();
190
191 let tempo_us_per_quarter = (60_000_000.0 / bpm) as u32;
193 track_events.push(TrackEvent {
194 delta: 0.into(),
195 kind: TrackEventKind::Meta(MetaMessage::Tempo(tempo_us_per_quarter.into())),
196 });
197
198 let midi_notes = collect_midi_notes(events);
199
200 let mut midi_messages: Vec<MidiEventTimed> = Vec::new();
202
203 for note in &midi_notes {
204 let start_ticks = time_to_ticks(note.start, bpm, ticks_per_beat);
205 let end_ticks = time_to_ticks(note.start + note.duration, bpm, ticks_per_beat);
206
207 midi_messages.push(MidiEventTimed {
209 ticks: start_ticks,
210 message: MidiMessage::NoteOn {
211 key: note.note.into(),
212 vel: ((note.velocity * 127.0) as u8).into(),
213 },
214 });
215
216 midi_messages.push(MidiEventTimed {
218 ticks: end_ticks,
219 message: MidiMessage::NoteOff {
220 key: note.note.into(),
221 vel: 0.into(),
222 },
223 });
224 }
225
226 midi_messages.sort_by_key(|msg| msg.ticks);
228
229 let mut last_ticks = 0u32;
231
232 for msg in midi_messages {
233 let delta = msg.ticks.saturating_sub(last_ticks);
234
235 track_events.push(TrackEvent {
236 delta: delta.into(),
237 kind: TrackEventKind::Midi {
238 channel: 0.into(),
239 message: msg.message,
240 },
241 });
242
243 last_ticks = msg.ticks;
244 }
245
246 track_events.push(TrackEvent {
248 delta: 0.into(),
249 kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
250 });
251
252 let track = Track::from(track_events);
254 let mut smf = Smf::new(header);
255 smf.tracks.push(track);
256
257 let mut buffer = Vec::new();
259 smf.write(&mut buffer)
260 .map_err(|e| anyhow!("Failed to write MIDI bytes: {}", e))?;
261
262 Ok(buffer)
263}
264
265#[cfg(feature = "cli")]
267pub fn export_midi_file(events: &[AudioEvent], output_path: &Path, bpm: f32) -> Result<()> {
268 if events.is_empty() {
269 return Err(anyhow!("No events to export"));
270 }
271 let bpm = validate_bpm(bpm)?;
272
273 let ticks_per_beat = 480; let header = Header::new(Format::SingleTrack, Timing::Metrical(ticks_per_beat.into()));
276
277 let mut track_events = Vec::new();
279
280 let tempo_us_per_quarter = (60_000_000.0 / bpm) as u32;
282 track_events.push(TrackEvent {
283 delta: 0.into(),
284 kind: TrackEventKind::Meta(MetaMessage::Tempo(tempo_us_per_quarter.into())),
285 });
286
287 let midi_notes = collect_midi_notes(events);
288
289 let mut midi_messages: Vec<MidiEventTimed> = Vec::new();
291
292 for note in &midi_notes {
293 let start_ticks = time_to_ticks(note.start, bpm, ticks_per_beat);
294 let end_ticks = time_to_ticks(note.start + note.duration, bpm, ticks_per_beat);
295
296 midi_messages.push(MidiEventTimed {
298 ticks: start_ticks,
299 message: MidiMessage::NoteOn {
300 key: note.note.into(),
301 vel: ((note.velocity * 127.0) as u8).into(),
302 },
303 });
304
305 midi_messages.push(MidiEventTimed {
307 ticks: end_ticks,
308 message: MidiMessage::NoteOff {
309 key: note.note.into(),
310 vel: 0.into(),
311 },
312 });
313 }
314
315 midi_messages.sort_by_key(|msg| msg.ticks);
317
318 let mut last_ticks = 0u32;
320
321 for msg in midi_messages {
322 let delta = msg.ticks.saturating_sub(last_ticks);
323
324 track_events.push(TrackEvent {
325 delta: delta.into(),
326 kind: TrackEventKind::Midi {
327 channel: 0.into(),
328 message: msg.message,
329 },
330 });
331
332 last_ticks = msg.ticks;
333 }
334
335 track_events.push(TrackEvent {
337 delta: 0.into(),
338 kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
339 });
340
341 let track = Track::from(track_events);
343 let mut smf = Smf::new(header);
344 smf.tracks.push(track);
345
346 smf.save(output_path)
348 .map_err(|e| anyhow!("Failed to write MIDI file {}: {}", output_path.display(), e))?;
349
350 println!(
351 "✅ MIDI exported: {} ({} events in, {} notes written)",
352 output_path.display(),
353 events.len(),
354 midi_notes.len()
355 );
356 Ok(())
357}
358
359#[cfg(not(feature = "cli"))]
360pub fn export_midi_file(_events: &[AudioEvent], _output_path: &Path, _bpm: f32) -> Result<()> {
361 Err(anyhow!("MIDI export not available without 'cli' feature"))
362}
363
364#[derive(Debug, Clone)]
366struct MidiNote {
367 note: u8,
368 start: f32,
369 duration: f32,
370 velocity: f32,
371}
372
373#[derive(Debug, Clone)]
374struct MidiEventTimed {
375 ticks: u32,
376 message: MidiMessage,
377}
378
379fn validate_bpm(bpm: f32) -> Result<f32> {
380 if bpm.is_finite() && bpm > 0.0 {
381 Ok(bpm)
382 } else {
383 Err(anyhow!("Invalid BPM for MIDI export: {}", bpm))
384 }
385}
386
387fn collect_midi_notes(events: &[AudioEvent]) -> Vec<MidiNote> {
388 let mut midi_notes = Vec::new();
389
390 for event in events {
391 match event {
392 AudioEvent::Note {
393 midi,
394 start_time,
395 duration,
396 velocity,
397 ..
398 } => push_midi_note(&mut midi_notes, *midi, *start_time, *duration, *velocity),
399 AudioEvent::Chord {
400 midis,
401 start_time,
402 duration,
403 velocity,
404 ..
405 } => {
406 for ¬e in midis {
407 push_midi_note(&mut midi_notes, note, *start_time, *duration, *velocity);
408 }
409 }
410 AudioEvent::Sample { .. } | AudioEvent::Stop { .. } => {}
411 }
412 }
413
414 midi_notes.sort_by(|a, b| a.start.total_cmp(&b.start));
415 midi_notes
416}
417
418fn push_midi_note(notes: &mut Vec<MidiNote>, note: u8, start: f32, duration: f32, velocity: f32) {
419 if !start.is_finite() || !duration.is_finite() || duration <= 0.0 {
420 return;
421 }
422
423 notes.push(MidiNote {
424 note,
425 start: start.max(0.0),
426 duration,
427 velocity: velocity.clamp(0.0, 1.0),
428 });
429}
430
431fn time_to_ticks(time_seconds: f32, bpm: f32, ticks_per_beat: u16) -> u32 {
433 let beats = time_seconds * (bpm / 60.0);
434 (beats * ticks_per_beat as f32) as u32
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 fn note_event(start_time: f32, duration: f32, velocity: f32) -> AudioEvent {
442 AudioEvent::Note {
443 node_id: Some("master".to_string()),
444 midi: 60,
445 start_time,
446 duration,
447 velocity,
448 synth_id: "test".to_string(),
449 synth_def: Default::default(),
450 pan: 0.0,
451 detune: 0.0,
452 gain: 1.0,
453 attack: None,
454 release: None,
455 delay_time: None,
456 delay_feedback: None,
457 delay_mix: None,
458 reverb_amount: None,
459 drive_amount: None,
460 drive_color: None,
461 effects: None,
462 use_per_note_automation: false,
463 }
464 }
465
466 #[test]
467 fn test_midi_export_rejects_invalid_bpm() {
468 let events = vec![note_event(0.0, 0.5, 1.0)];
469
470 assert!(events_to_midi_bytes(&events, 0.0).is_err());
471 assert!(events_to_midi_bytes(&events, f32::NAN).is_err());
472 }
473
474 #[test]
475 fn test_collect_midi_notes_filters_invalid_timings_and_clamps_values() {
476 let events = vec![
477 note_event(f32::NAN, 0.5, 1.0),
478 note_event(0.0, -0.5, 1.0),
479 note_event(-1.0, 0.5, 2.0),
480 ];
481
482 let notes = collect_midi_notes(&events);
483
484 assert_eq!(notes.len(), 1);
485 assert_eq!(notes[0].start, 0.0);
486 assert_eq!(notes[0].velocity, 1.0);
487 }
488}