Skip to main content

sound_to_patch/notes/
write.rs

1//! Writing a note list as a Standard MIDI File.
2//!
3//! The file is format 0: one track holds the tempo, a 4/4 time signature
4//! and every note on channel 1. A DAW opens it as one part at the stem's
5//! tempo. Bar lines assume 4/4, because the input gives only a BPM.
6
7use std::cmp::Reverse;
8use std::error::Error;
9use std::fmt;
10
11use midly::num::{u4, u7, u14, u15, u24, u28};
12use midly::{
13    Format, Header, MetaMessage, MidiMessage, PitchBend, Timing, TrackEvent, TrackEventKind,
14};
15
16use super::{NoteList, Velocity};
17use crate::cast;
18use crate::units::{Bpm, Seconds};
19
20/// The resolution of the file in ticks per quarter note.
21pub const TICKS_PER_BEAT: u16 = 480;
22
23/// The velocity that the `notes` command passes to
24/// [`Velocities::Constant`].
25pub const DEFAULT_VELOCITY: Velocity = Velocity::from_const(100);
26
27/// The shortest note the file holds. A shorter note is lengthened to it,
28/// and to at least one tick.
29pub const MIN_NOTE_LENGTH: Seconds = Seconds::from_const(0.010);
30
31/// Which velocity each note gets.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum Velocities {
34    /// Every note gets this one value. The transcriber's velocity is a
35    /// confidence, not a loudness, so the audio path writes a constant.
36    Constant(Velocity),
37    /// A note gets its own velocity, and a note without one gets this
38    /// value.
39    FromNotes(Velocity),
40}
41
42/// Encodes the notes as a format 0 file at a BPM.
43///
44/// Times become ticks with `round(seconds * BPM / 60 * 480)`, so each lies
45/// within half a tick of the note. The tempo event at tick 0 holds
46/// `round(60,000,000 / BPM)` microseconds per beat. A fractional pitch
47/// rounds to the nearest key. Three rules any player needs come first: a
48/// note shorter than [`MIN_NOTE_LENGTH`] or one tick is lengthened, of two
49/// notes with one key and one start only the longer stays, and a note that
50/// still sounds when its key starts again ends there. At one tick a
51/// note-off comes before a note-on, so two touching notes of one pitch
52/// stay two. The file has no program change and no pitch bend.
53///
54/// # Errors
55///
56/// Returns a [`MidiWriteError`] for an empty list, which no reader of ours
57/// accepts, and when two events lie further apart than a MIDI delta time
58/// holds, about nine hours at 1000 BPM.
59pub fn write_midi(
60    notes: &NoteList,
61    bpm: Bpm,
62    velocities: Velocities,
63) -> Result<Vec<u8>, MidiWriteError> {
64    encode(notes, bpm, velocities, None)
65}
66
67/// Encodes the notes as [`write_midi`] does, with a track name event at
68/// tick 0 before the tempo. `notes` names the stem this way, so a later
69/// run can tell a file of another stem; [`midi_track_name`] reads it back.
70///
71/// # Errors
72///
73/// The errors of [`write_midi`].
74pub fn write_named_midi(
75    notes: &NoteList,
76    bpm: Bpm,
77    velocities: Velocities,
78    name: &str,
79) -> Result<Vec<u8>, MidiWriteError> {
80    encode(notes, bpm, velocities, Some(name))
81}
82
83/// Returns the first track name of a Standard MIDI File, or `None` when the
84/// bytes do not parse or the file names no track.
85#[must_use]
86pub fn midi_track_name(bytes: &[u8]) -> Option<String> {
87    let smf = midly::Smf::parse(bytes).ok()?;
88    smf.tracks
89        .iter()
90        .flatten()
91        .find_map(|event| match event.kind {
92            TrackEventKind::Meta(MetaMessage::TrackName(name)) => {
93                Some(String::from_utf8_lossy(name).into_owned())
94            }
95            _ => None,
96        })
97}
98
99fn encode(
100    notes: &NoteList,
101    bpm: Bpm,
102    velocities: Velocities,
103    name: Option<&str>,
104) -> Result<Vec<u8>, MidiWriteError> {
105    let ticks_per_second = ticks_per_second(bpm);
106    let tick = |seconds: Seconds| {
107        cast::round_to_usize(seconds.get() * ticks_per_second)
108            .and_then(|tick| u64::try_from(tick).ok())
109            .ok_or(MidiWriteError::TooLong(seconds))
110    };
111    let mut spans = Vec::with_capacity(notes.notes().len());
112    for note in notes.notes() {
113        let start = tick(note.onset())?;
114        let floor = note.onset().get() + MIN_NOTE_LENGTH.get();
115        let end = tick(Seconds::clamped(note.offset().get().max(floor)))?.max(start + 1);
116        let key = cast::round_to_usize(note.pitch().get())
117            .and_then(|key| u8::try_from(key).ok())
118            .and_then(u7::try_from)
119            // Unreachable: a `MidiPitch` lies in 0 to 127, so its key fits.
120            .unwrap_or(u7::max_value());
121        let velocity = match velocities {
122            Velocities::Constant(velocity) => velocity,
123            Velocities::FromNotes(default) => note.velocity().unwrap_or(default),
124        };
125        spans.push((key, start, end, velocity));
126    }
127    encode_spans(spans, &[], bpm, name)
128}
129
130/// A note span in ticks: key, start, end and velocity.
131pub(crate) type Span = (u7, u64, u64, Velocity);
132
133/// Encodes note spans and pitch-bend events, each a tick and a 14-bit
134/// value, as a format 0 file. Of two notes with one key and one start only
135/// the longer stays, and a note ends no later than the next one of its key.
136/// At one tick a note-off comes first, then a bend, then a note-on.
137pub(crate) fn encode_spans(
138    mut spans: Vec<Span>,
139    bends: &[(u64, u16)],
140    bpm: Bpm,
141    name: Option<&str>,
142) -> Result<Vec<u8>, MidiWriteError> {
143    if spans.is_empty() {
144        return Err(MidiWriteError::NoNotes);
145    }
146    let ticks_per_second = ticks_per_second(bpm);
147    // Sorted by key, then start, longest first: the first of one key and
148    // start stays, and a note ends no later than the next one of its key.
149    spans.sort_by_key(|&(key, start, end, _)| (key, start, Reverse(end)));
150    spans.dedup_by_key(|&mut (key, start, ..)| (key, start));
151    let mut events = Vec::with_capacity(2 * spans.len() + bends.len());
152    for (i, &(key, start, end, velocity)) in spans.iter().enumerate() {
153        let end = match spans.get(i + 1) {
154            Some(&(next_key, next_start, ..)) if next_key == key => end.min(next_start),
155            _ => end,
156        };
157        events.push((start, Event::On, key, velocity.get()));
158        events.push((end, Event::Off, key, 0));
159    }
160    for &(at, value) in bends {
161        events.push((at, Event::Bend(value), u7::new(0), 0));
162    }
163    events.sort_by_key(|&(at, event, key, _)| (at, event, key));
164
165    let channel = u4::new(0);
166    let mut track: Vec<TrackEvent<'_>> = name
167        .map(|name| meta(MetaMessage::TrackName(name.as_bytes())))
168        .into_iter()
169        .collect();
170    track.extend([
171        meta(MetaMessage::Tempo(u24::new(tempo_us(bpm)))),
172        meta(MetaMessage::TimeSignature(4, 2, 24, 8)),
173    ]);
174    let mut now = 0_u64;
175    for (at, event, key, velocity) in events {
176        let message = match event {
177            Event::On => MidiMessage::NoteOn {
178                key,
179                vel: u7::new(velocity),
180            },
181            Event::Off => MidiMessage::NoteOff {
182                key,
183                vel: u7::new(0),
184            },
185            Event::Bend(value) => MidiMessage::PitchBend {
186                bend: PitchBend(u14::new(value)),
187            },
188        };
189        track.push(TrackEvent {
190            delta: u32::try_from(at - now).ok().and_then(u28::try_from).ok_or(
191                MidiWriteError::TooLong(Seconds::clamped(cast::u64_to_f64(at) / ticks_per_second)),
192            )?,
193            kind: TrackEventKind::Midi { channel, message },
194        });
195        now = at;
196    }
197    track.push(meta(MetaMessage::EndOfTrack));
198
199    let header = Header::new(
200        Format::SingleTrack,
201        Timing::Metrical(u15::new(TICKS_PER_BEAT)),
202    );
203    let mut bytes = Vec::new();
204    midly::write_std(&header, std::iter::once(&track), &mut bytes)
205        .map_err(|source| MidiWriteError::Encode(source.to_string()))?;
206    Ok(bytes)
207}
208
209/// The kinds of event, in the order they take at one tick.
210#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
211enum Event {
212    Off,
213    Bend(u16),
214    On,
215}
216
217/// The ticks in one second at a BPM.
218pub(crate) fn ticks_per_second(bpm: Bpm) -> f64 {
219    bpm.get() / 60.0 * f64::from(TICKS_PER_BEAT)
220}
221
222/// The tempo in whole microseconds per quarter note.
223fn tempo_us(bpm: Bpm) -> u32 {
224    // 4 to 1000 BPM gives 60,000 to 15,000,000, which fits 24 bits.
225    cast::round_to_usize(60e6 / bpm.get())
226        .and_then(|us| u32::try_from(us).ok())
227        .unwrap_or(u32::MAX)
228}
229
230fn meta(message: MetaMessage<'_>) -> TrackEvent<'_> {
231    TrackEvent {
232        delta: u28::new(0),
233        kind: TrackEventKind::Meta(message),
234    }
235}
236
237/// A note list that cannot become a MIDI file.
238#[derive(Clone, Debug, PartialEq)]
239pub enum MidiWriteError {
240    /// The list holds no note.
241    NoNotes,
242    /// A note lies too far from the previous event for a MIDI delta time.
243    TooLong(Seconds),
244    /// The encoder failed.
245    Encode(String),
246}
247
248impl fmt::Display for MidiWriteError {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        match self {
251            Self::NoNotes => write!(f, "cannot write a MIDI file without notes"),
252            Self::TooLong(at) => write!(
253                f,
254                "cannot write a note at {at}: it lies further from the previous note than a MIDI file can hold. Split the stem"
255            ),
256            Self::Encode(message) => write!(f, "cannot encode the MIDI file: {message}"),
257        }
258    }
259}
260
261impl Error for MidiWriteError {}