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