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, 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#[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 let mut midi_map = HashMap::new();
27
28 let mut bpm = 120.0f32;
30 let mut tempo_us_per_quarter: u32 = 500_000; let mut notes: Vec<Value> = Vec::new();
32
33 let ticks_per_beat: u32 = match smf.header.timing {
35 Timing::Metrical(t) => t.as_int() as u32,
36 _ => 480u32,
37 };
38
39 let mut active: std::collections::HashMap<(usize, u8, u8), Vec<usize>> = std::collections::HashMap::new();
41
42 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 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 let beats = time_ms * (bpm as f32) / 60000.0; 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 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 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 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 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 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 (_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 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 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
140pub 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 let ticks_per_beat = 480; let header = Header::new(Format::SingleTrack, Timing::Metrical(ticks_per_beat.into()));
153
154 let mut track_events = Vec::new();
156
157 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 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 for ¬e 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 }
203 }
204 }
205
206 midi_notes.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
208
209 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 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 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 midi_messages.sort_by_key(|msg| msg.ticks);
237
238 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 track_events.push(TrackEvent {
257 delta: 0.into(),
258 kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
259 });
260
261 let track = Track::from(track_events);
263 let mut smf = Smf::new(header);
264 smf.tracks.push(track);
265
266 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#[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 let ticks_per_beat = 480; let header = Header::new(Format::SingleTrack, Timing::Metrical(ticks_per_beat.into()));
284
285 let mut track_events = Vec::new();
287
288 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 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 for ¬e 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 }
334 }
335 }
336
337 midi_notes.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap());
339
340 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 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 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 midi_messages.sort_by_key(|msg| msg.ticks);
368
369 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 track_events.push(TrackEvent {
388 delta: 0.into(),
389 kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
390 });
391
392 let track = Track::from(track_events);
394 let mut smf = Smf::new(header);
395 smf.tracks.push(track);
396
397 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#[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
429fn 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}