Skip to main content

sim_lib_stream_file/
midi.rs

1use std::path::Path;
2
3use sim_kernel::{Error, Result};
4use sim_lib_midi_core::MemoryMidiSource;
5use sim_lib_midi_smf::{SmfDivision, SmfFile, SmfFormat, SmfTrack, read_smf, write_smf};
6use sim_lib_stream_core::{StreamMetadata, StreamValue};
7use sim_lib_stream_midi::{midi_source_to_stream, midi_stream_to_sink};
8
9use crate::effect_io::{read_file_with_effect, write_file_with_effect};
10
11/// Reads a Standard MIDI File from `path` and opens it as a MIDI stream.
12///
13/// The read is gated by the filesystem read capability and recorded as a
14/// KERNEL 6 filesystem effect. Events are packetized at most `max_events` per
15/// packet.
16pub fn read_smf_stream(
17    cx: &mut sim_kernel::Cx,
18    path: impl AsRef<Path>,
19    max_events: usize,
20    metadata: StreamMetadata,
21) -> Result<StreamValue> {
22    let bytes = read_file_with_effect(cx, path)?;
23    smf_bytes_to_stream(&bytes, max_events, metadata)
24}
25
26/// Parses Standard MIDI File bytes and opens them as a MIDI stream.
27pub fn smf_bytes_to_stream(
28    bytes: &[u8],
29    max_events: usize,
30    metadata: StreamMetadata,
31) -> Result<StreamValue> {
32    let file = read_smf(bytes).map_err(|err| Error::Eval(format!("malformed SMF file: {err}")))?;
33    smf_file_to_stream(&file, max_events, metadata)
34}
35
36/// Opens an already-parsed Standard MIDI File as a MIDI stream.
37///
38/// Merges every track into a single ordered timeline and packetizes it at most
39/// `max_events` events per packet.
40pub fn smf_file_to_stream(
41    file: &SmfFile,
42    max_events: usize,
43    metadata: StreamMetadata,
44) -> Result<StreamValue> {
45    let events = file
46        .merged_events()
47        .map_err(|err| Error::Eval(format!("cannot stream SMF file: {err}")))?
48        .into_iter()
49        .map(|tracked| tracked.event)
50        .collect();
51    let tpq = file.ticks_per_quarter().ok_or_else(|| {
52        Error::Eval("SMPTE SMF requires an explicit real-time stream clock adapter".to_owned())
53    })?;
54    let mut source = MemoryMidiSource::new(tpq, events);
55    midi_source_to_stream(&mut source, max_events, metadata)
56}
57
58/// Drains a MIDI stream and writes it to `path` as a Standard MIDI File.
59///
60/// The write is gated by the filesystem write capability and recorded as a
61/// KERNEL 6 filesystem effect. Returns the number of events written.
62pub fn write_smf_stream(
63    cx: &mut sim_kernel::Cx,
64    path: impl AsRef<Path>,
65    stream: &StreamValue,
66    tpq: u32,
67) -> Result<usize> {
68    let (file, count) = stream_to_smf_file(stream, tpq)?;
69    let bytes = write_smf(&file).map_err(|err| Error::Eval(format!("cannot write SMF: {err}")))?;
70    write_file_with_effect(cx, path, bytes)?;
71    Ok(count)
72}
73
74/// Drains a MIDI stream and encodes it as Standard MIDI File bytes.
75///
76/// Returns the encoded bytes together with the number of events written.
77pub fn stream_to_smf_bytes(stream: &StreamValue, tpq: u32) -> Result<(Vec<u8>, usize)> {
78    let (file, count) = stream_to_smf_file(stream, tpq)?;
79    let bytes = write_smf(&file).map_err(|err| Error::Eval(format!("cannot write SMF: {err}")))?;
80    Ok((bytes, count))
81}
82
83/// Drains a MIDI stream into a single-track [`SmfFile`] with `tpq` resolution.
84///
85/// Returns the assembled file and the number of events written. Errors when
86/// `tpq` exceeds the Standard MIDI File range.
87pub fn stream_to_smf_file(stream: &StreamValue, tpq: u32) -> Result<(SmfFile, usize)> {
88    let division = u16::try_from(tpq)
89        .ok()
90        .and_then(SmfDivision::metrical)
91        .ok_or_else(|| {
92            Error::Eval(format!(
93                "SMF TPQ {tpq} exceeds the metrical Standard MIDI File range"
94            ))
95        })?;
96    let mut sink = sim_lib_midi_core::MemoryMidiSink::new(tpq);
97    let count = midi_stream_to_sink(stream, &mut sink)?;
98    let file = SmfFile {
99        format: SmfFormat::SingleTrack,
100        division,
101        tracks: vec![SmfTrack {
102            events: sink.events().to_vec(),
103        }],
104    };
105    Ok((file, count))
106}