mirror of
https://github.com/storytold/soundcraft.git
synced 2026-11-03 00:41:02 -06:00
124 lines
6.3 KiB
Rust
124 lines
6.3 KiB
Rust
use super::{ChannelFormat, Clip, ClipContent, Engine, EngineError, Result, Samples, TrackId, TrackKind};
|
|
|
|
/// Most meter changes a MIDI file may bring into the tempo map; scores have a handful.
|
|
pub(super) const MAX_MIDI_METERS: usize = 10_000;
|
|
/// Most tempo changes a MIDI file may bring into the tempo map: conversions walk the tempo events.
|
|
pub(super) const MAX_MIDI_TEMPOS: usize = 10_000;
|
|
/// Most synth tracks a MIDI file may create in the session.
|
|
pub(super) const MAX_MIDI_TRACKS: usize = 512;
|
|
|
|
/// Import a Standard MIDI File as MIDI tracks (one per SMF track).
|
|
pub fn import_midi_bytes(e: &mut Engine, bytes: &[u8], at: Samples, tempo_map: bool) -> Result<Vec<TrackId>> {
|
|
let smf = soundcraft_midi::read_smf(bytes).map_err(|err| EngineError::Io(err.to_string()))?;
|
|
if tempo_map {
|
|
// Each meter is placed by walking the ones before it.
|
|
if smf.meters.len() > MAX_MIDI_METERS {
|
|
return Err(EngineError::Io(format!("MIDI file: more than {MAX_MIDI_METERS} meter changes")));
|
|
}
|
|
if smf.tempos.len() > MAX_MIDI_TEMPOS {
|
|
return Err(EngineError::Io(format!("MIDI file: more than {MAX_MIDI_TEMPOS} tempo changes")));
|
|
}
|
|
}
|
|
let tracks_to_import = smf.tracks.iter().filter(|tr| !tr.sequence.notes.is_empty() || !tr.sequence.ctrls.is_empty()).count();
|
|
if tracks_to_import > MAX_MIDI_TRACKS {
|
|
return Err(EngineError::Io(format!("MIDI file: more than {MAX_MIDI_TRACKS} tracks")));
|
|
}
|
|
let s = e.session_mut();
|
|
if tempo_map {
|
|
for (tick, bpm) in &smf.tempos {
|
|
if s.tempo.tempo_at_tick(*tick) == *bpm {
|
|
continue;
|
|
}
|
|
let _ = s.tempo.set_tempo(*tick, *bpm);
|
|
}
|
|
for (tick, n, d) in &smf.meters {
|
|
let _ = s.tempo.set_meter(*tick, u32::from(*n), u32::from(*d));
|
|
}
|
|
}
|
|
let base_tick = s.tempo.samples_to_ticks(at, s.sample_rate);
|
|
let mut out = Vec::new();
|
|
for (i, tr) in smf.tracks.iter().enumerate() {
|
|
if tr.sequence.notes.is_empty() && tr.sequence.ctrls.is_empty() {
|
|
continue;
|
|
}
|
|
let mut name = if tr.name.trim().is_empty() { format!("MIDI {}", i + 1) } else { tr.name.clone() };
|
|
soundcraft_model::truncate_chars(&mut name, soundcraft_model::MAX_NAME_CHARS);
|
|
let id = s.add_track(TrackKind::Instrument, ChannelFormat::Stereo, Some(&name));
|
|
let drums = !tr.sequence.notes.is_empty() && tr.sequence.notes.iter().all(|n| n.channel == 9);
|
|
if let Some(t) = s.track_mut(id) {
|
|
t.instrument = Some(soundcraft_model::Insert::new(if drums { "drum_synth" } else { "subtractive_synth" }));
|
|
}
|
|
// A controller on the clip's last tick would not play, so the clip runs one tick past the last one.
|
|
let past_ctrls = tr.sequence.ctrls.iter().map(|c| c.tick.saturating_add(1)).max().unwrap_or(0);
|
|
let end_tick = base_tick.saturating_add(tr.sequence.end_tick().max(past_ctrls).max(1));
|
|
let len = s.tempo.tick_to_samples(end_tick, s.sample_rate).saturating_sub(at);
|
|
if len > s.sample_rate.samples(86_400.0) {
|
|
return Err(EngineError::Io(format!("MIDI track {}: longer than 24 hours", i + 1)));
|
|
}
|
|
let cid = s.new_clip_id();
|
|
crate::edit::place_clip(s, id, Clip::midi(cid, name, at, len.max(1), tr.sequence.clone()));
|
|
out.push(id);
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
/// Export MIDI clips as a format-1 SMF. `only` limits the file to those tracks.
|
|
pub fn export_midi(e: &Engine, only: Option<&[TrackId]>) -> Result<Vec<u8>> {
|
|
let s = e.session();
|
|
let mut smf = soundcraft_midi::Smf {
|
|
format: 1,
|
|
tracks: Vec::new(),
|
|
tempos: s.tempo.tempos().iter().map(|t| (t.tick, t.bpm)).collect(),
|
|
meters: s.tempo.meters().iter().map(|m| (m.tick, u8::try_from(m.numerator).unwrap_or(4), u8::try_from(m.denominator).unwrap_or(4))).collect(),
|
|
markers: s.markers.iter().map(|m| (s.tempo.samples_to_ticks(m.start, s.sample_rate), m.name.clone())).collect(),
|
|
key_sigs: Vec::new(),
|
|
};
|
|
let chosen: Vec<_> = s.tracks.iter().filter(|t| t.kind.is_midi() && only.is_none_or(|ids| ids.contains(&t.id))).collect();
|
|
if only.is_some() && chosen.is_empty() {
|
|
return Err(EngineError::BadParams("file.export_midi".into(), "none of the requested tracks hold MIDI".into()));
|
|
}
|
|
for t in chosen {
|
|
let mut seq = soundcraft_midi::Sequence::default();
|
|
for c in t.clips() {
|
|
if let ClipContent::Midi { sequence } = &c.content {
|
|
let base = s.tempo.samples_to_ticks(c.start, s.sample_rate);
|
|
// Only what plays: notes starting inside the clip, cut at its end.
|
|
let clip_end = s.tempo.samples_to_ticks(c.end(), s.sample_rate);
|
|
for n in sequence.notes.iter().filter(|n| n.start.saturating_add(base) < clip_end) {
|
|
let mut n = *n;
|
|
n.start = n.start.saturating_add(base);
|
|
n.length = n.length.min(clip_end - n.start).max(1);
|
|
seq.notes.push(n);
|
|
}
|
|
let mut kept: Vec<soundcraft_midi::CtrlEvent> = Vec::new();
|
|
for ev in sequence.ctrls.iter().filter(|ev| ev.tick.saturating_add(base) < clip_end) {
|
|
let mut ev = *ev;
|
|
ev.tick = ev.tick.saturating_add(base);
|
|
kept.push(ev);
|
|
}
|
|
// A sustain still held at the clip end is released there.
|
|
let mut release: Vec<soundcraft_midi::CtrlEvent> = Vec::new();
|
|
for ev in &kept {
|
|
if ev.kind != soundcraft_midi::CtrlKind::Cc(64) {
|
|
continue;
|
|
}
|
|
match release.iter_mut().find(|r| r.channel == ev.channel) {
|
|
Some(r) if ev.tick >= r.tick => *r = *ev,
|
|
Some(_) => {}
|
|
None => release.push(*ev),
|
|
}
|
|
}
|
|
seq.ctrls.append(&mut kept);
|
|
for mut r in release.into_iter().filter(|r| r.value >= 64) {
|
|
r.tick = clip_end;
|
|
r.value = 0;
|
|
seq.ctrls.push(r);
|
|
}
|
|
}
|
|
}
|
|
seq.sort();
|
|
smf.tracks.push(soundcraft_midi::SmfTrack { name: t.name.clone(), sequence: seq });
|
|
}
|
|
soundcraft_midi::write_smf(&smf).map_err(|err| EngineError::Io(err.to_string()))
|
|
}
|