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