use crate::audio::midi::{MidiClip, MidiClipId, MidiEvent}; use std::fs; use std::path::Path; /// Load a MIDI file and convert it to a MidiClip pub fn load_midi_file>( path: P, clip_id: MidiClipId, _sample_rate: u32, ) -> Result { // Read the MIDI file let data = fs::read(path.as_ref()).map_err(|e| format!("Failed to read MIDI file: {}", e))?; // Parse with midly let smf = midly::Smf::parse(&data).map_err(|e| format!("Failed to parse MIDI file: {}", e))?; // Convert timing to ticks per second let ticks_per_beat = match smf.header.timing { midly::Timing::Metrical(tpb) => tpb.as_int() as f64, midly::Timing::Timecode(fps, subframe) => { // For timecode, calculate equivalent ticks per second (fps.as_f32() * subframe as f32) as f64 } }; // First pass: collect all events with their tick positions and tempo changes #[derive(Debug)] enum RawEvent { Midi { tick: u64, channel: u8, message: midly::MidiMessage, }, Tempo { tick: u64, microseconds_per_beat: f64, }, } let mut raw_events = Vec::new(); let mut max_time_ticks = 0u64; // Collect all events from all tracks with their absolute tick positions for track in &smf.tracks { let mut current_tick = 0u64; for event in track { current_tick += event.delta.as_int() as u64; max_time_ticks = max_time_ticks.max(current_tick); match event.kind { midly::TrackEventKind::Midi { channel, message } => { raw_events.push(RawEvent::Midi { tick: current_tick, channel: channel.as_int(), message, }); } midly::TrackEventKind::Meta(midly::MetaMessage::Tempo(tempo)) => { raw_events.push(RawEvent::Tempo { tick: current_tick, microseconds_per_beat: tempo.as_int() as f64, }); } _ => { // Ignore other meta events } } } } // Sort all events by tick position raw_events.sort_by_key(|e| match e { RawEvent::Midi { tick, .. } => *tick, RawEvent::Tempo { tick, .. } => *tick, }); // Second pass: convert ticks to timestamps with proper tempo tracking let mut events = Vec::new(); let mut microseconds_per_beat = 500000.0; // Default: 120 BPM let mut last_tick = 0u64; let mut accumulated_time = 0.0; // Time in seconds for raw_event in raw_events { match raw_event { RawEvent::Tempo { tick, microseconds_per_beat: new_tempo, } => { // Update accumulated time up to this tempo change let delta_ticks = tick - last_tick; let delta_time = (delta_ticks as f64 / ticks_per_beat) * (microseconds_per_beat / 1_000_000.0); accumulated_time += delta_time; last_tick = tick; // Update tempo for future events microseconds_per_beat = new_tempo; } RawEvent::Midi { tick, channel, message, } => { // Calculate time for this event let delta_ticks = tick - last_tick; let delta_time = (delta_ticks as f64 / ticks_per_beat) * (microseconds_per_beat / 1_000_000.0); accumulated_time += delta_time; last_tick = tick; // Store timestamp in seconds (sample-rate independent) let timestamp = accumulated_time; match message { midly::MidiMessage::NoteOn { key, vel } => { let velocity = vel.as_int(); if velocity > 0 { events.push(MidiEvent::note_on( timestamp, channel, key.as_int(), velocity, )); } else { events.push(MidiEvent::note_off(timestamp, channel, key.as_int(), 64)); } } midly::MidiMessage::NoteOff { key, vel } => { events.push(MidiEvent::note_off( timestamp, channel, key.as_int(), vel.as_int(), )); } midly::MidiMessage::Controller { controller, value } => { let status = 0xB0 | channel; events.push(MidiEvent::new( timestamp, status, controller.as_int(), value.as_int(), )); } _ => { // Ignore other MIDI messages } } } } } // Calculate final clip duration let final_delta_ticks = max_time_ticks - last_tick; let final_delta_time = (final_delta_ticks as f64 / ticks_per_beat) * (microseconds_per_beat / 1_000_000.0); let duration_seconds = accumulated_time + final_delta_time; // Create the MIDI clip (content only, positioning happens when creating instance) let clip = MidiClip::new(clip_id, events, duration_seconds, "Imported MIDI".to_string()); Ok(clip) }