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}