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