165 lines
5.8 KiB
Rust
165 lines
5.8 KiB
Rust
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<P: AsRef<Path>>(
|
|
path: P,
|
|
clip_id: MidiClipId,
|
|
_sample_rate: u32,
|
|
) -> Result<MidiClip, String> {
|
|
// 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)
|
|
}
|