1use super::duration::Duration;
2use super::notation::Articulation;
3use super::repeat::measure_sequence;
4use super::score::Score;
5use serde::{Deserialize, Serialize};
6
7fn default_fermata_multiplier() -> f64 {
8 1.5
9}
10fn default_swing_unit() -> Duration {
11 Duration::Eighth
12}
13fn default_metronome_channel() -> u8 {
14 9
15}
16fn default_accent_pitch() -> u8 {
17 76
18}
19fn default_beat_pitch() -> u8 {
20 77
21}
22fn default_accent_velocity() -> u8 {
23 100
24}
25fn default_beat_velocity() -> u8 {
26 70
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct MetronomeConfig {
35 #[serde(default = "default_metronome_channel")]
37 pub channel: u8,
38 #[serde(default = "default_accent_pitch")]
40 pub accent_pitch: u8,
41 #[serde(default = "default_beat_pitch")]
43 pub beat_pitch: u8,
44 #[serde(default = "default_accent_velocity")]
46 pub accent_velocity: u8,
47 #[serde(default = "default_beat_velocity")]
49 pub beat_velocity: u8,
50}
51
52impl Default for MetronomeConfig {
53 fn default() -> Self {
54 Self {
55 channel: 9,
56 accent_pitch: 76,
57 beat_pitch: 77,
58 accent_velocity: 100,
59 beat_velocity: 70,
60 }
61 }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct PlaybackOptions {
67 pub bpm_override: Option<u16>,
69 pub muted_parts: Vec<usize>,
71 #[serde(default)]
75 pub loop_region: Option<(usize, usize)>,
76 #[serde(default = "default_fermata_multiplier")]
78 pub fermata_multiplier: f64,
79 #[serde(default)]
83 pub swing: Option<f64>,
84 #[serde(default = "default_swing_unit")]
87 pub swing_unit: Duration,
88 #[serde(default)]
91 pub metronome: Option<MetronomeConfig>,
92}
93
94impl Default for PlaybackOptions {
95 fn default() -> Self {
96 Self {
97 bpm_override: None,
98 muted_parts: Vec::new(),
99 loop_region: None,
100 fermata_multiplier: 1.5,
101 swing: None,
102 swing_unit: Duration::Eighth,
103 metronome: None,
104 }
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct PlaybackEvent {
113 pub time_beats: f64,
115 pub time_secs: f64,
117 pub pitch_midi: u8,
119 pub velocity: u8,
122 pub duration_beats: f64,
124 pub duration_secs: f64,
126 pub pedal: bool,
128 pub part_index: usize,
130 pub channel: u8,
132 #[serde(default)]
134 pub is_metronome: bool,
135}
136
137pub fn to_playback_events(score: &Score, options: &PlaybackOptions) -> Vec<PlaybackEvent> {
143 let bpm = options
144 .bpm_override
145 .unwrap_or(score.settings.tempo_bpm)
146 .max(1) as f64;
147 let full_seq = measure_sequence(score);
148 let seq: Vec<usize> = if let Some((lo, hi)) = options.loop_region {
149 full_seq
150 .into_iter()
151 .filter(|&idx| idx >= lo && idx <= hi)
152 .collect()
153 } else {
154 full_seq
155 };
156 let mut events: Vec<PlaybackEvent> = Vec::new();
157
158 for (part_index, part) in score.parts.iter().enumerate() {
159 if options.muted_parts.contains(&part_index) {
160 continue;
161 }
162 for staff in &part.staves {
163 for voice_idx in 0..4usize {
164 let mut time_beats = 0.0f64;
165 let mut time_secs_cursor = 0.0f64;
166 let mut current_bpm = bpm;
167 for &idx in &seq {
168 let measure = match staff.measures.get(idx) {
169 Some(m) => m,
170 None => continue,
171 };
172 if let Some(b) = measure.tempo {
173 current_bpm = b.max(1) as f64;
174 }
175 let measure_beats = measure
176 .time_sig
177 .as_ref()
178 .unwrap_or(&score.settings.time_signature)
179 .total_beats();
180 let measure_start_beats = time_beats;
181 let measure_start_secs = time_secs_cursor;
182 let mut local_beats = 0.0f64;
183 let mut local_secs = 0.0f64;
184 let mut swing_first = true;
185 for note in &measure.voices[voice_idx] {
186 if note.is_grace {
187 continue;
188 }
189 let dur = match options.swing {
190 Some(ratio)
191 if note.tuplet.is_none()
192 && note.dot_count == 0
193 && note.duration == options.swing_unit =>
194 {
195 let pair = note.beats() * 2.0;
196 let d = if swing_first {
197 ratio * pair
198 } else {
199 (1.0 - ratio) * pair
200 };
201 swing_first = !swing_first;
202 d
203 }
204 Some(_) => {
205 swing_first = true;
206 note.beats()
207 }
208 None => note.beats(),
209 };
210 if !note.is_rest {
211 let mut velocity = note
212 .dynamic
213 .as_ref()
214 .map(|d| d.to_velocity())
215 .unwrap_or(64u8);
216 let mut sounding_dur = dur;
217 for art in ¬e.articulations {
218 match art {
219 Articulation::Staccato | Articulation::Staccatissimo => {
220 sounding_dur *= 0.5;
221 }
222 Articulation::Accent | Articulation::Marcato => {
223 velocity = velocity.saturating_add(20).min(127);
224 }
225 Articulation::Fermata => {
226 sounding_dur *= options.fermata_multiplier;
227 }
228 _ => {}
229 }
230 }
231 let pedal = note.pedal_start;
232 let transpose = if part.midi_channel == 9 {
233 0i8
234 } else {
235 staff.transpose_semitones
236 };
237 for pitch in ¬e.pitches {
238 let midi = (pitch.to_midi() + transpose as i16).clamp(0, 127) as u8;
239 events.push(PlaybackEvent {
240 time_beats: measure_start_beats + local_beats,
241 time_secs: measure_start_secs + local_secs,
242 pitch_midi: midi,
243 velocity,
244 duration_beats: sounding_dur,
245 duration_secs: sounding_dur / current_bpm * 60.0,
246 pedal,
247 part_index,
248 channel: part.midi_channel,
249 is_metronome: false,
250 });
251 }
252 }
253 local_beats += dur;
254 local_secs += dur / current_bpm * 60.0;
255 }
256 time_beats = measure_start_beats + measure_beats;
260 time_secs_cursor = measure_start_secs + measure_beats / current_bpm * 60.0;
261 }
262 }
263 }
264 }
265
266 if let Some(ref metro) = options.metronome {
267 let mut cursor_secs = 0.0f64;
268 let mut cursor_beats = 0.0f64;
269 let mut metro_bpm = bpm;
270 for &idx in &seq {
271 let first_staff = score.parts.first().and_then(|p| p.staves.first());
272 if let Some(t) = first_staff
273 .and_then(|s| s.measures.get(idx))
274 .and_then(|m| m.tempo)
275 {
276 metro_bpm = t.max(1) as f64;
277 }
278 let ts = first_staff
279 .and_then(|s| s.measures.get(idx))
280 .and_then(|m| m.time_sig.as_ref())
281 .unwrap_or(&score.settings.time_signature);
282 let beat_unit = ts.beat_unit_beats();
283 let num_beats = (ts.total_beats() / beat_unit).round() as u32;
284 for b in 0..num_beats {
285 let is_accent = b == 0;
286 let beat_offset_secs = b as f64 * beat_unit / metro_bpm * 60.0;
287 events.push(PlaybackEvent {
288 time_beats: cursor_beats + b as f64 * beat_unit,
289 time_secs: cursor_secs + beat_offset_secs,
290 pitch_midi: if is_accent {
291 metro.accent_pitch
292 } else {
293 metro.beat_pitch
294 },
295 velocity: if is_accent {
296 metro.accent_velocity
297 } else {
298 metro.beat_velocity
299 },
300 duration_beats: beat_unit * 0.1,
301 duration_secs: beat_unit * 0.1 / metro_bpm * 60.0,
302 pedal: false,
303 part_index: usize::MAX,
304 channel: metro.channel,
305 is_metronome: true,
306 });
307 }
308 let measure_beats = ts.total_beats();
309 cursor_secs += measure_beats / metro_bpm * 60.0;
310 cursor_beats += measure_beats;
311 }
312 }
313
314 events.sort_by(|a, b| {
315 a.time_beats
316 .partial_cmp(&b.time_beats)
317 .unwrap_or(std::cmp::Ordering::Equal)
318 });
319 events
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct PlaybackPosition {
327 pub measure_index: usize,
329 pub beat: f64,
331}
332
333struct MeasureSegment {
334 measure_idx: usize,
335 start_secs: f64,
336 duration_secs: f64,
337 beats: f64,
338 bpm: f64,
339}
340
341fn build_measure_segments(score: &Score, options: &PlaybackOptions) -> Vec<MeasureSegment> {
342 let init_bpm = options
343 .bpm_override
344 .unwrap_or(score.settings.tempo_bpm)
345 .max(1) as f64;
346 let full_seq = measure_sequence(score);
347 let seq: Vec<usize> = if let Some((lo, hi)) = options.loop_region {
348 full_seq
349 .into_iter()
350 .filter(|&i| i >= lo && i <= hi)
351 .collect()
352 } else {
353 full_seq
354 };
355
356 let mut segments = Vec::with_capacity(seq.len());
357 let mut cursor_secs = 0.0f64;
358 let mut current_bpm = init_bpm;
359
360 for idx in seq {
361 let first_measure = score
362 .parts
363 .first()
364 .and_then(|p| p.staves.first())
365 .and_then(|s| s.measures.get(idx));
366 if let Some(t) = first_measure.and_then(|m| m.tempo) {
367 current_bpm = t.max(1) as f64;
368 }
369 let ts = first_measure
370 .and_then(|m| m.time_sig.as_ref())
371 .unwrap_or(&score.settings.time_signature);
372 let beats = ts.total_beats();
373 let duration_secs = beats / current_bpm * 60.0;
374
375 segments.push(MeasureSegment {
376 measure_idx: idx,
377 start_secs: cursor_secs,
378 duration_secs,
379 beats,
380 bpm: current_bpm,
381 });
382 cursor_secs += duration_secs;
383 }
384 segments
385}
386
387pub fn compute_playback_position(
393 score: &Score,
394 options: &PlaybackOptions,
395 elapsed_secs: f64,
396) -> Option<PlaybackPosition> {
397 if elapsed_secs < 0.0 {
398 return None;
399 }
400 let segments = build_measure_segments(score, options);
401 for seg in &segments {
402 if elapsed_secs < seg.start_secs + seg.duration_secs + 1e-9 {
403 let beat = ((elapsed_secs - seg.start_secs) * seg.bpm / 60.0).clamp(0.0, seg.beats);
404 return Some(PlaybackPosition {
405 measure_index: seg.measure_idx,
406 beat,
407 });
408 }
409 }
410 None
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use crate::model::{
417 duration::Duration,
418 pitch::{Pitch, Step},
419 score::{Note, Score},
420 };
421
422 fn opts(bpm: Option<u16>) -> PlaybackOptions {
423 PlaybackOptions {
424 bpm_override: bpm,
425 ..Default::default()
426 }
427 }
428
429 #[test]
430 fn empty_score_no_events() {
431 let score = Score::new("T", 120, 4, 4, 0, 1);
432 assert!(to_playback_events(&score, &opts(None)).is_empty());
433 }
434
435 #[test]
436 fn single_note_at_beat_zero() {
437 let mut score = Score::new("T", 120, 4, 4, 0, 1);
438 score.parts[0].staves[0].measures[0].voices[0] =
439 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
440 let events = to_playback_events(&score, &opts(None));
441 assert_eq!(events.len(), 1);
442 assert!((events[0].time_beats).abs() < 1e-9);
443 assert_eq!(events[0].pitch_midi, 60);
444 assert_eq!(events[0].velocity, 64);
445 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
446 assert_eq!(events[0].part_index, 0);
447 }
448
449 #[test]
450 fn chord_expands_to_multiple_events() {
451 let mut score = Score::new("T", 120, 4, 4, 0, 1);
452 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
453 note.pitches.push(Pitch::new(Step::E, 4));
454 note.pitches.push(Pitch::new(Step::G, 4));
455 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
456 let events = to_playback_events(&score, &opts(None));
457 assert_eq!(events.len(), 3);
458 assert!(events.iter().all(|e| e.time_beats.abs() < 1e-9));
459 }
460
461 #[test]
462 fn grace_notes_excluded() {
463 let mut score = Score::new("T", 120, 4, 4, 0, 1);
464 let mut grace = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
465 grace.is_grace = true;
466 let regular = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
467 score.parts[0].staves[0].measures[0].voices[0] = vec![grace, regular];
468 let events = to_playback_events(&score, &opts(None));
469 assert_eq!(events.len(), 1);
470 assert_eq!(events[0].pitch_midi, 60);
471 }
472
473 #[test]
474 fn second_note_has_correct_time() {
475 let mut score = Score::new("T", 120, 4, 4, 0, 1);
476 score.parts[0].staves[0].measures[0].voices[0] = vec![
477 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
478 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
479 ];
480 let events = to_playback_events(&score, &opts(None));
481 assert_eq!(events.len(), 2);
482 assert!((events[0].time_beats).abs() < 1e-9);
483 assert!((events[1].time_beats - 1.0).abs() < 1e-9);
484 }
485
486 #[test]
487 fn sparse_voice_keeps_measure_boundaries() {
488 let mut score = Score::new("T", 120, 4, 4, 0, 2);
489 let note = || Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
490 score.parts[0].staves[0].measures[0].voices[1] = vec![note()];
491 score.parts[0].staves[0].measures[1].voices[1] = vec![note()];
492
493 let events = to_playback_events(&score, &opts(None));
494 assert_eq!(events.len(), 2);
495 assert!((events[0].time_beats).abs() < 1e-9);
496 assert!((events[1].time_beats - 4.0).abs() < 1e-9);
497 assert!((events[1].time_secs - 2.0).abs() < 1e-9);
498 }
499
500 #[test]
501 fn time_secs_120_bpm_quarter_note_is_half_second() {
502 let mut score = Score::new("T", 120, 4, 4, 0, 1);
503 score.parts[0].staves[0].measures[0].voices[0] =
504 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
505 let events = to_playback_events(&score, &opts(None));
506 assert!((events[0].time_secs).abs() < 1e-9);
507 assert!((events[0].duration_secs - 0.5).abs() < 1e-9);
508 }
509
510 #[test]
511 fn bpm_override_changes_time_secs() {
512 let mut score = Score::new("T", 120, 4, 4, 0, 1);
513 score.parts[0].staves[0].measures[0].voices[0] = vec![
514 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
515 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
516 ];
517 let events = to_playback_events(&score, &opts(Some(60)));
518 assert!((events[0].time_secs).abs() < 1e-9);
519 assert!((events[1].time_secs - 1.0).abs() < 1e-9);
520 }
521
522 #[test]
523 fn transpose_semitones_shifts_midi_output() {
524 let mut score = Score::new("T", 120, 4, 4, 0, 1);
525 score.parts[0].staves[0].transpose_semitones = -2;
526 score.parts[0].staves[0].measures[0].voices[0] =
527 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
528 let events = to_playback_events(&score, &opts(None));
529 assert_eq!(events.len(), 1);
530 assert_eq!(events[0].pitch_midi, 58);
531 }
532
533 #[test]
534 fn percussion_channel_9_ignores_transpose_semitones() {
535 let mut score = Score::new("T", 120, 4, 4, 0, 1);
536 score.parts[0].midi_channel = 9;
537 score.parts[0].staves[0].transpose_semitones = -2;
538 score.parts[0].staves[0].measures[0].voices[0] =
539 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
540 let events = to_playback_events(&score, &opts(None));
541 assert_eq!(events.len(), 1);
542 assert_eq!(events[0].pitch_midi, 60);
543 }
544
545 #[test]
546 fn staccato_halves_duration_beats() {
547 let mut score = Score::new("T", 120, 4, 4, 0, 1);
548 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
549 note.articulations
550 .push(crate::model::notation::Articulation::Staccato);
551 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
552 let events = to_playback_events(&score, &opts(None));
553 assert_eq!(events.len(), 1);
554 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
555 }
556
557 #[test]
558 fn staccatissimo_also_halves_duration() {
559 let mut score = Score::new("T", 120, 4, 4, 0, 1);
560 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
561 note.articulations
562 .push(crate::model::notation::Articulation::Staccatissimo);
563 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
564 let events = to_playback_events(&score, &opts(None));
565 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
566 }
567
568 #[test]
569 fn staccato_does_not_shift_next_note_time() {
570 let mut score = Score::new("T", 120, 4, 4, 0, 1);
571 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
572 n1.articulations
573 .push(crate::model::notation::Articulation::Staccato);
574 let n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
575 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
576 let events = to_playback_events(&score, &opts(None));
577 assert_eq!(events.len(), 2);
578 assert!((events[1].time_beats - 1.0).abs() < 1e-9);
579 }
580
581 #[test]
582 fn accent_boosts_velocity_clamped() {
583 let mut score = Score::new("T", 120, 4, 4, 0, 1);
584 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
585 note.articulations
586 .push(crate::model::notation::Articulation::Accent);
587 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
588 let events = to_playback_events(&score, &opts(None));
589 assert_eq!(events[0].velocity, 84);
590 }
591
592 #[test]
593 fn accent_clamped_at_127() {
594 let mut score = Score::new("T", 120, 4, 4, 0, 1);
595 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
596 note.dynamic = Some(crate::model::notation::Dynamic::Ffff);
597 note.articulations
598 .push(crate::model::notation::Articulation::Accent);
599 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
600 let events = to_playback_events(&score, &opts(None));
601 assert_eq!(events[0].velocity, 127);
602 }
603
604 #[test]
605 fn tenuto_keeps_full_duration() {
606 let mut score = Score::new("T", 120, 4, 4, 0, 1);
607 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
608 note.articulations
609 .push(crate::model::notation::Articulation::Tenuto);
610 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
611 let events = to_playback_events(&score, &opts(None));
612 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
613 }
614
615 #[test]
616 fn pedal_start_sets_pedal_field() {
617 let mut score = Score::new("T", 120, 4, 4, 0, 1);
618 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
619 note.pedal_start = true;
620 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
621 let events = to_playback_events(&score, &opts(None));
622 assert!(events[0].pedal);
623 }
624
625 #[test]
626 fn no_pedal_start_pedal_is_false() {
627 let mut score = Score::new("T", 120, 4, 4, 0, 1);
628 score.parts[0].staves[0].measures[0].voices[0] =
629 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
630 let events = to_playback_events(&score, &opts(None));
631 assert!(!events[0].pedal);
632 }
633
634 #[test]
635 fn set_tempo_at_measure_changes_time_secs() {
636 let mut score = Score::new("T", 120, 4, 4, 0, 2);
637 score.parts[0].staves[0].measures[0].voices[0] =
638 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
639 score.parts[0].staves[0].measures[1].tempo = Some(60);
640 score.parts[0].staves[0].measures[1].voices[0] =
641 vec![Note::new(Pitch::new(Step::D, 4), Duration::Whole)];
642 let events = to_playback_events(&score, &opts(None));
643 assert_eq!(events.len(), 2);
644 assert!((events[0].time_secs).abs() < 1e-9);
645 assert!((events[0].duration_secs - 2.0).abs() < 1e-9);
646 assert!((events[1].time_secs - 2.0).abs() < 1e-9);
647 assert!((events[1].duration_secs - 4.0).abs() < 1e-9);
648 }
649
650 #[test]
651 fn muted_part_produces_no_events() {
652 let mut score = Score::new("T", 120, 4, 4, 0, 1);
653 score.parts[0].staves[0].measures[0].voices[0] =
654 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
655 let options = PlaybackOptions {
656 muted_parts: vec![0],
657 ..Default::default()
658 };
659 assert!(to_playback_events(&score, &options).is_empty());
660 }
661
662 #[test]
663 fn part_index_field_set_correctly() {
664 let mut score = Score::new("T", 120, 4, 4, 0, 1);
665 score.parts[0].staves[0].measures[0].voices[0] =
666 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
667 let events = to_playback_events(&score, &opts(None));
668 assert_eq!(events[0].part_index, 0);
669 }
670
671 #[test]
674 fn loop_region_filters_measures() {
675 let mut score = Score::new("T", 120, 4, 4, 0, 3);
677 for mi in 0..3 {
678 score.parts[0].staves[0].measures[mi].voices[0] =
679 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
680 }
681 let options = PlaybackOptions {
682 loop_region: Some((1, 2)),
683 ..Default::default()
684 };
685 let events = to_playback_events(&score, &options);
686 assert_eq!(events.len(), 2);
687 assert!((events[0].time_beats).abs() < 1e-9);
689 }
690
691 #[test]
692 fn loop_region_none_plays_all_measures() {
693 let mut score = Score::new("T", 120, 4, 4, 0, 3);
694 for mi in 0..3 {
695 score.parts[0].staves[0].measures[mi].voices[0] =
696 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
697 }
698 let events = to_playback_events(&score, &opts(None));
699 assert_eq!(events.len(), 3);
700 }
701
702 #[test]
705 fn fermata_multiplier_extends_duration() {
706 let mut score = Score::new("T", 120, 4, 4, 0, 1);
707 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
708 note.articulations
709 .push(crate::model::notation::Articulation::Fermata);
710 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
711 let options = PlaybackOptions {
712 fermata_multiplier: 2.0,
713 ..Default::default()
714 };
715 let events = to_playback_events(&score, &options);
716 assert_eq!(events.len(), 1);
717 assert!((events[0].duration_beats - 2.0).abs() < 1e-9);
718 }
719
720 #[test]
721 fn fermata_default_multiplier_is_1_5() {
722 let mut score = Score::new("T", 120, 4, 4, 0, 1);
723 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
724 note.articulations
725 .push(crate::model::notation::Articulation::Fermata);
726 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
727 let events = to_playback_events(&score, &PlaybackOptions::default());
728 assert_eq!(events.len(), 1);
729 assert!((events[0].duration_beats - 1.5).abs() < 1e-9);
730 }
731
732 #[test]
733 fn non_fermata_note_unaffected_by_fermata_multiplier() {
734 let mut score = Score::new("T", 120, 4, 4, 0, 1);
735 score.parts[0].staves[0].measures[0].voices[0] =
736 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
737 let options = PlaybackOptions {
738 fermata_multiplier: 3.0,
739 ..Default::default()
740 };
741 let events = to_playback_events(&score, &options);
742 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
743 }
744
745 #[test]
748 fn swing_triplet_first_eighth_is_long() {
749 let mut score = Score::new("T", 120, 4, 4, 0, 1);
751 score.parts[0].staves[0].measures[0].voices[0] = vec![
752 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
753 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
754 ];
755 let options = PlaybackOptions {
756 swing: Some(0.67),
757 ..Default::default()
758 };
759 let events = to_playback_events(&score, &options);
760 let e_c = events.iter().find(|e| e.pitch_midi == 60).unwrap();
762 let e_d = events.iter().find(|e| e.pitch_midi == 62).unwrap();
763 assert!(
764 (e_c.duration_beats - 0.67).abs() < 1e-9,
765 "first eighth should be 0.67"
766 );
767 assert!(
768 (e_d.duration_beats - 0.33).abs() < 1e-9,
769 "second eighth should be 0.33"
770 );
771 assert!(
773 (e_d.time_beats - 0.67).abs() < 1e-9,
774 "second note start should be at 0.67"
775 );
776 }
777
778 #[test]
779 fn swing_non_eighth_not_affected() {
780 let mut score = Score::new("T", 120, 4, 4, 0, 1);
781 score.parts[0].staves[0].measures[0].voices[0] =
782 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
783 let options = PlaybackOptions {
784 swing: Some(0.67),
785 ..Default::default()
786 };
787 let events = to_playback_events(&score, &options);
788 assert!(
789 (events[0].duration_beats - 1.0).abs() < 1e-9,
790 "quarter note unaffected"
791 );
792 }
793
794 #[test]
795 fn swing_none_is_straight() {
796 let mut score = Score::new("T", 120, 4, 4, 0, 1);
797 score.parts[0].staves[0].measures[0].voices[0] =
798 vec![Note::new(Pitch::new(Step::C, 4), Duration::Eighth)];
799 let options = PlaybackOptions {
800 swing: None,
801 ..Default::default()
802 };
803 let events = to_playback_events(&score, &options);
804 assert!(
805 (events[0].duration_beats - 0.5).abs() < 1e-9,
806 "no swing = straight eighth"
807 );
808 }
809
810 #[test]
811 fn channel_matches_part_midi_channel() {
812 let mut score = Score::new("T", 120, 4, 4, 0, 1);
813 score.parts[0].midi_channel = 3;
814 score.parts[0].staves[0].measures[0].voices[0] =
815 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
816 let events = to_playback_events(&score, &opts(None));
817 assert_eq!(events[0].channel, 3);
818 }
819
820 #[test]
821 fn swing_unit_default_is_eighth() {
822 assert_eq!(PlaybackOptions::default().swing_unit, Duration::Eighth);
823 }
824
825 #[test]
826 fn swing_unit_sixteenth() {
827 let mut score = Score::new("T", 120, 4, 4, 0, 1);
828 score.parts[0].staves[0].measures[0].voices[0] = vec![
829 Note::new(Pitch::new(Step::C, 4), Duration::Sixteenth),
830 Note::new(Pitch::new(Step::D, 4), Duration::Sixteenth),
831 ];
832 let options = PlaybackOptions {
833 swing: Some(0.67),
834 swing_unit: Duration::Sixteenth,
835 ..Default::default()
836 };
837 let events = to_playback_events(&score, &options);
838 let e_c = events.iter().find(|e| e.pitch_midi == 60).unwrap();
839 let e_d = events.iter().find(|e| e.pitch_midi == 62).unwrap();
840 assert!(
841 (e_c.duration_beats - 0.335).abs() < 1e-9,
842 "first 16th should be 0.335"
843 );
844 assert!(
845 (e_d.duration_beats - 0.165).abs() < 1e-9,
846 "second 16th should be 0.165"
847 );
848 }
849
850 #[test]
851 fn swing_resets_per_measure() {
852 let mut score = Score::new("T", 120, 4, 4, 0, 2);
854 let pair = || {
855 vec![
856 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
857 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
858 ]
859 };
860 score.parts[0].staves[0].measures[0].voices[0] = pair();
861 score.parts[0].staves[0].measures[1].voices[0] = pair();
862 let options = PlaybackOptions {
863 swing: Some(0.67),
864 ..Default::default()
865 };
866 let events = to_playback_events(&score, &options);
867 let durations: Vec<f64> = events.iter().map(|e| e.duration_beats).collect();
869 assert!((durations[0] - 0.67).abs() < 1e-9, "m0 first note long");
871 assert!((durations[1] - 0.33).abs() < 1e-9, "m0 second note short");
873 assert!(
875 (durations[2] - 0.67).abs() < 1e-9,
876 "m1 first note long (reset)"
877 );
878 assert!((durations[3] - 0.33).abs() < 1e-9, "m1 second note short");
880 }
881
882 #[test]
885 fn playback_position_at_zero_is_measure_0_beat_0() {
886 let score = Score::new("T", 120, 4, 4, 0, 4);
887 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 0.0).unwrap();
888 assert_eq!(pos.measure_index, 0);
889 assert!(pos.beat.abs() < 1e-9);
890 }
891
892 #[test]
893 fn playback_position_at_half_measure_is_beat_2() {
894 let score = Score::new("T", 120, 4, 4, 0, 4);
896 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 0.5).unwrap();
897 assert_eq!(pos.measure_index, 0);
898 assert!(
899 (pos.beat - 1.0).abs() < 1e-9,
900 "expected beat 1.0, got {}",
901 pos.beat
902 );
903 }
904
905 #[test]
906 fn playback_position_beyond_score_is_none() {
907 let score = Score::new("T", 120, 4, 4, 0, 1);
909 assert!(compute_playback_position(&score, &PlaybackOptions::default(), 10.0).is_none());
910 }
911
912 #[test]
913 fn playback_position_tempo_change_takes_effect() {
914 let mut score = Score::new("T", 120, 4, 4, 0, 2);
917 score.parts[0].staves[0].measures[1].tempo = Some(60);
918 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 2.5).unwrap();
919 assert_eq!(pos.measure_index, 1);
920 assert!(
921 (pos.beat - 0.5).abs() < 1e-9,
922 "expected beat 0.5, got {}",
923 pos.beat
924 );
925 }
926
927 #[test]
928 fn playback_position_loop_region_starts_at_zero() {
929 let score = Score::new("T", 120, 4, 4, 0, 4);
931 let options = PlaybackOptions {
932 loop_region: Some((1, 2)),
933 ..Default::default()
934 };
935 let pos = compute_playback_position(&score, &options, 0.0).unwrap();
936 assert_eq!(pos.measure_index, 1);
937 assert!(pos.beat.abs() < 1e-9);
938 }
939
940 #[test]
943 fn metronome_injects_beat_events() {
944 let score = Score::new("T", 120, 4, 4, 0, 1);
946 let options = PlaybackOptions {
947 metronome: Some(MetronomeConfig::default()),
948 ..Default::default()
949 };
950 let events = to_playback_events(&score, &options);
951 let metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
952 assert_eq!(metro_events.len(), 4, "expected 4 metronome clicks in 4/4");
953 }
954
955 #[test]
956 fn metronome_accent_is_first_beat() {
957 let score = Score::new("T", 120, 4, 4, 0, 1);
958 let metro = MetronomeConfig::default();
959 let options = PlaybackOptions {
960 metronome: Some(metro.clone()),
961 ..Default::default()
962 };
963 let events = to_playback_events(&score, &options);
964 let mut metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
965 metro_events.sort_by(|a, b| a.time_beats.partial_cmp(&b.time_beats).unwrap());
966 assert_eq!(metro_events[0].pitch_midi, metro.accent_pitch);
967 assert_eq!(metro_events[0].velocity, metro.accent_velocity);
968 }
969
970 #[test]
971 fn metronome_regular_beat_pitch() {
972 let score = Score::new("T", 120, 4, 4, 0, 1);
973 let metro = MetronomeConfig::default();
974 let options = PlaybackOptions {
975 metronome: Some(metro.clone()),
976 ..Default::default()
977 };
978 let events = to_playback_events(&score, &options);
979 let mut metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
980 metro_events.sort_by(|a, b| a.time_beats.partial_cmp(&b.time_beats).unwrap());
981 for ev in &metro_events[1..] {
982 assert_eq!(ev.pitch_midi, metro.beat_pitch);
983 assert_eq!(ev.velocity, metro.beat_velocity);
984 }
985 }
986
987 #[test]
988 fn metronome_events_are_marked() {
989 let score = Score::new("T", 120, 4, 4, 0, 1);
990 let options = PlaybackOptions {
991 metronome: Some(MetronomeConfig::default()),
992 ..Default::default()
993 };
994 let events = to_playback_events(&score, &options);
995 assert!(events.iter().any(|e| e.is_metronome));
996 }
997
998 #[test]
999 fn metronome_none_produces_no_extra_events() {
1000 let score = Score::new("T", 120, 4, 4, 0, 1);
1001 let events = to_playback_events(&score, &PlaybackOptions::default());
1002 assert!(events.iter().all(|e| !e.is_metronome));
1003 }
1004}