devalang_wasm/engine/audio/
midi.rs1use 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::{Format, Header, MidiMessage, Smf, Timing, Track, TrackEvent, TrackEventKind};
9
10#[cfg(feature = "cli")]
11use std::collections::HashMap;
12
13#[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 let mut midi_map = HashMap::new();
24
25 let mut bpm = 120.0;
27 let mut notes = Vec::new();
28
29 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 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 }
63 _ => {}
64 }
65 }
66 TrackEventKind::Meta(meta) => {
67 if let midly::MetaMessage::Tempo(tempo) = meta {
69 bpm = 60_000_000.0 / tempo.as_int() as f32;
71 }
72 }
73 _ => {}
74 }
75 }
76 }
77
78 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
91pub 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 let ticks_per_beat = 480; let header = Header::new(Format::SingleTrack, Timing::Metrical(ticks_per_beat.into()));
104
105 let mut track_events = Vec::new();
107
108 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 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 for ¬e 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 }
154 }
155 }
156
157 midi_notes.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
159
160 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 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 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 midi_messages.sort_by_key(|msg| msg.ticks);
188
189 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 track_events.push(TrackEvent {
208 delta: 0.into(),
209 kind: TrackEventKind::Meta(midly::MetaMessage::EndOfTrack),
210 });
211
212 let track = Track::from(track_events);
214 let mut smf = Smf::new(header);
215 smf.tracks.push(track);
216
217 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#[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 let ticks_per_beat = 480; let header = Header::new(Format::SingleTrack, Timing::Metrical(ticks_per_beat.into()));
235
236 let mut track_events = Vec::new();
238
239 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 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 for ¬e 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 }
285 }
286 }
287
288 midi_notes.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
290
291 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 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 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 midi_messages.sort_by_key(|msg| msg.ticks);
319
320 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 track_events.push(TrackEvent {
339 delta: 0.into(),
340 kind: TrackEventKind::Meta(midly::MetaMessage::EndOfTrack),
341 });
342
343 let track = Track::from(track_events);
345 let mut smf = Smf::new(header);
346 smf.tracks.push(track);
347
348 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#[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
380fn 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}