Skip to main content

sim_lib_midi_core/
model.rs

1use std::convert::TryFrom;
2use std::ops::{Add, Sub};
3
4use sim_kernel::{CodecId, Origin, SourceId, Span};
5
6use crate::MidiError;
7
8/// A point in musical time measured in ticks at a given resolution.
9///
10/// `ticks` counts the offset and `tpq` is the ticks-per-quarter-note
11/// resolution, so the same instant can be represented at different
12/// resolutions. Helpers convert between resolutions via [`rebase`](Self::rebase)
13/// (exact) and [`quantize`](Self::quantize) (rounding).
14#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
15pub struct TickTime {
16    /// Tick offset, interpreted at this value's [`tpq`](Self::tpq) resolution.
17    pub ticks: i64,
18    /// Resolution in ticks per quarter note; must be non-zero.
19    pub tpq: u32,
20}
21
22impl TickTime {
23    /// The origin: zero ticks at unit resolution.
24    pub const ZERO: Self = Self { ticks: 0, tpq: 1 };
25
26    /// Creates a time at `tpq` resolution, failing with [`MidiError::ZeroTpq`]
27    /// when `tpq` is zero.
28    pub fn new(ticks: i64, tpq: u32) -> Result<Self, MidiError> {
29        if tpq == 0 {
30            Err(MidiError::ZeroTpq)
31        } else {
32            Ok(Self { ticks, tpq })
33        }
34    }
35
36    /// Builds a time of whole quarter notes at unit resolution (`tpq == 1`).
37    pub fn from_quarters(quarters: i64) -> Self {
38        Self {
39            ticks: quarters,
40            tpq: 1,
41        }
42    }
43
44    /// Scales the tick count by an integer factor, keeping the resolution.
45    pub fn mul_int(self, factor: i64) -> Self {
46        Self {
47            ticks: self.ticks * factor,
48            tpq: self.tpq,
49        }
50    }
51
52    /// Scales the tick count by `numerator / denominator`, failing with
53    /// [`MidiError::InvalidRatio`] when `denominator` is zero.
54    pub fn mul_ratio(self, numerator: i64, denominator: i64) -> Result<Self, MidiError> {
55        if denominator == 0 {
56            return Err(MidiError::InvalidRatio(numerator, denominator));
57        }
58        Ok(Self {
59            ticks: self.ticks * numerator / denominator,
60            tpq: self.tpq,
61        })
62    }
63
64    /// Scales the tick count by `denominator / numerator`, failing with
65    /// [`MidiError::InvalidRatio`] when `numerator` is zero.
66    pub fn div_ratio(self, numerator: i64, denominator: i64) -> Result<Self, MidiError> {
67        if numerator == 0 {
68            return Err(MidiError::InvalidRatio(numerator, denominator));
69        }
70        Ok(Self {
71            ticks: self.ticks * denominator / numerator,
72            tpq: self.tpq,
73        })
74    }
75
76    /// Re-expresses this time at `new_tpq` resolution without rounding.
77    ///
78    /// Fails with [`MidiError::ZeroTpq`] when `new_tpq` is zero, or
79    /// [`MidiError::InexactRebase`] when the conversion would not be exact.
80    pub fn rebase(self, new_tpq: u32) -> Result<Self, MidiError> {
81        if new_tpq == 0 {
82            return Err(MidiError::ZeroTpq);
83        }
84        let scaled = self.ticks * i64::from(new_tpq);
85        if scaled % i64::from(self.tpq) != 0 {
86            return Err(MidiError::InexactRebase);
87        }
88        Ok(Self {
89            ticks: scaled / i64::from(self.tpq),
90            tpq: new_tpq,
91        })
92    }
93
94    /// Re-expresses this time at `new_tpq` resolution, rounding to the nearest
95    /// tick. A zero `new_tpq` is clamped to unit resolution.
96    pub fn quantize(self, new_tpq: u32) -> Self {
97        let scaled = self.ticks as f64 * f64::from(new_tpq) / f64::from(self.tpq);
98        Self {
99            ticks: scaled.round() as i64,
100            tpq: new_tpq.max(1),
101        }
102    }
103
104    /// Returns the time as a `(ticks, tpq)` rational in quarter notes.
105    pub fn as_rational(self) -> (i64, i64) {
106        (self.ticks, i64::from(self.tpq))
107    }
108
109    /// Returns the time as a floating-point count of quarter notes.
110    pub fn as_f64_quarters(self) -> f64 {
111        self.ticks as f64 / self.tpq as f64
112    }
113}
114
115impl Add for TickTime {
116    type Output = Self;
117
118    fn add(self, other: Self) -> Self::Output {
119        let other = other.quantize(self.tpq);
120        Self {
121            ticks: self.ticks + other.ticks,
122            tpq: self.tpq,
123        }
124    }
125}
126
127impl Sub for TickTime {
128    type Output = Self;
129
130    fn sub(self, other: Self) -> Self::Output {
131        let other = other.quantize(self.tpq);
132        Self {
133            ticks: self.ticks - other.ticks,
134            tpq: self.tpq,
135        }
136    }
137}
138
139/// A 7-bit MIDI data value in the range `0..=127`.
140#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
141pub struct U7(pub u8);
142
143impl TryFrom<u16> for U7 {
144    type Error = MidiError;
145
146    fn try_from(value: u16) -> Result<Self, Self::Error> {
147        if value <= 127 {
148            Ok(Self(value as u8))
149        } else {
150            Err(MidiError::InvalidU7(value))
151        }
152    }
153}
154
155/// A 14-bit MIDI data value in the range `0..=16_383`, as used by pitch bend.
156#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
157pub struct U14(pub u16);
158
159impl TryFrom<u16> for U14 {
160    type Error = MidiError;
161
162    fn try_from(value: u16) -> Result<Self, Self::Error> {
163        if value <= 16_383 {
164            Ok(Self(value))
165        } else {
166            Err(MidiError::InvalidU14(value))
167        }
168    }
169}
170
171/// A MIDI channel number in the range `0..=15`.
172#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
173pub struct Channel(pub u8);
174
175impl Channel {
176    /// Creates a channel, failing with [`MidiError::InvalidChannel`] when
177    /// `value` exceeds 15.
178    pub fn new(value: u8) -> Result<Self, MidiError> {
179        if value <= 15 {
180            Ok(Self(value))
181        } else {
182            Err(MidiError::InvalidChannel(value))
183        }
184    }
185}
186
187/// A MIDI channel-voice message, carrying its target [`Channel`] and payload.
188#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
189pub enum ChannelMessage {
190    /// Note-off: release `key` on `ch` with release velocity `vel`.
191    NoteOff {
192        /// Target channel.
193        ch: Channel,
194        /// Note number.
195        key: U7,
196        /// Release velocity.
197        vel: U7,
198    },
199    /// Note-on: start `key` on `ch` with velocity `vel` (velocity 0 is a
200    /// conventional note-off).
201    NoteOn {
202        /// Target channel.
203        ch: Channel,
204        /// Note number.
205        key: U7,
206        /// Attack velocity.
207        vel: U7,
208    },
209    /// Polyphonic key pressure (aftertouch) for a single note.
210    PolyAftertouch {
211        /// Target channel.
212        ch: Channel,
213        /// Note number.
214        key: U7,
215        /// Key pressure.
216        pressure: U7,
217    },
218    /// Control-change message setting controller `cc` to `value`.
219    ControlChange {
220        /// Target channel.
221        ch: Channel,
222        /// Controller number (see the `CC_*` constants).
223        cc: U7,
224        /// Controller value.
225        value: U7,
226    },
227    /// Program (patch) change.
228    ProgramChange {
229        /// Target channel.
230        ch: Channel,
231        /// Program number.
232        program: U7,
233    },
234    /// Channel pressure (aftertouch) applied to the whole channel.
235    ChanAftertouch {
236        /// Target channel.
237        ch: Channel,
238        /// Channel pressure.
239        pressure: U7,
240    },
241    /// Pitch-bend wheel position as a 14-bit value (`8192` is centred).
242    PitchBend {
243        /// Target channel.
244        ch: Channel,
245        /// Bend amount.
246        value: U14,
247    },
248}
249
250/// A meta event: track-scoped information that is not transmitted over the
251/// wire, recognised types plus an [`Other`](Self::Other) escape hatch.
252#[derive(Clone, Debug, PartialEq, Eq, Hash)]
253pub enum MetaEvent {
254    /// End-of-track marker.
255    EndOfTrack,
256    /// Tempo change expressed in microseconds per quarter note.
257    Tempo {
258        /// Microseconds per quarter note.
259        us_per_quarter: u32,
260    },
261    /// Time signature.
262    TimeSig {
263        /// Numerator (beats per bar).
264        num: u8,
265        /// Denominator as a power of two (e.g. `2` for a quarter-note beat).
266        den_pow2: u8,
267        /// MIDI clocks per metronome click.
268        clocks_per_click: u8,
269        /// Notated 32nd notes per quarter note.
270        thirty_seconds_per_quarter: u8,
271    },
272    /// Key signature.
273    KeySig {
274        /// Number of sharps (positive) or flats (negative).
275        sharps_flats: i8,
276        /// Whether the key is minor.
277        minor: bool,
278    },
279    /// Any other meta type, carried as a raw [`MetaBucket`].
280    Other(MetaBucket),
281}
282
283/// A raw meta event: its type byte plus payload bytes.
284///
285/// The [`meta_view`](crate::meta_view) helpers interpret common buckets such as
286/// text, track name, marker, and SMPTE offset.
287#[derive(Clone, Debug, PartialEq, Eq, Hash)]
288pub struct MetaBucket {
289    /// MIDI meta type byte.
290    pub type_byte: u8,
291    /// Raw payload bytes.
292    pub data: Vec<u8>,
293}
294
295/// An SMPTE timecode offset, as carried by the SMPTE-offset meta event.
296#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
297pub struct SmpteOffset {
298    /// Hours field.
299    pub hours: u8,
300    /// Minutes field.
301    pub minutes: u8,
302    /// Seconds field.
303    pub seconds: u8,
304    /// Frames field.
305    pub frames: u8,
306    /// Fractional-frame (subframe) field.
307    pub subframes: u8,
308}
309
310/// A system-exclusive event, distinguished by its leading status byte.
311#[derive(Clone, Debug, PartialEq, Eq, Hash)]
312pub enum SysExEvent {
313    /// A complete or first-packet message introduced by `0xF0`.
314    F0 {
315        /// Message body bytes (excluding the leading status byte).
316        data: Vec<u8>,
317    },
318    /// A continuation or escape packet introduced by `0xF7`.
319    F7 {
320        /// Message body bytes (excluding the leading status byte).
321        data: Vec<u8>,
322    },
323}
324
325/// An uninterpreted run of bytes with a status byte, used as a fallback.
326#[derive(Clone, Debug, PartialEq, Eq, Hash)]
327pub struct RawBytes {
328    /// Status byte.
329    pub status: u8,
330    /// Data bytes following the status byte.
331    pub data: Vec<u8>,
332}
333
334/// The payload of a [`MidiEvent`]: one of the four event families.
335#[derive(Clone, Debug, PartialEq, Eq, Hash)]
336pub enum MidiPayload {
337    /// A channel-voice message.
338    Channel(ChannelMessage),
339    /// A meta event.
340    Meta(MetaEvent),
341    /// A system-exclusive event.
342    SysEx(SysExEvent),
343    /// Uninterpreted raw bytes.
344    Raw(RawBytes),
345}
346
347/// A timestamped MIDI event with provenance.
348#[derive(Clone, Debug, PartialEq, Eq, Hash)]
349pub struct MidiEvent {
350    /// Event time.
351    pub time: TickTime,
352    /// Source provenance carried from the originating codec.
353    pub origin: Origin,
354    /// Event payload.
355    pub payload: MidiPayload,
356}
357
358/// Returns an [`Origin`] tagging events synthesised by the music stack rather
359/// than read from an input codec.
360pub fn synthetic_origin() -> Origin {
361    Origin {
362        codec: CodecId(0),
363        source: SourceId("music-stack".to_owned()),
364        span: Span { start: 0, end: 0 },
365        trivia: Vec::new(),
366    }
367}