1use serde::{Deserialize, Serialize};
2use super::duration::Duration;
3use super::notation::Articulation;
4use super::repeat::measure_sequence;
5use super::score::Score;
6
7fn default_fermata_multiplier() -> f64 { 1.5 }
8fn default_swing_unit() -> Duration { Duration::Eighth }
9fn default_metronome_channel() -> u8 { 9 }
10fn default_accent_pitch() -> u8 { 76 }
11fn default_beat_pitch() -> u8 { 77 }
12fn default_accent_velocity() -> u8 { 100 }
13fn default_beat_velocity() -> u8 { 70 }
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct MetronomeConfig {
21 #[serde(default = "default_metronome_channel")]
23 pub channel: u8,
24 #[serde(default = "default_accent_pitch")]
26 pub accent_pitch: u8,
27 #[serde(default = "default_beat_pitch")]
29 pub beat_pitch: u8,
30 #[serde(default = "default_accent_velocity")]
32 pub accent_velocity: u8,
33 #[serde(default = "default_beat_velocity")]
35 pub beat_velocity: u8,
36}
37
38impl Default for MetronomeConfig {
39 fn default() -> Self {
40 Self { channel: 9, accent_pitch: 76, beat_pitch: 77, accent_velocity: 100, beat_velocity: 70 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct PlaybackOptions {
47 pub bpm_override: Option<u16>,
49 pub muted_parts: Vec<usize>,
51 #[serde(default)]
55 pub loop_region: Option<(usize, usize)>,
56 #[serde(default = "default_fermata_multiplier")]
58 pub fermata_multiplier: f64,
59 #[serde(default)]
63 pub swing: Option<f64>,
64 #[serde(default = "default_swing_unit")]
67 pub swing_unit: Duration,
68 #[serde(default)]
71 pub metronome: Option<MetronomeConfig>,
72}
73
74impl Default for PlaybackOptions {
75 fn default() -> Self {
76 Self {
77 bpm_override: None,
78 muted_parts: Vec::new(),
79 loop_region: None,
80 fermata_multiplier: 1.5,
81 swing: None,
82 swing_unit: Duration::Eighth,
83 metronome: None,
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct PlaybackEvent {
93 pub time_beats: f64,
95 pub time_secs: f64,
97 pub pitch_midi: u8,
99 pub velocity: u8,
102 pub duration_beats: f64,
104 pub duration_secs: f64,
106 pub pedal: bool,
108 pub part_index: usize,
110 pub channel: u8,
112 #[serde(default)]
114 pub is_metronome: bool,
115}
116
117pub fn to_playback_events(score: &Score, options: &PlaybackOptions) -> Vec<PlaybackEvent> {
123 let bpm = options.bpm_override.unwrap_or(score.settings.tempo_bpm).max(1) as f64;
124 let full_seq = measure_sequence(score);
125 let seq: Vec<usize> = if let Some((lo, hi)) = options.loop_region {
126 full_seq.into_iter().filter(|&idx| idx >= lo && idx <= hi).collect()
127 } else {
128 full_seq
129 };
130 let mut events: Vec<PlaybackEvent> = Vec::new();
131
132 for (part_index, part) in score.parts.iter().enumerate() {
133 if options.muted_parts.contains(&part_index) {
134 continue;
135 }
136 for staff in &part.staves {
137 for voice_idx in 0..4usize {
138 let mut time_beats = 0.0f64;
139 let mut time_secs_cursor = 0.0f64;
140 let mut current_bpm = bpm;
141 for &idx in &seq {
142 let measure = match staff.measures.get(idx) {
143 Some(m) => m,
144 None => continue,
145 };
146 if let Some(b) = measure.tempo {
147 current_bpm = b.max(1) as f64;
148 }
149 let mut swing_first = true;
150 for note in &measure.voices[voice_idx] {
151 if note.is_grace {
152 continue;
153 }
154 let dur = match options.swing {
155 Some(ratio)
156 if note.tuplet.is_none()
157 && note.dot_count == 0
158 && note.duration == options.swing_unit =>
159 {
160 let pair = note.beats() * 2.0;
161 let d = if swing_first { ratio * pair } else { (1.0 - ratio) * pair };
162 swing_first = !swing_first;
163 d
164 }
165 Some(_) => { swing_first = true; note.beats() }
166 None => note.beats(),
167 };
168 if !note.is_rest {
169 let mut velocity = note.dynamic
170 .as_ref()
171 .map(|d| d.to_velocity())
172 .unwrap_or(64u8);
173 let mut sounding_dur = dur;
174 for art in ¬e.articulations {
175 match art {
176 Articulation::Staccato | Articulation::Staccatissimo => {
177 sounding_dur *= 0.5;
178 }
179 Articulation::Accent | Articulation::Marcato => {
180 velocity = velocity.saturating_add(20).min(127);
181 }
182 Articulation::Fermata => {
183 sounding_dur *= options.fermata_multiplier;
184 }
185 _ => {}
186 }
187 }
188 let pedal = note.pedal_start;
189 let transpose = if part.midi_channel == 9 { 0i8 } else { staff.transpose_semitones };
190 for pitch in ¬e.pitches {
191 let midi = (pitch.to_midi() + transpose as i16)
192 .clamp(0, 127) as u8;
193 events.push(PlaybackEvent {
194 time_beats,
195 time_secs: time_secs_cursor,
196 pitch_midi: midi,
197 velocity,
198 duration_beats: sounding_dur,
199 duration_secs: sounding_dur / current_bpm * 60.0,
200 pedal,
201 part_index,
202 channel: part.midi_channel,
203 is_metronome: false,
204 });
205 }
206 }
207 time_beats += dur;
208 time_secs_cursor += dur / current_bpm * 60.0;
209 }
210 }
211 }
212 }
213 }
214
215 if let Some(ref metro) = options.metronome {
216 let mut cursor_secs = 0.0f64;
217 let mut cursor_beats = 0.0f64;
218 let mut metro_bpm = bpm;
219 for &idx in &seq {
220 let first_staff = score.parts.first()
221 .and_then(|p| p.staves.first());
222 if let Some(t) = first_staff.and_then(|s| s.measures.get(idx)).and_then(|m| m.tempo) {
223 metro_bpm = t.max(1) as f64;
224 }
225 let ts = first_staff
226 .and_then(|s| s.measures.get(idx))
227 .and_then(|m| m.time_sig.as_ref())
228 .unwrap_or(&score.settings.time_signature);
229 let beat_unit = ts.beat_unit_beats();
230 let num_beats = (ts.total_beats() / beat_unit).round() as u32;
231 for b in 0..num_beats {
232 let is_accent = b == 0;
233 let beat_offset_secs = b as f64 * beat_unit / metro_bpm * 60.0;
234 events.push(PlaybackEvent {
235 time_beats: cursor_beats + b as f64 * beat_unit,
236 time_secs: cursor_secs + beat_offset_secs,
237 pitch_midi: if is_accent { metro.accent_pitch } else { metro.beat_pitch },
238 velocity: if is_accent { metro.accent_velocity } else { metro.beat_velocity },
239 duration_beats: beat_unit * 0.1,
240 duration_secs: beat_unit * 0.1 / metro_bpm * 60.0,
241 pedal: false,
242 part_index: usize::MAX,
243 channel: metro.channel,
244 is_metronome: true,
245 });
246 }
247 let measure_beats = ts.total_beats();
248 cursor_secs += measure_beats / metro_bpm * 60.0;
249 cursor_beats += measure_beats;
250 }
251 }
252
253 events.sort_by(|a, b| {
254 a.time_beats.partial_cmp(&b.time_beats).unwrap_or(std::cmp::Ordering::Equal)
255 });
256 events
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct PlaybackPosition {
264 pub measure_index: usize,
266 pub beat: f64,
268}
269
270struct MeasureSegment {
271 measure_idx: usize,
272 start_secs: f64,
273 duration_secs: f64,
274 beats: f64,
275 bpm: f64,
276}
277
278fn build_measure_segments(score: &Score, options: &PlaybackOptions) -> Vec<MeasureSegment> {
279 let init_bpm = options.bpm_override.unwrap_or(score.settings.tempo_bpm).max(1) as f64;
280 let full_seq = measure_sequence(score);
281 let seq: Vec<usize> = if let Some((lo, hi)) = options.loop_region {
282 full_seq.into_iter().filter(|&i| i >= lo && i <= hi).collect()
283 } else {
284 full_seq
285 };
286
287 let mut segments = Vec::with_capacity(seq.len());
288 let mut cursor_secs = 0.0f64;
289 let mut current_bpm = init_bpm;
290
291 for idx in seq {
292 let first_measure = score.parts.first()
293 .and_then(|p| p.staves.first())
294 .and_then(|s| s.measures.get(idx));
295 if let Some(t) = first_measure.and_then(|m| m.tempo) {
296 current_bpm = t.max(1) as f64;
297 }
298 let ts = first_measure
299 .and_then(|m| m.time_sig.as_ref())
300 .unwrap_or(&score.settings.time_signature);
301 let beats = ts.total_beats();
302 let duration_secs = beats / current_bpm * 60.0;
303
304 segments.push(MeasureSegment {
305 measure_idx: idx,
306 start_secs: cursor_secs,
307 duration_secs,
308 beats,
309 bpm: current_bpm,
310 });
311 cursor_secs += duration_secs;
312 }
313 segments
314}
315
316pub fn compute_playback_position(
322 score: &Score,
323 options: &PlaybackOptions,
324 elapsed_secs: f64,
325) -> Option<PlaybackPosition> {
326 if elapsed_secs < 0.0 { return None; }
327 let segments = build_measure_segments(score, options);
328 for seg in &segments {
329 if elapsed_secs < seg.start_secs + seg.duration_secs + 1e-9 {
330 let beat = ((elapsed_secs - seg.start_secs) * seg.bpm / 60.0)
331 .clamp(0.0, seg.beats);
332 return Some(PlaybackPosition { measure_index: seg.measure_idx, beat });
333 }
334 }
335 None
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use crate::model::{
342 pitch::{Pitch, Step},
343 duration::Duration,
344 score::{Note, Score},
345 };
346
347 fn opts(bpm: Option<u16>) -> PlaybackOptions {
348 PlaybackOptions { bpm_override: bpm, ..Default::default() }
349 }
350
351 #[test]
352 fn empty_score_no_events() {
353 let score = Score::new("T", 120, 4, 4, 0, 1);
354 assert!(to_playback_events(&score, &opts(None)).is_empty());
355 }
356
357 #[test]
358 fn single_note_at_beat_zero() {
359 let mut score = Score::new("T", 120, 4, 4, 0, 1);
360 score.parts[0].staves[0].measures[0].voices[0] =
361 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
362 let events = to_playback_events(&score, &opts(None));
363 assert_eq!(events.len(), 1);
364 assert!((events[0].time_beats).abs() < 1e-9);
365 assert_eq!(events[0].pitch_midi, 60);
366 assert_eq!(events[0].velocity, 64);
367 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
368 assert_eq!(events[0].part_index, 0);
369 }
370
371 #[test]
372 fn chord_expands_to_multiple_events() {
373 let mut score = Score::new("T", 120, 4, 4, 0, 1);
374 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
375 note.pitches.push(Pitch::new(Step::E, 4));
376 note.pitches.push(Pitch::new(Step::G, 4));
377 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
378 let events = to_playback_events(&score, &opts(None));
379 assert_eq!(events.len(), 3);
380 assert!(events.iter().all(|e| e.time_beats.abs() < 1e-9));
381 }
382
383 #[test]
384 fn grace_notes_excluded() {
385 let mut score = Score::new("T", 120, 4, 4, 0, 1);
386 let mut grace = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
387 grace.is_grace = true;
388 let regular = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
389 score.parts[0].staves[0].measures[0].voices[0] = vec![grace, regular];
390 let events = to_playback_events(&score, &opts(None));
391 assert_eq!(events.len(), 1);
392 assert_eq!(events[0].pitch_midi, 60);
393 }
394
395 #[test]
396 fn second_note_has_correct_time() {
397 let mut score = Score::new("T", 120, 4, 4, 0, 1);
398 score.parts[0].staves[0].measures[0].voices[0] = vec![
399 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
400 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
401 ];
402 let events = to_playback_events(&score, &opts(None));
403 assert_eq!(events.len(), 2);
404 assert!((events[0].time_beats).abs() < 1e-9);
405 assert!((events[1].time_beats - 1.0).abs() < 1e-9);
406 }
407
408 #[test]
409 fn time_secs_120_bpm_quarter_note_is_half_second() {
410 let mut score = Score::new("T", 120, 4, 4, 0, 1);
411 score.parts[0].staves[0].measures[0].voices[0] =
412 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
413 let events = to_playback_events(&score, &opts(None));
414 assert!((events[0].time_secs).abs() < 1e-9);
415 assert!((events[0].duration_secs - 0.5).abs() < 1e-9);
416 }
417
418 #[test]
419 fn bpm_override_changes_time_secs() {
420 let mut score = Score::new("T", 120, 4, 4, 0, 1);
421 score.parts[0].staves[0].measures[0].voices[0] = vec![
422 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
423 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
424 ];
425 let events = to_playback_events(&score, &opts(Some(60)));
426 assert!((events[0].time_secs).abs() < 1e-9);
427 assert!((events[1].time_secs - 1.0).abs() < 1e-9);
428 }
429
430 #[test]
431 fn transpose_semitones_shifts_midi_output() {
432 let mut score = Score::new("T", 120, 4, 4, 0, 1);
433 score.parts[0].staves[0].transpose_semitones = -2;
434 score.parts[0].staves[0].measures[0].voices[0] =
435 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
436 let events = to_playback_events(&score, &opts(None));
437 assert_eq!(events.len(), 1);
438 assert_eq!(events[0].pitch_midi, 58);
439 }
440
441 #[test]
442 fn percussion_channel_9_ignores_transpose_semitones() {
443 let mut score = Score::new("T", 120, 4, 4, 0, 1);
444 score.parts[0].midi_channel = 9;
445 score.parts[0].staves[0].transpose_semitones = -2;
446 score.parts[0].staves[0].measures[0].voices[0] =
447 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
448 let events = to_playback_events(&score, &opts(None));
449 assert_eq!(events.len(), 1);
450 assert_eq!(events[0].pitch_midi, 60);
451 }
452
453 #[test]
454 fn staccato_halves_duration_beats() {
455 let mut score = Score::new("T", 120, 4, 4, 0, 1);
456 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
457 note.articulations.push(crate::model::notation::Articulation::Staccato);
458 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
459 let events = to_playback_events(&score, &opts(None));
460 assert_eq!(events.len(), 1);
461 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
462 }
463
464 #[test]
465 fn staccatissimo_also_halves_duration() {
466 let mut score = Score::new("T", 120, 4, 4, 0, 1);
467 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
468 note.articulations.push(crate::model::notation::Articulation::Staccatissimo);
469 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
470 let events = to_playback_events(&score, &opts(None));
471 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
472 }
473
474 #[test]
475 fn staccato_does_not_shift_next_note_time() {
476 let mut score = Score::new("T", 120, 4, 4, 0, 1);
477 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
478 n1.articulations.push(crate::model::notation::Articulation::Staccato);
479 let n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
480 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
481 let events = to_playback_events(&score, &opts(None));
482 assert_eq!(events.len(), 2);
483 assert!((events[1].time_beats - 1.0).abs() < 1e-9);
484 }
485
486 #[test]
487 fn accent_boosts_velocity_clamped() {
488 let mut score = Score::new("T", 120, 4, 4, 0, 1);
489 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
490 note.articulations.push(crate::model::notation::Articulation::Accent);
491 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
492 let events = to_playback_events(&score, &opts(None));
493 assert_eq!(events[0].velocity, 84);
494 }
495
496 #[test]
497 fn accent_clamped_at_127() {
498 let mut score = Score::new("T", 120, 4, 4, 0, 1);
499 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
500 note.dynamic = Some(crate::model::notation::Dynamic::Ffff);
501 note.articulations.push(crate::model::notation::Articulation::Accent);
502 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
503 let events = to_playback_events(&score, &opts(None));
504 assert_eq!(events[0].velocity, 127);
505 }
506
507 #[test]
508 fn tenuto_keeps_full_duration() {
509 let mut score = Score::new("T", 120, 4, 4, 0, 1);
510 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
511 note.articulations.push(crate::model::notation::Articulation::Tenuto);
512 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
513 let events = to_playback_events(&score, &opts(None));
514 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
515 }
516
517 #[test]
518 fn pedal_start_sets_pedal_field() {
519 let mut score = Score::new("T", 120, 4, 4, 0, 1);
520 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
521 note.pedal_start = true;
522 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
523 let events = to_playback_events(&score, &opts(None));
524 assert!(events[0].pedal);
525 }
526
527 #[test]
528 fn no_pedal_start_pedal_is_false() {
529 let mut score = Score::new("T", 120, 4, 4, 0, 1);
530 score.parts[0].staves[0].measures[0].voices[0] =
531 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
532 let events = to_playback_events(&score, &opts(None));
533 assert!(!events[0].pedal);
534 }
535
536 #[test]
537 fn set_tempo_at_measure_changes_time_secs() {
538 let mut score = Score::new("T", 120, 4, 4, 0, 2);
539 score.parts[0].staves[0].measures[0].voices[0] =
540 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
541 score.parts[0].staves[0].measures[1].tempo = Some(60);
542 score.parts[0].staves[0].measures[1].voices[0] =
543 vec![Note::new(Pitch::new(Step::D, 4), Duration::Whole)];
544 let events = to_playback_events(&score, &opts(None));
545 assert_eq!(events.len(), 2);
546 assert!((events[0].time_secs).abs() < 1e-9);
547 assert!((events[0].duration_secs - 2.0).abs() < 1e-9);
548 assert!((events[1].time_secs - 2.0).abs() < 1e-9);
549 assert!((events[1].duration_secs - 4.0).abs() < 1e-9);
550 }
551
552 #[test]
553 fn muted_part_produces_no_events() {
554 let mut score = Score::new("T", 120, 4, 4, 0, 1);
555 score.parts[0].staves[0].measures[0].voices[0] =
556 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
557 let options = PlaybackOptions { muted_parts: vec![0], ..Default::default() };
558 assert!(to_playback_events(&score, &options).is_empty());
559 }
560
561 #[test]
562 fn part_index_field_set_correctly() {
563 let mut score = Score::new("T", 120, 4, 4, 0, 1);
564 score.parts[0].staves[0].measures[0].voices[0] =
565 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
566 let events = to_playback_events(&score, &opts(None));
567 assert_eq!(events[0].part_index, 0);
568 }
569
570 #[test]
573 fn loop_region_filters_measures() {
574 let mut score = Score::new("T", 120, 4, 4, 0, 3);
576 for mi in 0..3 {
577 score.parts[0].staves[0].measures[mi].voices[0] =
578 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
579 }
580 let options = PlaybackOptions { loop_region: Some((1, 2)), ..Default::default() };
581 let events = to_playback_events(&score, &options);
582 assert_eq!(events.len(), 2);
583 assert!((events[0].time_beats).abs() < 1e-9);
585 }
586
587 #[test]
588 fn loop_region_none_plays_all_measures() {
589 let mut score = Score::new("T", 120, 4, 4, 0, 3);
590 for mi in 0..3 {
591 score.parts[0].staves[0].measures[mi].voices[0] =
592 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
593 }
594 let events = to_playback_events(&score, &opts(None));
595 assert_eq!(events.len(), 3);
596 }
597
598 #[test]
601 fn fermata_multiplier_extends_duration() {
602 let mut score = Score::new("T", 120, 4, 4, 0, 1);
603 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
604 note.articulations.push(crate::model::notation::Articulation::Fermata);
605 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
606 let options = PlaybackOptions { fermata_multiplier: 2.0, ..Default::default() };
607 let events = to_playback_events(&score, &options);
608 assert_eq!(events.len(), 1);
609 assert!((events[0].duration_beats - 2.0).abs() < 1e-9);
610 }
611
612 #[test]
613 fn fermata_default_multiplier_is_1_5() {
614 let mut score = Score::new("T", 120, 4, 4, 0, 1);
615 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
616 note.articulations.push(crate::model::notation::Articulation::Fermata);
617 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
618 let events = to_playback_events(&score, &PlaybackOptions::default());
619 assert_eq!(events.len(), 1);
620 assert!((events[0].duration_beats - 1.5).abs() < 1e-9);
621 }
622
623 #[test]
624 fn non_fermata_note_unaffected_by_fermata_multiplier() {
625 let mut score = Score::new("T", 120, 4, 4, 0, 1);
626 score.parts[0].staves[0].measures[0].voices[0] =
627 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
628 let options = PlaybackOptions { fermata_multiplier: 3.0, ..Default::default() };
629 let events = to_playback_events(&score, &options);
630 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
631 }
632
633 #[test]
636 fn swing_triplet_first_eighth_is_long() {
637 let mut score = Score::new("T", 120, 4, 4, 0, 1);
639 score.parts[0].staves[0].measures[0].voices[0] = vec![
640 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
641 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
642 ];
643 let options = PlaybackOptions { swing: Some(0.67), ..Default::default() };
644 let events = to_playback_events(&score, &options);
645 let e_c = events.iter().find(|e| e.pitch_midi == 60).unwrap();
647 let e_d = events.iter().find(|e| e.pitch_midi == 62).unwrap();
648 assert!((e_c.duration_beats - 0.67).abs() < 1e-9, "first eighth should be 0.67");
649 assert!((e_d.duration_beats - 0.33).abs() < 1e-9, "second eighth should be 0.33");
650 assert!((e_d.time_beats - 0.67).abs() < 1e-9, "second note start should be at 0.67");
652 }
653
654 #[test]
655 fn swing_non_eighth_not_affected() {
656 let mut score = Score::new("T", 120, 4, 4, 0, 1);
657 score.parts[0].staves[0].measures[0].voices[0] =
658 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
659 let options = PlaybackOptions { swing: Some(0.67), ..Default::default() };
660 let events = to_playback_events(&score, &options);
661 assert!((events[0].duration_beats - 1.0).abs() < 1e-9, "quarter note unaffected");
662 }
663
664 #[test]
665 fn swing_none_is_straight() {
666 let mut score = Score::new("T", 120, 4, 4, 0, 1);
667 score.parts[0].staves[0].measures[0].voices[0] =
668 vec![Note::new(Pitch::new(Step::C, 4), Duration::Eighth)];
669 let options = PlaybackOptions { swing: None, ..Default::default() };
670 let events = to_playback_events(&score, &options);
671 assert!((events[0].duration_beats - 0.5).abs() < 1e-9, "no swing = straight eighth");
672 }
673
674 #[test]
675 fn channel_matches_part_midi_channel() {
676 let mut score = Score::new("T", 120, 4, 4, 0, 1);
677 score.parts[0].midi_channel = 3;
678 score.parts[0].staves[0].measures[0].voices[0] =
679 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
680 let events = to_playback_events(&score, &opts(None));
681 assert_eq!(events[0].channel, 3);
682 }
683
684 #[test]
685 fn swing_unit_default_is_eighth() {
686 assert_eq!(PlaybackOptions::default().swing_unit, Duration::Eighth);
687 }
688
689 #[test]
690 fn swing_unit_sixteenth() {
691 let mut score = Score::new("T", 120, 4, 4, 0, 1);
692 score.parts[0].staves[0].measures[0].voices[0] = vec![
693 Note::new(Pitch::new(Step::C, 4), Duration::Sixteenth),
694 Note::new(Pitch::new(Step::D, 4), Duration::Sixteenth),
695 ];
696 let options = PlaybackOptions {
697 swing: Some(0.67),
698 swing_unit: Duration::Sixteenth,
699 ..Default::default()
700 };
701 let events = to_playback_events(&score, &options);
702 let e_c = events.iter().find(|e| e.pitch_midi == 60).unwrap();
703 let e_d = events.iter().find(|e| e.pitch_midi == 62).unwrap();
704 assert!((e_c.duration_beats - 0.335).abs() < 1e-9, "first 16th should be 0.335");
705 assert!((e_d.duration_beats - 0.165).abs() < 1e-9, "second 16th should be 0.165");
706 }
707
708 #[test]
709 fn swing_resets_per_measure() {
710 let mut score = Score::new("T", 120, 4, 4, 0, 2);
712 let pair = || vec![
713 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
714 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
715 ];
716 score.parts[0].staves[0].measures[0].voices[0] = pair();
717 score.parts[0].staves[0].measures[1].voices[0] = pair();
718 let options = PlaybackOptions { swing: Some(0.67), ..Default::default() };
719 let events = to_playback_events(&score, &options);
720 let durations: Vec<f64> = events.iter().map(|e| e.duration_beats).collect();
722 assert!((durations[0] - 0.67).abs() < 1e-9, "m0 first note long");
724 assert!((durations[1] - 0.33).abs() < 1e-9, "m0 second note short");
726 assert!((durations[2] - 0.67).abs() < 1e-9, "m1 first note long (reset)");
728 assert!((durations[3] - 0.33).abs() < 1e-9, "m1 second note short");
730 }
731
732 #[test]
735 fn playback_position_at_zero_is_measure_0_beat_0() {
736 let score = Score::new("T", 120, 4, 4, 0, 4);
737 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 0.0).unwrap();
738 assert_eq!(pos.measure_index, 0);
739 assert!(pos.beat.abs() < 1e-9);
740 }
741
742 #[test]
743 fn playback_position_at_half_measure_is_beat_2() {
744 let score = Score::new("T", 120, 4, 4, 0, 4);
746 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 0.5).unwrap();
747 assert_eq!(pos.measure_index, 0);
748 assert!((pos.beat - 1.0).abs() < 1e-9, "expected beat 1.0, got {}", pos.beat);
749 }
750
751 #[test]
752 fn playback_position_beyond_score_is_none() {
753 let score = Score::new("T", 120, 4, 4, 0, 1);
755 assert!(compute_playback_position(&score, &PlaybackOptions::default(), 10.0).is_none());
756 }
757
758 #[test]
759 fn playback_position_tempo_change_takes_effect() {
760 let mut score = Score::new("T", 120, 4, 4, 0, 2);
763 score.parts[0].staves[0].measures[1].tempo = Some(60);
764 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 2.5).unwrap();
765 assert_eq!(pos.measure_index, 1);
766 assert!((pos.beat - 0.5).abs() < 1e-9, "expected beat 0.5, got {}", pos.beat);
767 }
768
769 #[test]
770 fn playback_position_loop_region_starts_at_zero() {
771 let score = Score::new("T", 120, 4, 4, 0, 4);
773 let options = PlaybackOptions { loop_region: Some((1, 2)), ..Default::default() };
774 let pos = compute_playback_position(&score, &options, 0.0).unwrap();
775 assert_eq!(pos.measure_index, 1);
776 assert!(pos.beat.abs() < 1e-9);
777 }
778
779 #[test]
782 fn metronome_injects_beat_events() {
783 let score = Score::new("T", 120, 4, 4, 0, 1);
785 let options = PlaybackOptions {
786 metronome: Some(MetronomeConfig::default()),
787 ..Default::default()
788 };
789 let events = to_playback_events(&score, &options);
790 let metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
791 assert_eq!(metro_events.len(), 4, "expected 4 metronome clicks in 4/4");
792 }
793
794 #[test]
795 fn metronome_accent_is_first_beat() {
796 let score = Score::new("T", 120, 4, 4, 0, 1);
797 let metro = MetronomeConfig::default();
798 let options = PlaybackOptions { metronome: Some(metro.clone()), ..Default::default() };
799 let events = to_playback_events(&score, &options);
800 let mut metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
801 metro_events.sort_by(|a, b| a.time_beats.partial_cmp(&b.time_beats).unwrap());
802 assert_eq!(metro_events[0].pitch_midi, metro.accent_pitch);
803 assert_eq!(metro_events[0].velocity, metro.accent_velocity);
804 }
805
806 #[test]
807 fn metronome_regular_beat_pitch() {
808 let score = Score::new("T", 120, 4, 4, 0, 1);
809 let metro = MetronomeConfig::default();
810 let options = PlaybackOptions { metronome: Some(metro.clone()), ..Default::default() };
811 let events = to_playback_events(&score, &options);
812 let mut metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
813 metro_events.sort_by(|a, b| a.time_beats.partial_cmp(&b.time_beats).unwrap());
814 for ev in &metro_events[1..] {
815 assert_eq!(ev.pitch_midi, metro.beat_pitch);
816 assert_eq!(ev.velocity, metro.beat_velocity);
817 }
818 }
819
820 #[test]
821 fn metronome_events_are_marked() {
822 let score = Score::new("T", 120, 4, 4, 0, 1);
823 let options = PlaybackOptions {
824 metronome: Some(MetronomeConfig::default()),
825 ..Default::default()
826 };
827 let events = to_playback_events(&score, &options);
828 assert!(events.iter().any(|e| e.is_metronome));
829 }
830
831 #[test]
832 fn metronome_none_produces_no_extra_events() {
833 let score = Score::new("T", 120, 4, 4, 0, 1);
834 let events = to_playback_events(&score, &PlaybackOptions::default());
835 assert!(events.iter().all(|e| !e.is_metronome));
836 }
837}