devalang_wasm/engine/audio/
midi.rs

1use crate::engine::audio::events::AudioEvent;
2use crate::language::syntax::ast::Value;
3/// MIDI file loading and parsing
4use anyhow::{Result, anyhow};
5use std::path::Path;
6
7#[cfg(any(feature = "cli", feature = "wasm"))]
8use midly::{Format, Header, MidiMessage, Smf, Timing, Track, TrackEvent, TrackEventKind, MetaMessage};
9
10#[cfg(all(target_arch = "wasm32", not(any(feature = "cli", feature = "wasm"))))]
11use crate::midly::{Format, Header, MidiMessage, Smf, Timing, Track, TrackEvent, TrackEventKind, MetaMessage};
12
13#[cfg(feature = "cli")]
14use std::collections::HashMap;
15
16/// Load a MIDI file and return a Value::Map representing the MIDI data
17#[cfg(feature = "cli")]
18pub fn load_midi_file(path: &Path) -> Result<Value> {
19    let bytes = std::fs::read(path)
20        .map_err(|e| anyhow!("Failed to read MIDI file {}: {}", path.display(), e))?;
21
22    let smf = Smf::parse(&bytes)
23        .map_err(|e| anyhow!("Failed to parse MIDI file {}: {}", path.display(), e))?;
24
25    // Convert MIDI data to a map
26    let mut midi_map = HashMap::new();
27
28    // Defaults
29    let mut bpm = 120.0f32;
30    let mut tempo_us_per_quarter: u32 = 500_000; // default 120 BPM
31    let mut notes: Vec<Value> = Vec::new();
32
33    // Determine ticks per beat from header timing
34    let ticks_per_beat: u32 = match smf.header.timing {
35        Timing::Metrical(t) => t.as_int() as u32,
36        _ => 480u32,
37    };
38
39    // Active note-on map to pair note-offs: key = (track, channel, key) -> Vec of indices in notes
40    let mut active: std::collections::HashMap<(usize, u8, u8), Vec<usize>> = std::collections::HashMap::new();
41
42    // Process all tracks
43    for (track_idx, track) in smf.tracks.iter().enumerate() {
44        let mut current_ticks: u32 = 0;
45
46        for event in track {
47            current_ticks = current_ticks.wrapping_add(event.delta.as_int() as u32);
48
49            match event.kind {
50                TrackEventKind::Midi { channel, message } => {
51                    let chan = channel.as_int();
52                    match message {
53                        MidiMessage::NoteOn { key, vel } => {
54                            if vel.as_int() > 0 {
55                                // compute time in ms from ticks using current tempo
56                                let time_ms = (current_ticks as f32) * (tempo_us_per_quarter as f32) / (ticks_per_beat as f32) / 1000.0;
57
58                                let mut note_map = HashMap::new();
59                                note_map.insert("tick".to_string(), Value::Number(current_ticks as f32));
60                                note_map.insert("time".to_string(), Value::Number(time_ms));
61                                // store beat position (useful to rescale when interpreter BPM changes)
62                                let beats = time_ms * (bpm as f32) / 60000.0; // time_ms / (60000/midi_bpm)
63                                note_map.insert("beat".to_string(), Value::Number(beats));
64                                note_map.insert("note".to_string(), Value::Number(key.as_int() as f32));
65                                note_map.insert("velocity".to_string(), Value::Number(vel.as_int() as f32));
66                                note_map.insert("track".to_string(), Value::Number(track_idx as f32));
67                                note_map.insert("channel".to_string(), Value::Number(chan as f32));
68
69                                notes.push(Value::Map(note_map));
70
71                                // record active index for pairing
72                                let idx = notes.len() - 1;
73                                active.entry((track_idx, chan as u8, key.as_int() as u8)).or_default().push(idx);
74                            }
75                        }
76                        MidiMessage::NoteOff { key, .. } => {
77                            // pair with most recent active note-on for same track/channel/key
78                            let key_tuple = (track_idx, channel.as_int() as u8, key.as_int() as u8);
79                            if let Some(vec_idxs) = active.get_mut(&key_tuple) {
80                                if let Some(on_idx) = vec_idxs.pop() {
81                                    // compute duration from ticks
82                                    // find onset tick stored in notes[on_idx]
83                                    if let Some(Value::Map(on_map)) = notes.get_mut(on_idx) {
84                                        if let Some(Value::Number(on_tick)) = on_map.get("tick") {
85                                            let onset_ticks = *on_tick as u32;
86                                            let dur_ticks = current_ticks.saturating_sub(onset_ticks);
87                                            let duration_ms = (dur_ticks as f32) * (tempo_us_per_quarter as f32) / (ticks_per_beat as f32) / 1000.0;
88                                            on_map.insert("duration".to_string(), Value::Number(duration_ms));
89                                            // also store duration in beats for easier rescaling
90                                            let duration_beats = duration_ms * (bpm as f32) / 60000.0;
91                                            on_map.insert("duration_beats".to_string(), Value::Number(duration_beats));
92                                        }
93                                    }
94                                }
95                            }
96                        }
97                        _ => {}
98                    }
99                }
100                TrackEventKind::Meta(meta) => {
101                    // Extract tempo if present
102                    if let MetaMessage::Tempo(t) = meta {
103                        tempo_us_per_quarter = t.as_int();
104                        bpm = 60_000_000.0f32 / tempo_us_per_quarter as f32;
105                    }
106                }
107                _ => {}
108            }
109        }
110    }
111
112    // For any lingering active notes without note-off, set a default duration (500 ms)
113                for (_key, vec_idxs) in active.iter() {
114        for &idx in vec_idxs.iter() {
115            if let Some(Value::Map(m)) = notes.get_mut(idx) {
116                if !m.contains_key("duration") {
117                    m.insert("duration".to_string(), Value::Number(500.0));
118                    // default duration beats using current bpm
119                    let default_beats = 500.0 * (bpm as f32) / 60000.0;
120                    m.insert("duration_beats".to_string(), Value::Number(default_beats));
121                }
122            }
123        }
124    }
125
126    // Store in map
127    midi_map.insert("bpm".to_string(), Value::Number(bpm));
128    midi_map.insert("ticks_per_beat".to_string(), Value::Number(ticks_per_beat as f32));
129    midi_map.insert("notes".to_string(), Value::Array(notes));
130    midi_map.insert("type".to_string(), Value::String("midi".to_string()));
131
132    Ok(Value::Map(midi_map))
133}
134
135#[cfg(not(feature = "cli"))]
136pub fn load_midi_file(_path: &Path) -> Result<Value> {
137    Err(anyhow!("MIDI loading not available without 'cli' feature"))
138}
139
140// ============================================================================
141// MIDI EXPORT
142// ============================================================================
143
144/// Export AudioEvents to MIDI bytes (for WASM)
145pub fn events_to_midi_bytes(events: &[AudioEvent], bpm: f32) -> Result<Vec<u8>> {
146    if events.is_empty() {
147        return Err(anyhow!("No events to export"));
148    }
149
150    // Create MIDI header (single track format)
151    let ticks_per_beat = 480; // Standard MIDI resolution
152    let header = Header::new(Format::SingleTrack, Timing::Metrical(ticks_per_beat.into()));
153
154    // Create track events list
155    let mut track_events = Vec::new();
156
157    // Add tempo meta event at start
158    let tempo_us_per_quarter = (60_000_000.0 / bpm) as u32;
159    track_events.push(TrackEvent {
160        delta: 0.into(),
161        kind: TrackEventKind::Meta(MetaMessage::Tempo(tempo_us_per_quarter.into())),
162    });
163
164    // Collect all note events (expand chords to individual notes)
165    let mut midi_notes = Vec::new();
166
167    for event in events {
168        match event {
169            AudioEvent::Note {
170                midi,
171                start_time,
172                duration,
173                velocity,
174                ..
175            } => {
176                midi_notes.push(MidiNote {
177                    note: *midi,
178                    start: *start_time,
179                    duration: *duration,
180                    velocity: *velocity,
181                });
182            }
183            AudioEvent::Chord {
184                midis,
185                start_time,
186                duration,
187                velocity,
188                ..
189            } => {
190                // Expand chord into individual notes
191                for &note in midis {
192                    midi_notes.push(MidiNote {
193                        note,
194                        start: *start_time,
195                        duration: *duration,
196                        velocity: *velocity,
197                    });
198                }
199            }
200            AudioEvent::Sample { .. } => {
201                // Samples are not exported to MIDI
202            }
203        }
204    }
205
206    // Sort by start time
207    midi_notes.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
208
209    // Convert events to MIDI messages with proper delta timing
210    let mut midi_messages: Vec<MidiEventTimed> = Vec::new();
211
212    for note in &midi_notes {
213        let start_ticks = time_to_ticks(note.start, bpm, ticks_per_beat);
214        let end_ticks = time_to_ticks(note.start + note.duration, bpm, ticks_per_beat);
215
216        // Note On
217        midi_messages.push(MidiEventTimed {
218            ticks: start_ticks,
219            message: MidiMessage::NoteOn {
220                key: note.note.into(),
221                vel: (note.velocity as u8).into(),
222            },
223        });
224
225        // Note Off
226        midi_messages.push(MidiEventTimed {
227            ticks: end_ticks,
228            message: MidiMessage::NoteOff {
229                key: note.note.into(),
230                vel: 0.into(),
231            },
232        });
233    }
234
235    // Sort all messages by time
236    midi_messages.sort_by_key(|msg| msg.ticks);
237
238    // Convert to delta times and create TrackEvents
239    let mut last_ticks = 0u32;
240
241    for msg in midi_messages {
242        let delta = msg.ticks.saturating_sub(last_ticks);
243
244        track_events.push(TrackEvent {
245            delta: delta.into(),
246            kind: TrackEventKind::Midi {
247                channel: 0.into(),
248                message: msg.message,
249            },
250        });
251
252        last_ticks = msg.ticks;
253    }
254
255    // End of track marker
256    track_events.push(TrackEvent {
257        delta: 0.into(),
258        kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
259    });
260
261    // Create SMF and write to memory buffer
262    let track = Track::from(track_events);
263    let mut smf = Smf::new(header);
264    smf.tracks.push(track);
265
266    // Write to memory buffer
267    let mut buffer = Vec::new();
268    smf.write(&mut buffer)
269        .map_err(|e| anyhow!("Failed to write MIDI bytes: {}", e))?;
270
271    Ok(buffer)
272}
273
274/// Export AudioEvents to a standard MIDI file
275#[cfg(feature = "cli")]
276pub fn export_midi_file(events: &[AudioEvent], output_path: &Path, bpm: f32) -> Result<()> {
277    if events.is_empty() {
278        return Err(anyhow!("No events to export"));
279    }
280
281    // Create MIDI header (single track format)
282    let ticks_per_beat = 480; // Standard MIDI resolution
283    let header = Header::new(Format::SingleTrack, Timing::Metrical(ticks_per_beat.into()));
284
285    // Create track events list
286    let mut track_events = Vec::new();
287
288    // Add tempo meta event at start
289    let tempo_us_per_quarter = (60_000_000.0 / bpm) as u32;
290    track_events.push(TrackEvent {
291        delta: 0.into(),
292        kind: TrackEventKind::Meta(MetaMessage::Tempo(tempo_us_per_quarter.into())),
293    });
294
295    // Collect all note events (expand chords to individual notes)
296    let mut midi_notes = Vec::new();
297
298    for event in events {
299        match event {
300            AudioEvent::Note {
301                midi,
302                start_time,
303                duration,
304                velocity,
305                ..
306            } => {
307                midi_notes.push(MidiNote {
308                    note: *midi,
309                    start: *start_time,
310                    duration: *duration,
311                    velocity: *velocity,
312                });
313            }
314            AudioEvent::Chord {
315                midis,
316                start_time,
317                duration,
318                velocity,
319                ..
320            } => {
321                // Expand chord into individual notes
322                for &note in midis {
323                    midi_notes.push(MidiNote {
324                        note,
325                        start: *start_time,
326                        duration: *duration,
327                        velocity: *velocity,
328                    });
329                }
330            }
331            AudioEvent::Sample { .. } => {
332                // Samples are not exported to MIDI
333            }
334        }
335    }
336
337    // Sort by start time
338    midi_notes.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
339
340    // Convert events to MIDI messages with proper delta timing
341    let mut midi_messages: Vec<MidiEventTimed> = Vec::new();
342
343    for note in &midi_notes {
344        let start_ticks = time_to_ticks(note.start, bpm, ticks_per_beat);
345        let end_ticks = time_to_ticks(note.start + note.duration, bpm, ticks_per_beat);
346
347        // Note On
348        midi_messages.push(MidiEventTimed {
349            ticks: start_ticks,
350            message: MidiMessage::NoteOn {
351                key: note.note.into(),
352                vel: (note.velocity as u8).into(),
353            },
354        });
355
356        // Note Off
357        midi_messages.push(MidiEventTimed {
358            ticks: end_ticks,
359            message: MidiMessage::NoteOff {
360                key: note.note.into(),
361                vel: 0.into(),
362            },
363        });
364    }
365
366    // Sort all messages by time
367    midi_messages.sort_by_key(|msg| msg.ticks);
368
369    // Convert to delta times and create TrackEvents
370    let mut last_ticks = 0u32;
371
372    for msg in midi_messages {
373        let delta = msg.ticks.saturating_sub(last_ticks);
374
375        track_events.push(TrackEvent {
376            delta: delta.into(),
377            kind: TrackEventKind::Midi {
378                channel: 0.into(),
379                message: msg.message,
380            },
381        });
382
383        last_ticks = msg.ticks;
384    }
385
386    // End of track marker
387    track_events.push(TrackEvent {
388        delta: 0.into(),
389        kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
390    });
391
392    // Create SMF and write to file
393    let track = Track::from(track_events);
394    let mut smf = Smf::new(header);
395    smf.tracks.push(track);
396
397    // Write to file directly (midly 0.5 API)
398    smf.save(output_path)
399        .map_err(|e| anyhow!("Failed to write MIDI file {}: {}", output_path.display(), e))?;
400
401    println!(
402        "✅ MIDI exported: {} ({} events)",
403        output_path.display(),
404        events.len()
405    );
406    Ok(())
407}
408
409#[cfg(not(feature = "cli"))]
410pub fn export_midi_file(_events: &[AudioEvent], _output_path: &Path, _bpm: f32) -> Result<()> {
411    Err(anyhow!("MIDI export not available without 'cli' feature"))
412}
413
414// Helper structures for MIDI export
415#[derive(Debug, Clone)]
416struct MidiNote {
417    note: u8,
418    start: f32,
419    duration: f32,
420    velocity: f32,
421}
422
423#[derive(Debug, Clone)]
424struct MidiEventTimed {
425    ticks: u32,
426    message: MidiMessage,
427}
428
429/// Convert time in seconds to MIDI ticks
430fn time_to_ticks(time_seconds: f32, bpm: f32, ticks_per_beat: u16) -> u32 {
431    let beats = time_seconds * (bpm / 60.0);
432    (beats * ticks_per_beat as f32) as u32
433}