1use std::sync::Arc;
7
8use unlit3dx::prelude::{MidiEvent, MidiEventKind, midi_file};
9use unlit3dx::synth::MidiFile;
10
11pub const MUSIC_BOX_PROGRAM: u8 = 10;
14
15pub const ELECTRIC_PIANO_PROGRAM: u8 = 4;
18
19pub const SCALE: [i32; 10] = [48, 50, 52, 55, 57, 60, 62, 64, 67, 69];
24
25fn note(events: &mut Vec<MidiEvent>, channel: u8, key: i32, start: f64, hold: f64, velocity: u8) {
28 events.push((
29 start,
30 MidiEventKind::NoteOn {
31 channel,
32 key: key as u8,
33 velocity,
34 },
35 ));
36 events.push((
37 start + hold,
38 MidiEventKind::NoteOff {
39 channel,
40 key: key as u8,
41 },
42 ));
43}
44
45pub fn sequence_midi() -> Arc<MidiFile> {
48 let mut events = vec![(
49 0.0,
50 MidiEventKind::ProgramChange {
51 channel: 0,
52 program: MUSIC_BOX_PROGRAM,
53 },
54 )];
55 for (index, &key) in SCALE.iter().chain(SCALE.iter().rev().skip(1)).enumerate() {
56 let start = index as f64 * 0.28;
57 note(&mut events, 0, key, start, 0.25, 96);
58 }
59 midi_file(events).expect("the demo sequence is well formed")
60}
61
62const BGM_CHORDS: [[i32; 3]; 8] = [
66 [48, 55, 60],
67 [45, 52, 57],
68 [41, 53, 57],
69 [43, 50, 59],
70 [48, 55, 60],
71 [45, 52, 57],
72 [41, 53, 60],
73 [43, 55, 62],
74];
75
76const BGM_MELODY: [Option<usize>; 16] = [
79 Some(5),
80 None,
81 Some(4),
82 Some(3),
83 Some(2),
84 None,
85 Some(3),
86 Some(4),
87 Some(5),
88 None,
89 Some(7),
90 Some(6),
91 Some(4),
92 None,
93 Some(2),
94 Some(3),
95];
96
97const BGM_BAR_SECONDS: f64 = 2.0;
99
100const BGM_ARPEGGIO: [usize; 8] = [0, 1, 2, 1, 0, 1, 2, 1];
103
104pub fn bgm_midi() -> Arc<MidiFile> {
109 let eighth = BGM_BAR_SECONDS / 8.0;
110 let mut events = vec![
113 (
114 0.0,
115 MidiEventKind::ProgramChange {
116 channel: 0,
117 program: ELECTRIC_PIANO_PROGRAM,
118 },
119 ),
120 (
121 0.0,
122 MidiEventKind::ProgramChange {
123 channel: 1,
124 program: ELECTRIC_PIANO_PROGRAM,
125 },
126 ),
127 (
128 0.0,
129 MidiEventKind::ProgramChange {
130 channel: 2,
131 program: MUSIC_BOX_PROGRAM,
132 },
133 ),
134 ];
135 for (bar, chord) in BGM_CHORDS.iter().enumerate() {
136 let start = bar as f64 * BGM_BAR_SECONDS;
137 note(&mut events, 0, chord[0] - 12, start, BGM_BAR_SECONDS, 56);
139 for (step, &finger) in BGM_ARPEGGIO.iter().enumerate() {
142 let key = chord[finger] + 12;
143 note(
144 &mut events,
145 1,
146 key,
147 start + step as f64 * eighth,
148 eighth,
149 44,
150 );
151 }
152 for half in 0..2 {
154 let Some(degree) = BGM_MELODY[bar * 2 + half] else {
155 continue;
156 };
157 note(
158 &mut events,
159 2,
160 SCALE[degree] + 12,
161 start + half as f64 * (BGM_BAR_SECONDS / 2.0),
162 BGM_BAR_SECONDS / 2.0 * 0.9,
163 72,
164 );
165 }
166 }
167 midi_file(events).expect("the background loop is well formed")
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use unlit3dx::synth::MidiMessage;
174
175 fn presses_and_releases(file: &MidiFile) -> (usize, usize) {
178 let mut presses = 0;
179 let mut releases = 0;
180 for message in file.get_messages() {
181 let MidiMessage::Normal { status, .. } = message else {
182 continue;
183 };
184 match status & 0xF0 {
185 0x90 => presses += 1,
186 0x80 => releases += 1,
187 _ => {}
188 }
189 }
190 (presses, releases)
191 }
192
193 #[test]
196 fn the_midi_tracks_release_every_note() {
197 for file in [bgm_midi(), sequence_midi()] {
198 let (presses, releases) = presses_and_releases(&file);
199 assert!(presses > 0, "a track with no notes is not music");
200 assert_eq!(
201 presses,
202 releases,
203 "the track leaves {} keys hanging",
204 presses - releases
205 );
206 let times = file.get_times();
207 for pair in times.windows(2) {
208 assert!(
209 pair[0] <= pair[1],
210 "the event times run backwards at {}",
211 pair[1]
212 );
213 }
214 }
215 }
216
217 #[test]
220 fn the_background_loop_spans_whole_bars() {
221 let file = bgm_midi();
222 let bars = file.get_length() / BGM_BAR_SECONDS;
223 assert!(
224 (bars - bars.round()).abs() < f64::EPSILON,
225 "the loop is {bars} bars long"
226 );
227 assert!(bars >= 4.0, "a four-bar loop is the shortest that breathes");
228 }
229}