rust_music/
score.rs

1use std::collections::BTreeMap;
2
3use crate::errors::ScoreError;
4use crate::errors::ToMidiConversionError;
5use crate::num::*;
6use crate::Instrument;
7use crate::Part;
8use crate::PhraseEntry;
9use crate::Result;
10
11use crate::midly::{
12    Format, Header, MetaMessage, MidiMessage, Smf, Timing, TrackEvent, TrackEventKind,
13};
14
15/// Describes the scale mode (Major or Minor, other modes are not specified)
16#[derive(Debug, Default, Clone, PartialEq, Eq)]
17pub enum Mode {
18    #[default]
19    Major = 0,
20    Minor = 1,
21}
22
23/// Contains information about the score that aren't needed for MIDI play
24/// such as time signature, key signature (number of accidentals), and mode
25#[derive(Debug, Default, Clone, PartialEq, Eq)]
26pub struct Metadata {
27    /// Describes the number of accidentals of the `Score`.
28    /// Should always be between -7 and 7. Negative numbers are
29    /// Flats, positive numbers are Sharps
30    pub key_signature: i8,
31    /// Describes the mode of the scale
32    pub mode: Mode,
33    /// Describes the numerator in the time signature
34    pub time_numerator: u8,
35    /// Describes the denominator in the time signature
36    pub time_denominator: u8,
37}
38
39/// Describes the tempo of a score in beats per minute
40#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
41pub struct Tempo(u32);
42
43impl Tempo {
44    /// Returns a new tempo if non null, otherwise, returns an error
45    ///
46    /// # Errors
47    ///
48    /// Returns `ScoreError::InvalidTempo` if tempo is 0
49    pub fn new(tempo: u32) -> Result<Self> {
50        if tempo == 0 {
51            return Err(ScoreError::InvalidTempo.into());
52        }
53        Ok(Self(tempo))
54    }
55}
56
57/// Describes a full `Score`
58#[derive(Debug, Default, Clone, PartialEq)]
59pub struct Score {
60    /// Title of the `Score`
61    name: String,
62    /// List of `Part`s in the `Score`
63    parts: Vec<Part>,
64    /// Tempo (beats per minute) at which the `Score` should be played
65    tempo: u32,
66    /// Optional information about the `Score`
67    metadata: Option<Metadata>,
68    /// Duration in beats of the `Score`
69    duration: f64,
70}
71
72impl Score {
73    /// Returns a new empty `Score` from the given arguments
74    ///
75    /// # Arguments
76    ///
77    /// * `name` - Title of the `Score`
78    /// * `tempo` - Tempo of the `Score`
79    /// * `Metadata` - Optional information
80    pub fn new<S: ToString>(name: S, tempo: Tempo, metadata: Option<Metadata>) -> Score {
81        Score {
82            name: name.to_string(),
83            parts: Vec::new(),
84            tempo: tempo.0,
85            metadata,
86            duration: 0.,
87        }
88    }
89
90    /// Adds a `Part` to the `Score`.
91    /// Warning: q Score can contain unlimited Parts but if exporting to
92    /// Standard MIDI File, any Score with more than 16 Parts will fail
93    /// because MIDI only supports 16 channels.
94    pub fn add_part(&mut self, part: Part) {
95        self.duration = self.duration.max(part.duration());
96        self.parts.push(part);
97    }
98
99    /// Modifies the tempo of the `Score`
100    ///
101    /// # Errors
102    ///
103    /// Returns `ScoreError::InvalidTempo` if tempo is 0
104    pub fn set_tempo(&mut self, tempo: u32) -> Result<()> {
105        if tempo == 0 {
106            return Err(ScoreError::InvalidTempo.into());
107        }
108        self.tempo = tempo;
109        Ok(())
110    }
111
112    pub fn write_midi_file<W: std::io::Write>(&self, w: W) -> Result<()> {
113        let smf: Smf = self.try_into()?;
114        Ok(smf.write_std(w)?)
115    }
116
117    /// Returns the title of the `Score`
118    pub fn name(&self) -> &str {
119        self.name.as_str()
120    }
121
122    /// Returns the `Part`s of the `Score`
123    pub fn parts(&self) -> &[Part] {
124        self.parts.as_slice()
125    }
126
127    /// Returns the tempo of the `Score`
128    pub fn tempo(&self) -> u32 {
129        self.tempo
130    }
131
132    /// Returns the metadata
133    pub fn metadata(&self) -> Option<&Metadata> {
134        self.metadata.as_ref()
135    }
136
137    // Returns the total duration (in beats, i.e. the "rhythm" unit) of the `Score`.
138    // This corresponds to the end of the `Part` that finishes the latest.
139    pub fn duration(&self) -> f64 {
140        self.duration
141    }
142}
143
144impl<'a> TryFrom<&'a Score> for Smf<'a> {
145    type Error = crate::Error;
146
147    /// Converts a Score into a Standard MIDI File (midly::Smf)
148    ///
149    /// # Arguments
150    ///
151    /// * `score` - Score to convert
152    ///
153    /// # Errors
154    ///
155    /// Returns `ToMidiConversionError`
156    /// TODO: complete errors description
157    fn try_from(score: &'a Score) -> Result<Smf<'a>> {
158        if score.parts.len() > 16 {
159            return Err(ToMidiConversionError::TooManyParts(score.parts.len()).into());
160        }
161
162        let header = Header {
163            format: if score.parts().len() == 1 {
164                Format::SingleTrack
165            } else {
166                Format::Parallel
167            },
168            timing: Timing::Metrical(u15::from(480)),
169        };
170        let mut metadata_events = vec![
171            TrackEvent {
172                delta: u28::default(),
173                kind: TrackEventKind::Meta(MetaMessage::TrackName(score.name.as_bytes())),
174            },
175            TrackEvent {
176                delta: u28::default(),
177                kind: TrackEventKind::Meta(MetaMessage::Tempo(u24::from(60_000_000 / score.tempo))),
178            },
179        ];
180        if let Some(mdata) = score.metadata() {
181            metadata_events.push(TrackEvent {
182                delta: u28::default(),
183                kind: TrackEventKind::Meta(MetaMessage::TimeSignature(
184                    mdata.time_numerator,
185                    mdata.time_denominator,
186                    24u8,
187                    32u8,
188                )),
189            });
190            metadata_events.push(TrackEvent {
191                delta: u28::default(),
192                kind: TrackEventKind::Meta(MetaMessage::KeySignature(
193                    mdata.key_signature,
194                    matches!(mdata.mode, Mode::Minor),
195                )),
196            });
197            // TODO: Handle more metadata (copyright, text fields, etc.)
198        }
199
200        let mut tracks = Vec::new();
201
202        for (channel, part) in score.parts().iter().enumerate() {
203            let mut notes_per_time: BTreeMap<u64, (Vec<TrackEvent>, Vec<TrackEvent>)> =
204                BTreeMap::new();
205            for phrase in part.phrases() {
206                let mut cur_time = (phrase.0 * 480.).round() as u64;
207                for phrase_entry in phrase.1.entries() {
208                    match phrase_entry {
209                        PhraseEntry::Chord(c) => {
210                            notes_per_time.entry(cur_time).or_default().0.extend(
211                                c.notes().iter().map(|n| TrackEvent {
212                                    delta: u28::default(),
213                                    kind: TrackEventKind::Midi {
214                                        channel: u4::new(channel as u8),
215                                        message: MidiMessage::NoteOn {
216                                            key: n.pitch(),
217                                            vel: n.dynamic(),
218                                        },
219                                    },
220                                }),
221                            );
222                            for n in c.notes() {
223                                notes_per_time
224                                    .entry(cur_time + (n.rhythm() * 480.).round() as u64)
225                                    .or_default()
226                                    .1
227                                    .push(TrackEvent {
228                                        delta: u28::default(),
229                                        kind: TrackEventKind::Midi {
230                                            channel: u4::new(channel as u8),
231                                            message: MidiMessage::NoteOff {
232                                                key: n.pitch(),
233                                                vel: u7::default(),
234                                            },
235                                        },
236                                    })
237                            }
238                            cur_time += (c.rhythm() * 480.).round() as u64;
239                        }
240                        PhraseEntry::Note(n) => {
241                            notes_per_time
242                                .entry(cur_time)
243                                .or_default()
244                                .0
245                                .push(TrackEvent {
246                                    delta: u28::default(),
247                                    kind: TrackEventKind::Midi {
248                                        channel: u4::new(channel as u8),
249                                        message: MidiMessage::NoteOn {
250                                            key: n.pitch(),
251                                            vel: n.dynamic(),
252                                        },
253                                    },
254                                });
255                            notes_per_time
256                                .entry(cur_time + (n.rhythm() * 480.).round() as u64)
257                                .or_default()
258                                .1
259                                .push(TrackEvent {
260                                    delta: u28::default(),
261                                    kind: TrackEventKind::Midi {
262                                        channel: u4::new(channel as u8),
263                                        message: MidiMessage::NoteOff {
264                                            key: n.pitch(),
265                                            vel: u7::default(),
266                                        },
267                                    },
268                                });
269                            cur_time += (n.rhythm() * 480.).round() as u64;
270                        }
271                        PhraseEntry::Rest(r) => {
272                            cur_time += (r * 480.).round() as u64;
273                        }
274                    };
275                }
276            }
277            // TODO: investigate if the usage of `round` on the time value (in ticks) can cause issues
278            if notes_per_time.is_empty() {
279                continue;
280            }
281
282            let mut track = metadata_events.clone();
283            let part_instrument = part.instrument();
284            if !matches!(part_instrument, Instrument::None) {
285                track.push(TrackEvent {
286                    delta: u28::default(),
287                    kind: TrackEventKind::Midi {
288                        channel: u4::new(channel as u8),
289                        message: MidiMessage::ProgramChange {
290                            program: u7::new(part_instrument as u8),
291                        },
292                    },
293                });
294            }
295
296            let mut previous_time = 0;
297            for (current_time, track_events) in notes_per_time {
298                let mut delta = current_time - previous_time;
299                // do NoteOffs first
300                for mut te in track_events.1 {
301                    // TODO: raise error if > maxu28 (use `std::num::Wrapping`?)
302                    te.delta = u28::new(delta as u32);
303                    delta = 0; // the first event at this time has the whole delta but the others have 0
304                    track.push(te);
305                }
306                // then NoteOns
307                for mut te in track_events.0 {
308                    // TODO: raise error if > maxu28
309                    te.delta = u28::new(delta as u32);
310                    delta = 0;
311                    track.push(te);
312                }
313                previous_time = current_time;
314            }
315
316            track.push(TrackEvent {
317                delta: u28::default(),
318                kind: TrackEventKind::Meta(MetaMessage::EndOfTrack),
319            });
320            tracks.push(track);
321        }
322
323        Ok(Smf { header, tracks })
324    }
325}