Skip to main content

sim_lib_music_core/
model.rs

1use num_rational::Ratio;
2use std::any::Any;
3use thiserror::Error;
4
5pub use sim_lib_midi_core::{Channel, ChannelMessage, MidiEvent, MidiPayload, TickTime};
6pub use sim_lib_midi_smf::SmfFile;
7pub use sim_lib_pitch_core::{Pitch, PitchClass, PitchError, parse_pitch};
8
9use crate::{arranger::Arranger, piano_roll::PianoRoll};
10
11/// Exact musical time measured in whole notes as a rational number of beats.
12///
13/// Durations and onsets throughout the model are expressed in this type so that
14/// tuplets and subdivisions stay exact rather than accumulating float drift.
15pub type Time = Ratio<i64>;
16
17/// Error returned when a musical value violates a model invariant.
18#[derive(Debug, Error, Clone, PartialEq, Eq)]
19pub enum MusicError {
20    /// A duration was negative.
21    #[error("duration cannot be negative")]
22    NegativeDuration,
23    /// An onset position was negative.
24    #[error("onset cannot be negative")]
25    NegativeOnset,
26    /// A tempo was zero or otherwise non-positive.
27    #[error("tempo must be positive")]
28    InvalidTempo,
29    /// A time signature had a zero denominator.
30    #[error("time signature denominator must be non-zero")]
31    InvalidTimeSignature,
32    /// A melody contained overlapping voices instead of a single line.
33    #[error("melody items must be monophonic")]
34    NonMonophonicMelody,
35    /// A play range ended before it started.
36    #[error("play range end cannot precede start")]
37    InvalidTimeRange,
38    /// A play pulses-per-quarter resolution was zero.
39    #[error("play PPQ must be greater than zero")]
40    InvalidPpq,
41    /// A lane targeted something incompatible with its event kind.
42    #[error("lane {lane} target {target} is invalid for its event kind")]
43    InvalidLaneTarget {
44        /// Name of the offending lane.
45        lane: String,
46        /// The invalid target the lane referenced.
47        target: String,
48    },
49    /// A piano-roll grid had a non-positive ticks-per-quarter or step.
50    #[error("piano-roll time grid must have positive TPQ and step")]
51    InvalidPianoRollGrid,
52    /// A piano-roll lane held a cell kind it cannot accept.
53    #[error("piano-roll lane {lane} of kind {lane_kind} cannot contain {cell_kind} cells")]
54    PianoRollLaneCellMismatch {
55        /// Name of the offending lane.
56        lane: String,
57        /// Kind of the lane.
58        lane_kind: String,
59        /// Kind of the cell that did not fit the lane.
60        cell_kind: String,
61    },
62}
63
64/// A playable musical structure that can report its span and flatten to atoms.
65///
66/// Implementors are the concrete node types ([Note], [Rest], [Par], [Seq], and
67/// the larger forms) that compose a [Music] tree.
68pub trait MusicObject: Send + Sync + Any {
69    /// Returns a stable, human-readable tag for this object kind.
70    fn kind(&self) -> &'static str;
71    /// Returns the total duration this object occupies.
72    fn duration(&self) -> Time;
73    /// Appends the timed atoms produced by this object, shifted by `offset`.
74    fn voices<'a>(&'a self, offset: Time, out: &mut Vec<TimedAtom<'a>>);
75    /// Clones this object into a fresh boxed trait object.
76    fn clone_box(&self) -> Box<dyn MusicObject>;
77    /// Returns this object as a `&dyn Any` for downcasting.
78    fn as_any(&self) -> &dyn Any;
79}
80
81impl Clone for Box<dyn MusicObject> {
82    fn clone(&self) -> Self {
83        self.clone_box()
84    }
85}
86
87/// A single flattened atom together with its absolute onset time.
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct TimedAtom<'a> {
90    /// Absolute time at which the atom begins.
91    pub onset: Time,
92    /// The atom sounding (or resting) at this onset.
93    pub atom: AtomRef<'a>,
94}
95
96/// A leaf event produced when a [MusicObject] is flattened into voices.
97#[derive(Clone, Debug, PartialEq, Eq)]
98pub enum AtomRef<'a> {
99    /// A sounding note.
100    Note(Note),
101    /// A silent rest.
102    Rest(Rest),
103    /// Carries the borrow lifetime when no owned atom is present.
104    Phantom(std::marker::PhantomData<&'a ()>),
105}
106
107/// Performance articulation applied to a note.
108#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
109pub enum Articulation {
110    /// Default articulation with no modification.
111    Normal,
112    /// Shortened, detached note.
113    Staccato,
114    /// Smoothly connected to the following note.
115    Legato,
116    /// Held to its full notated length.
117    Tenuto,
118    /// Emphasized with extra attack.
119    Accent,
120    /// Strongly accented and detached.
121    Marcato,
122}
123
124/// A single pitched note with timing, dynamics, and articulation.
125#[derive(Clone, Debug, PartialEq, Eq)]
126pub struct Note {
127    /// Sounding length of the note.
128    pub duration: Time,
129    /// Pitch the note sounds.
130    pub pitch: Pitch,
131    /// MIDI velocity (0-127).
132    pub velocity: u8,
133    /// MIDI channel the note plays on.
134    pub channel: Channel,
135    /// Articulation applied to the note.
136    pub articulation: Articulation,
137}
138
139impl Note {
140    /// Builds a note, validating that `duration` is non-negative.
141    pub fn new(
142        duration: Time,
143        pitch: Pitch,
144        velocity: u8,
145        channel: Channel,
146        articulation: Articulation,
147    ) -> Result<Self, MusicError> {
148        ensure_non_negative(duration)?;
149        Ok(Self {
150            duration,
151            pitch,
152            velocity,
153            channel,
154            articulation,
155        })
156    }
157}
158
159/// A span of silence.
160#[derive(Clone, Debug, PartialEq, Eq)]
161pub struct Rest {
162    /// Length of the silence.
163    pub duration: Time,
164}
165
166impl Rest {
167    /// Builds a rest, validating that `duration` is non-negative.
168    ///
169    /// # Examples
170    ///
171    /// ```
172    /// use sim_lib_music_core::{Rest, Time};
173    ///
174    /// let rest = Rest::new(Time::from_integer(1)).unwrap();
175    /// assert_eq!(rest.duration, Time::from_integer(1));
176    /// ```
177    pub fn new(duration: Time) -> Result<Self, MusicError> {
178        ensure_non_negative(duration)?;
179        Ok(Self { duration })
180    }
181}
182
183/// Parallel composition: children that sound simultaneously.
184#[derive(Clone)]
185pub struct Par {
186    /// The objects played at the same time.
187    pub children: Vec<Box<dyn MusicObject>>,
188}
189
190impl std::fmt::Debug for Par {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        f.debug_struct("Par")
193            .field("children_len", &self.children.len())
194            .finish()
195    }
196}
197
198/// Sequential composition: children that play one after another.
199#[derive(Clone)]
200pub struct Seq {
201    /// The objects played back to back in order.
202    pub children: Vec<Box<dyn MusicObject>>,
203}
204
205impl std::fmt::Debug for Seq {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        f.debug_struct("Seq")
208            .field("children_len", &self.children.len())
209            .finish()
210    }
211}
212
213/// A set of pitches sounded together, optionally tagged with a chord symbol.
214#[derive(Clone, Debug, PartialEq, Eq)]
215pub struct Chord {
216    /// Sounding length of the chord.
217    pub duration: Time,
218    /// Chord symbol label (for example `"Cmaj7"`).
219    pub symbol: String,
220    /// Pitches that make up the chord.
221    pub pitches: Vec<Pitch>,
222    /// MIDI velocity (0-127) applied to every pitch.
223    pub velocity: u8,
224    /// MIDI channel the chord plays on.
225    pub channel: Channel,
226}
227
228impl Chord {
229    /// Builds a chord, validating that `duration` is non-negative.
230    pub fn new(
231        duration: Time,
232        symbol: impl Into<String>,
233        pitches: Vec<Pitch>,
234        velocity: u8,
235        channel: Channel,
236    ) -> Result<Self, MusicError> {
237        ensure_non_negative(duration)?;
238        Ok(Self {
239            duration,
240            symbol: symbol.into(),
241            pitches,
242            velocity,
243            channel,
244        })
245    }
246}
247
248/// One element of a monophonic [Melody]: either a note or a rest.
249#[derive(Clone, Debug, PartialEq, Eq)]
250pub enum MelodyItem {
251    /// A sounding note.
252    Note(Note),
253    /// A silent rest.
254    Rest(Rest),
255}
256
257impl MelodyItem {
258    /// Returns the duration of this item, whether note or rest.
259    pub fn duration(&self) -> Time {
260        match self {
261            Self::Note(note) => note.duration,
262            Self::Rest(rest) => rest.duration,
263        }
264    }
265}
266
267/// A single monophonic line of notes and rests in sequence.
268#[derive(Clone, Debug, PartialEq, Eq)]
269pub struct Melody {
270    /// The ordered notes and rests of the line.
271    pub items: Vec<MelodyItem>,
272}
273
274impl Melody {
275    /// Builds a melody, validating that every item duration is non-negative.
276    pub fn new(items: Vec<MelodyItem>) -> Result<Self, MusicError> {
277        for item in &items {
278            ensure_non_negative(item.duration())?;
279        }
280        Ok(Self { items })
281    }
282
283    /// Returns the summed duration of all items in the line.
284    ///
285    /// # Examples
286    ///
287    /// ```
288    /// use sim_lib_music_core::{Melody, MelodyItem, Rest, Time};
289    ///
290    /// let melody = Melody::new(vec![
291    ///     MelodyItem::Rest(Rest::new(Time::from_integer(1)).unwrap()),
292    ///     MelodyItem::Rest(Rest::new(Time::from_integer(2)).unwrap()),
293    /// ])
294    /// .unwrap();
295    /// assert_eq!(melody.total_duration(), Time::from_integer(3));
296    /// ```
297    pub fn total_duration(&self) -> Time {
298        self.items
299            .iter()
300            .fold(Time::from_integer(0), |sum, item| sum + item.duration())
301    }
302}
303
304/// An ordered sequence of chords, optionally in a named key.
305#[derive(Clone, Debug, PartialEq, Eq)]
306pub struct Progression {
307    /// Key the progression is heard in, if specified.
308    pub key: Option<String>,
309    /// The chords in playing order.
310    pub chords: Vec<Chord>,
311}
312
313impl Progression {
314    /// Builds a progression, validating that every chord duration is non-negative.
315    pub fn new(key: Option<String>, chords: Vec<Chord>) -> Result<Self, MusicError> {
316        for chord in &chords {
317            ensure_non_negative(chord.duration)?;
318        }
319        Ok(Self { key, chords })
320    }
321}
322
323/// Several independent melodic lines sounding together with labels.
324#[derive(Clone, Debug, PartialEq, Eq)]
325pub struct Counterpoint {
326    /// The independent melodic voices.
327    pub voices: Vec<Melody>,
328    /// Display names for each voice, parallel to `voices`.
329    pub voice_names: Vec<String>,
330}
331
332impl Counterpoint {
333    /// Builds counterpoint, supplying default voice names when the counts mismatch.
334    pub fn new(voices: Vec<Melody>, voice_names: Vec<String>) -> Result<Self, MusicError> {
335        let voice_names = normalize_voice_names(voices.len(), voice_names);
336        Ok(Self {
337            voices,
338            voice_names,
339        })
340    }
341
342    /// Returns voice names, filling in defaults when the stored list is wrong-length.
343    pub fn normalized_voice_names(&self) -> Vec<String> {
344        normalize_voice_names(self.voices.len(), self.voice_names.clone())
345    }
346}
347
348/// A raw MIDI track wrapped as a music object.
349#[derive(Clone, Debug, PartialEq, Eq)]
350pub struct MidiTrackObj {
351    /// The MIDI events of the track.
352    pub events: Vec<MidiEvent>,
353    /// Preferred channel for events that do not carry one.
354    pub channel_hint: Option<Channel>,
355}
356
357impl MidiTrackObj {
358    /// Wraps a list of MIDI events with an optional channel hint.
359    pub fn new(events: Vec<MidiEvent>, channel_hint: Option<Channel>) -> Self {
360        Self {
361            events,
362            channel_hint,
363        }
364    }
365}
366
367/// A complete Standard MIDI File wrapped as a music object.
368#[derive(Clone, Debug, PartialEq, Eq)]
369pub struct MidiFileObj {
370    /// The parsed SMF contents.
371    pub file: SmfFile,
372}
373
374impl MidiFileObj {
375    /// Wraps a parsed Standard MIDI File.
376    pub fn new(file: SmfFile) -> Self {
377        Self { file }
378    }
379}
380
381/// A top-level score: global settings plus a musical body.
382#[derive(Clone, Debug)]
383pub struct Score {
384    /// Tempo in beats per minute.
385    pub tempo_bpm: u32,
386    /// Time signature as a (numerator, denominator) pair.
387    pub time_signature: (u8, u8),
388    /// Key the score is in, if specified.
389    pub key: Option<String>,
390    /// The musical content of the score.
391    pub body: Music,
392}
393
394impl Score {
395    /// Builds a score, validating positive tempo and a non-zero time signature.
396    pub fn new(
397        tempo_bpm: u32,
398        time_signature: (u8, u8),
399        key: Option<String>,
400        body: Music,
401    ) -> Result<Self, MusicError> {
402        if tempo_bpm == 0 {
403            return Err(MusicError::InvalidTempo);
404        }
405        if time_signature.1 == 0 {
406            return Err(MusicError::InvalidTimeSignature);
407        }
408        Ok(Self {
409            tempo_bpm,
410            time_signature,
411            key,
412            body,
413        })
414    }
415}
416
417/// The unified musical value: any node that can appear in a [Score] body.
418#[derive(Clone)]
419pub enum Music {
420    /// A single note.
421    Note(Note),
422    /// A rest.
423    Rest(Rest),
424    /// Parallel composition of children.
425    Par(Par),
426    /// Sequential composition of children.
427    Seq(Seq),
428    /// A chord.
429    Chord(Chord),
430    /// A monophonic melody line.
431    Melody(Melody),
432    /// A chord progression.
433    Progression(Progression),
434    /// Multiple voices in counterpoint.
435    Counterpoint(Counterpoint),
436    /// A piano-roll grid.
437    PianoRoll(PianoRoll),
438    /// An arranger timeline.
439    Arranger(Arranger),
440    /// A raw MIDI track.
441    MidiTrack(MidiTrackObj),
442    /// A complete MIDI file.
443    MidiFile(MidiFileObj),
444}
445
446impl std::fmt::Debug for Music {
447    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448        match self {
449            Self::Note(_) => f.write_str("Music::Note(..)"),
450            Self::Rest(_) => f.write_str("Music::Rest(..)"),
451            Self::Par(par) => f.debug_tuple("Music::Par").field(par).finish(),
452            Self::Seq(seq) => f.debug_tuple("Music::Seq").field(seq).finish(),
453            Self::Chord(chord) => f.debug_tuple("Music::Chord").field(chord).finish(),
454            Self::Melody(melody) => f.debug_tuple("Music::Melody").field(melody).finish(),
455            Self::Progression(progression) => f
456                .debug_tuple("Music::Progression")
457                .field(progression)
458                .finish(),
459            Self::Counterpoint(counterpoint) => f
460                .debug_tuple("Music::Counterpoint")
461                .field(counterpoint)
462                .finish(),
463            Self::PianoRoll(roll) => f.debug_tuple("Music::PianoRoll").field(roll).finish(),
464            Self::Arranger(arranger) => f.debug_tuple("Music::Arranger").field(arranger).finish(),
465            Self::MidiTrack(track) => f.debug_tuple("Music::MidiTrack").field(track).finish(),
466            Self::MidiFile(file) => f.debug_tuple("Music::MidiFile").field(file).finish(),
467        }
468    }
469}
470
471pub(crate) fn ensure_non_negative(value: Time) -> Result<(), MusicError> {
472    if value < Time::from_integer(0) {
473        Err(MusicError::NegativeDuration)
474    } else {
475        Ok(())
476    }
477}
478
479fn normalize_voice_names(count: usize, voice_names: Vec<String>) -> Vec<String> {
480    if voice_names.len() == count {
481        voice_names
482    } else {
483        (0..count)
484            .map(|index| format!("Voice {}", index + 1))
485            .collect()
486    }
487}