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}