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