1use super::duration::Duration;
2use super::notation::{Articulation, ChordSymbol, GuitarTechnique, TabPosition};
3use super::repeat::measure_sequence;
4use super::score::{GuitarBendPoint, Note, NoteAddr, Score};
5use serde::{Deserialize, Serialize};
6use std::collections::{BTreeMap, BTreeSet};
7
8fn default_fermata_multiplier() -> f64 {
9 1.5
10}
11fn default_fermata_hold_beats() -> f64 {
12 0.0
13}
14fn default_swing_unit() -> Duration {
15 Duration::Eighth
16}
17fn default_tempo_multiplier() -> f64 {
18 1.0
19}
20fn default_metronome_channel() -> u8 {
21 9
22}
23fn default_accent_pitch() -> u8 {
24 76
25}
26fn default_beat_pitch() -> u8 {
27 77
28}
29fn default_accent_velocity() -> u8 {
30 100
31}
32fn default_beat_velocity() -> u8 {
33 70
34}
35
36#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
42#[serde(rename_all = "kebab-case")]
43pub enum PlaybackRealizationProfile {
44 #[default]
46 Authored,
47 OrnamentArpeggioV1,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
61pub struct MetronomeConfig {
62 #[serde(default = "default_metronome_channel")]
64 pub channel: u8,
65 #[serde(default = "default_accent_pitch")]
67 pub accent_pitch: u8,
68 #[serde(default = "default_beat_pitch")]
70 pub beat_pitch: u8,
71 #[serde(default = "default_accent_velocity")]
73 pub accent_velocity: u8,
74 #[serde(default = "default_beat_velocity")]
76 pub beat_velocity: u8,
77}
78
79impl Default for MetronomeConfig {
80 fn default() -> Self {
81 Self {
82 channel: 9,
83 accent_pitch: 76,
84 beat_pitch: 77,
85 accent_velocity: 100,
86 beat_velocity: 70,
87 }
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93#[serde(default)]
94pub struct PlaybackOptions {
95 pub bpm_override: Option<u16>,
100 #[serde(default = "default_tempo_multiplier")]
105 pub tempo_multiplier: f64,
106 pub muted_parts: Vec<usize>,
108 #[serde(default, skip_serializing_if = "Vec::is_empty")]
111 pub shared_dynamics: Vec<NoteAddr>,
112 #[serde(default, skip_serializing_if = "Vec::is_empty")]
115 pub shared_hairpins: Vec<NoteAddr>,
116 #[serde(default)]
120 pub loop_region: Option<(usize, usize)>,
121 #[serde(default = "default_fermata_multiplier")]
125 pub fermata_multiplier: f64,
126 #[serde(default = "default_fermata_hold_beats")]
129 pub fermata_hold_beats: f64,
130 #[serde(default)]
135 pub swing: Option<f64>,
136 #[serde(default = "default_swing_unit")]
139 pub swing_unit: Duration,
140 #[serde(default)]
143 pub metronome: Option<MetronomeConfig>,
144 #[serde(default)]
147 pub realization_profile: PlaybackRealizationProfile,
148}
149
150impl Default for PlaybackOptions {
151 fn default() -> Self {
152 Self {
153 bpm_override: None,
154 tempo_multiplier: 1.0,
155 muted_parts: Vec::new(),
156 shared_dynamics: Vec::new(),
157 shared_hairpins: Vec::new(),
158 loop_region: None,
159 fermata_multiplier: 1.5,
160 fermata_hold_beats: 0.0,
161 swing: None,
162 swing_unit: Duration::Eighth,
163 metronome: None,
164 realization_profile: PlaybackRealizationProfile::Authored,
165 }
166 }
167}
168
169fn normalized_tempo_multiplier(options: &PlaybackOptions) -> f64 {
170 let multiplier = options.tempo_multiplier;
171 if multiplier.is_finite() && multiplier > 0.0 {
172 multiplier
173 } else {
174 1.0
175 }
176}
177
178fn swingable_rhythm_slot(note: &Note, unit: &Duration) -> bool {
179 !note.is_grace
180 && !note.is_cue
181 && note.tuplet.is_none()
182 && note.dot_count == 0
183 && note.duration == *unit
184}
185
186fn starts_swing_pair(
187 notes: &[Note],
188 note_index: usize,
189 written_beats: f64,
190 grid_offset_beats: f64,
191 measure_beats: f64,
192 unit: &Duration,
193) -> bool {
194 let Some(first) = notes.get(note_index) else {
195 return false;
196 };
197 let Some(second) = notes.get(note_index + 1) else {
198 return false;
199 };
200 if !swingable_rhythm_slot(first, unit) || !swingable_rhythm_slot(second, unit) {
201 return false;
202 }
203 let pair_beats = first.beats() * 2.0;
204 if written_beats + pair_beats > measure_beats + 1e-9 {
205 return false;
206 }
207 let slot = (written_beats + grid_offset_beats) / pair_beats;
208 (slot - slot.round()).abs() < 1e-9
209}
210
211fn initial_tempo_bpm(score: &Score, options: &PlaybackOptions) -> f64 {
212 f64::from(
213 options
214 .bpm_override
215 .unwrap_or(score.settings.tempo_bpm)
216 .max(1),
217 ) * normalized_tempo_multiplier(options)
218}
219
220fn written_tempo_bpm(options: &PlaybackOptions, tempo: u16) -> Option<f64> {
221 options
222 .bpm_override
223 .is_none()
224 .then(|| f64::from(tempo.max(1)) * normalized_tempo_multiplier(options))
225}
226
227fn written_tempo_ramp_bpm(options: &PlaybackOptions, tempo: Option<u16>) -> Option<f64> {
228 options.bpm_override.is_none().then_some(())?;
229 tempo.map(|tempo| f64::from(tempo.max(1)) * normalized_tempo_multiplier(options))
230}
231
232#[derive(Clone, Copy, Default)]
233struct ScoreTempoMark {
234 bpm: Option<u16>,
235 ramp_to_bpm: Option<u16>,
236}
237
238fn score_tempo_marks(score: &Score) -> Vec<ScoreTempoMark> {
241 let count = score
242 .parts
243 .iter()
244 .flat_map(|part| &part.staves)
245 .map(|staff| staff.measures.len())
246 .max()
247 .unwrap_or(0);
248 let mut marks = vec![ScoreTempoMark::default(); count];
249 for (index, mark) in marks.iter_mut().enumerate() {
250 for part in &score.parts {
251 for staff in &part.staves {
252 if let Some(measure) = staff.measures.get(index) {
253 mark.bpm = mark.bpm.or(measure.tempo);
254 mark.ramp_to_bpm = mark.ramp_to_bpm.or(measure.tempo_ramp_to);
255 }
256 }
257 }
258 }
259 marks
260}
261
262fn semantic_tempo_bpm(bpm: f64) -> u16 {
263 bpm.round().clamp(1.0, f64::from(u16::MAX)) as u16
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
270pub struct PlaybackEvent {
271 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub address: Option<String>,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
277 pub source: Option<NoteAddr>,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
281 pub source_voice_number: Option<u32>,
282 pub time_beats: f64,
284 pub time_secs: f64,
286 pub pitch_midi: u8,
288 #[serde(default)]
293 pub pitch_midi_cents: i32,
294 #[serde(default)]
298 pub pitch_bend_curve: Vec<GuitarBendPoint>,
299 #[serde(default)]
302 pub post_note_pause_beats: f64,
303 #[serde(default)]
306 pub articulations: Vec<Articulation>,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub chord_symbol: Option<ChordSymbol>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub guitar_technique: Option<GuitarTechnique>,
315 pub velocity: u8,
319 pub duration_beats: f64,
321 pub duration_secs: f64,
323 pub pedal: bool,
325 pub part_index: usize,
327 pub channel: u8,
329 #[serde(default)]
331 pub program: u8,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub instrument_id: Option<String>,
335 #[serde(default)]
337 pub is_metronome: bool,
338}
339
340pub const PLAYBACK_COMPARISON_CONTRACT_VERSION: u16 = 2;
342pub const MAX_PLAYBACK_COMPARISON_EVENTS: usize = 1_000_000;
344const MAX_PLAYBACK_MISMATCHES: usize = 256;
345pub const MAX_TAB_PERFORMANCE_EVENTS: usize = 1_000_000;
347const MAX_TAB_PERFORMANCE_DIAGNOSTICS: usize = 1_024;
348
349pub const OFFLINE_RENDER_CONTRACT_VERSION: u16 = 3;
351
352#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
355#[serde(rename_all = "kebab-case")]
356pub enum OfflineRenderScope {
357 #[default]
358 FullScore,
359 MeasureRange {
360 start: usize,
361 end: usize,
362 },
363 Selection {
364 addresses: Vec<NoteAddr>,
365 },
366}
367
368#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
370#[serde(rename_all = "kebab-case")]
371pub enum OfflineRenderNavigationPolicy {
372 #[default]
374 Authored,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
379pub struct OfflineRenderFrameEvent {
380 pub event: PlaybackEvent,
381 pub start_frame: u64,
382 pub duration_frames: u64,
383 pub end_frame: u64,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
390#[serde(rename_all = "kebab-case")]
391pub enum OfflineRenderSemanticEventKind {
392 InstrumentChange {
395 instrument_id: String,
396 channel: u8,
397 program: u8,
398 transpose_semitones: i8,
399 },
400 Controller {
401 channel: u8,
402 controller: u8,
403 value: u8,
404 },
405 PitchBend {
407 channel: u8,
408 value: i16,
409 },
410 ProgramChange {
412 channel: u8,
413 program: u8,
414 },
415 Aftertouch {
417 channel: u8,
418 #[serde(default, skip_serializing_if = "Option::is_none")]
419 key: Option<u8>,
420 value: u8,
421 },
422 Tempo {
423 bpm: u16,
424 #[serde(default, skip_serializing_if = "Option::is_none")]
425 ramp_to_bpm: Option<u16>,
426 },
427 Navigation {
428 marker: String,
429 },
430 Pedal {
431 down: bool,
432 },
433 Articulation {
434 articulation: Articulation,
435 },
436 GuitarBend {
437 points: Vec<GuitarBendPoint>,
438 },
439 ReleaseTail,
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
446pub struct OfflineRenderSemanticFrameEvent {
447 pub kind: OfflineRenderSemanticEventKind,
448 pub frame: u64,
449 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub source: Option<NoteAddr>,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
452 pub measure: Option<OfflineRenderMeasureAddress>,
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
457pub struct OfflineRenderMeasureAddress {
458 pub part: usize,
459 pub staff: usize,
460 pub measure: usize,
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
465#[serde(rename_all = "kebab-case")]
466pub enum OfflineRenderDiagnosticKind {
467 UnresolvedSelectionAddress,
469 EmptySelection,
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
475pub struct OfflineRenderDiagnostic {
476 pub kind: OfflineRenderDiagnosticKind,
477 #[serde(default, skip_serializing_if = "Option::is_none")]
478 pub source: Option<NoteAddr>,
479}
480
481#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
486pub enum OfflineRenderFormat {
487 #[default]
488 Wav,
489 Flac,
490 Mp3,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
495pub struct OfflineRenderRequest {
496 #[serde(default)]
498 pub format: OfflineRenderFormat,
499 #[serde(default = "default_offline_render_sample_rate")]
501 pub sample_rate_hz: u32,
502 #[serde(default = "default_offline_render_channels")]
504 pub channels: u8,
505 #[serde(default, skip_serializing_if = "Option::is_none")]
507 pub provider_identity: Option<String>,
508 #[serde(default, skip_serializing_if = "Option::is_none")]
510 pub asset_identity: Option<String>,
511 #[serde(default, skip_serializing_if = "Option::is_none")]
513 pub view_id: Option<String>,
514 #[serde(default)]
515 pub scope: OfflineRenderScope,
516 #[serde(default)]
517 pub navigation_policy: OfflineRenderNavigationPolicy,
518 #[serde(default)]
520 pub release_tail_millis: u32,
521 #[serde(default, skip_serializing_if = "Option::is_none")]
524 pub playback_options: Option<PlaybackOptions>,
525}
526
527fn default_offline_render_sample_rate() -> u32 {
528 48_000
529}
530
531fn default_offline_render_channels() -> u8 {
532 2
533}
534
535impl Default for OfflineRenderRequest {
536 fn default() -> Self {
537 Self {
538 format: OfflineRenderFormat::Wav,
539 sample_rate_hz: default_offline_render_sample_rate(),
540 channels: default_offline_render_channels(),
541 provider_identity: None,
542 asset_identity: None,
543 view_id: None,
544 scope: OfflineRenderScope::FullScore,
545 navigation_policy: OfflineRenderNavigationPolicy::Authored,
546 release_tail_millis: 0,
547 playback_options: None,
548 }
549 }
550}
551
552#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
557pub struct OfflineRenderManifest {
558 pub contract_version: u16,
559 pub playback_contract_version: u16,
560 pub format: OfflineRenderFormat,
561 pub sample_rate_hz: u32,
562 pub channels: u8,
563 #[serde(default, skip_serializing_if = "Option::is_none")]
564 pub provider_identity: Option<String>,
565 #[serde(default, skip_serializing_if = "Option::is_none")]
566 pub asset_identity: Option<String>,
567 #[serde(default, skip_serializing_if = "Option::is_none")]
568 pub view_id: Option<String>,
569 #[serde(default)]
570 pub scope: OfflineRenderScope,
571 #[serde(default)]
572 pub navigation_policy: OfflineRenderNavigationPolicy,
573 #[serde(default)]
574 pub release_tail_frames: u64,
575 #[serde(default)]
577 pub playback_options: PlaybackOptions,
578 pub duration_frames: u64,
579 pub events: Vec<PlaybackEvent>,
580 #[serde(default)]
583 pub frame_events: Vec<OfflineRenderFrameEvent>,
584 #[serde(default)]
587 pub semantic_events: Vec<OfflineRenderSemanticFrameEvent>,
588 #[serde(default)]
589 pub diagnostics: Vec<OfflineRenderDiagnostic>,
590}
591
592impl OfflineRenderManifest {
593 pub fn validate(&self) -> Result<(), crate::Error> {
597 if !(1..=OFFLINE_RENDER_CONTRACT_VERSION).contains(&self.contract_version)
598 || !(8_000..=384_000).contains(&self.sample_rate_hz)
599 || !(1..=8).contains(&self.channels)
600 || self.release_tail_frames > self.duration_frames
601 {
602 return Err(crate::Error::InvalidOfflineRenderRequest);
603 }
604
605 if self.contract_version >= 2 && self.frame_events.len() != self.events.len() {
606 return Err(crate::Error::InvalidOfflineRenderRequest);
607 }
608 let mut latest_event_end = 0_u64;
609 for (index, frame_event) in self.frame_events.iter().enumerate() {
610 let Some(expected_event) = self.events.get(index) else {
611 return Err(crate::Error::InvalidOfflineRenderRequest);
612 };
613 if frame_event.event != *expected_event
614 || frame_event
615 .start_frame
616 .checked_add(frame_event.duration_frames)
617 != Some(frame_event.end_frame)
618 || frame_event.end_frame > self.duration_frames
619 {
620 return Err(crate::Error::InvalidOfflineRenderRequest);
621 }
622 latest_event_end = latest_event_end.max(frame_event.end_frame);
623 }
624 let latest_semantic_frame = self
625 .semantic_events
626 .iter()
627 .map(|event| event.frame)
628 .max()
629 .unwrap_or(0);
630 if latest_semantic_frame > self.duration_frames {
631 return Err(crate::Error::InvalidOfflineRenderRequest);
632 }
633 if self.contract_version >= 2
634 && latest_event_end
635 .max(latest_semantic_frame)
636 .checked_add(self.release_tail_frames)
637 != Some(self.duration_frames)
638 {
639 return Err(crate::Error::InvalidOfflineRenderRequest);
640 }
641 Ok(())
642 }
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
649pub struct OfflineRenderResult {
650 pub contract_version: u16,
651 pub format: OfflineRenderFormat,
652 pub sample_rate_hz: u32,
653 pub channels: u8,
654 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub provider_identity: Option<String>,
656 pub duration_frames: u64,
657 pub output_bytes: u64,
658}
659
660#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
662pub struct PlaybackAuxSend {
663 pub source_bus: String,
664 pub destination_bus: String,
665 pub gain_db: f64,
667}
668
669#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
671pub struct PlaybackEffectRoute {
672 pub bus_id: String,
673 pub effect_id: String,
674 #[serde(default = "default_effect_enabled")]
675 pub enabled: bool,
676}
677
678fn default_effect_enabled() -> bool {
679 true
680}
681
682#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
684pub struct PlaybackRoutingConfig {
685 #[serde(default, skip_serializing_if = "Option::is_none")]
686 pub provider_identity: Option<String>,
687 #[serde(default = "default_master_bus")]
688 pub master_bus: String,
689 #[serde(default = "default_metronome_bus")]
690 pub metronome_bus: String,
691 #[serde(default)]
693 pub part_buses: BTreeMap<usize, String>,
694 #[serde(default)]
695 pub instrument_buses: BTreeMap<String, String>,
696 #[serde(default)]
697 pub aux_sends: Vec<PlaybackAuxSend>,
698 #[serde(default)]
699 pub effect_routes: Vec<PlaybackEffectRoute>,
700}
701
702fn default_master_bus() -> String {
703 "master".into()
704}
705
706fn default_metronome_bus() -> String {
707 "metronome".into()
708}
709
710impl Default for PlaybackRoutingConfig {
711 fn default() -> Self {
712 Self {
713 provider_identity: None,
714 master_bus: default_master_bus(),
715 metronome_bus: default_metronome_bus(),
716 part_buses: BTreeMap::new(),
717 instrument_buses: BTreeMap::new(),
718 aux_sends: Vec::new(),
719 effect_routes: Vec::new(),
720 }
721 }
722}
723
724#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
726pub struct PlaybackBusRoute {
727 pub part_index: usize,
728 pub channel: u8,
729 pub program: u8,
730 #[serde(default, skip_serializing_if = "Option::is_none")]
731 pub instrument_id: Option<String>,
732 pub is_metronome: bool,
733 pub bus_id: String,
734}
735
736#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
738pub struct PlaybackRoutingManifest {
739 pub contract_version: u16,
740 #[serde(default, skip_serializing_if = "Option::is_none")]
741 pub provider_identity: Option<String>,
742 pub master_bus: String,
743 pub metronome_bus: String,
744 pub routes: Vec<PlaybackBusRoute>,
745 pub aux_sends: Vec<PlaybackAuxSend>,
746 pub effect_routes: Vec<PlaybackEffectRoute>,
747}
748
749pub const PLAYBACK_ROUTING_CONTRACT_VERSION: u16 = 1;
751
752fn valid_routing_id(value: &str) -> bool {
753 !value.is_empty()
754 && value.len() <= 64
755 && value
756 .bytes()
757 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
758}
759
760fn validate_playback_routing(config: &PlaybackRoutingConfig) -> Result<(), crate::Error> {
761 if !valid_routing_id(&config.master_bus)
762 || !valid_routing_id(&config.metronome_bus)
763 || config.part_buses.values().any(|bus| !valid_routing_id(bus))
764 || config
765 .instrument_buses
766 .values()
767 .any(|bus| !valid_routing_id(bus))
768 || config.aux_sends.len() > 64
769 || config.effect_routes.len() > 128
770 {
771 return Err(crate::Error::InvalidPlaybackRouting);
772 }
773 for send in &config.aux_sends {
774 if !valid_routing_id(&send.source_bus)
775 || !valid_routing_id(&send.destination_bus)
776 || !send.gain_db.is_finite()
777 || !(-120.0..=24.0).contains(&send.gain_db)
778 {
779 return Err(crate::Error::InvalidPlaybackRouting);
780 }
781 }
782 if config
783 .effect_routes
784 .iter()
785 .any(|effect| !valid_routing_id(&effect.bus_id) || !valid_routing_id(&effect.effect_id))
786 {
787 return Err(crate::Error::InvalidPlaybackRouting);
788 }
789 Ok(())
790}
791
792pub fn build_playback_routing_manifest(
794 score: &Score,
795 playback_options: &PlaybackOptions,
796 config: &PlaybackRoutingConfig,
797) -> Result<PlaybackRoutingManifest, crate::Error> {
798 validate_playback_routing(config)?;
799 let events = to_playback_events_bounded(score, playback_options)?;
800 let mut routes = BTreeMap::new();
801 for event in events {
802 let bus_id = if event.is_metronome {
803 config.metronome_bus.clone()
804 } else if let Some(bus) = event
805 .instrument_id
806 .as_ref()
807 .and_then(|instrument_id| config.instrument_buses.get(instrument_id))
808 {
809 bus.clone()
810 } else if let Some(bus) = config.part_buses.get(&event.part_index) {
811 bus.clone()
812 } else {
813 config.master_bus.clone()
814 };
815 let route = PlaybackBusRoute {
816 part_index: event.part_index,
817 channel: event.channel,
818 program: event.program,
819 instrument_id: event.instrument_id,
820 is_metronome: event.is_metronome,
821 bus_id,
822 };
823 routes.insert(route.clone(), route);
824 }
825 Ok(PlaybackRoutingManifest {
826 contract_version: PLAYBACK_ROUTING_CONTRACT_VERSION,
827 provider_identity: config.provider_identity.clone(),
828 master_bus: config.master_bus.clone(),
829 metronome_bus: config.metronome_bus.clone(),
830 routes: routes.into_values().collect(),
831 aux_sends: config.aux_sends.clone(),
832 effect_routes: config.effect_routes.clone(),
833 })
834}
835
836pub fn build_offline_render_manifest(
838 score: &Score,
839 playback_options: &PlaybackOptions,
840 request: &OfflineRenderRequest,
841) -> Result<OfflineRenderManifest, crate::Error> {
842 if !(8_000..=384_000).contains(&request.sample_rate_hz) || !(1..=8).contains(&request.channels)
843 {
844 return Err(crate::Error::InvalidOfflineRenderRequest);
845 }
846 let resolved_score = match request.view_id.as_deref() {
847 Some(view_id) => score.resolve_view(view_id)?,
848 None => score.clone(),
849 };
850 let mut effective_options = request
851 .playback_options
852 .clone()
853 .unwrap_or_else(|| playback_options.clone());
854 if let OfflineRenderScope::MeasureRange { start, end } = request.scope {
855 if start > end {
856 return Err(crate::Error::InvalidOfflineRenderRequest);
857 }
858 effective_options.loop_region = Some((start, end));
859 }
860 let mut events = to_playback_events_bounded(&resolved_score, &effective_options)?;
861 let mut diagnostics = Vec::new();
862 let mut selection_origin_secs = 0.0;
863 if let OfflineRenderScope::Selection { ref addresses } = request.scope {
864 if addresses.is_empty() {
865 return Err(crate::Error::InvalidOfflineRenderRequest);
866 }
867 for address in addresses {
868 if !events
869 .iter()
870 .any(|event| event.source.as_ref() == Some(address))
871 {
872 diagnostics.push(OfflineRenderDiagnostic {
873 kind: OfflineRenderDiagnosticKind::UnresolvedSelectionAddress,
874 source: Some(address.clone()),
875 });
876 }
877 }
878 events.retain(|event| {
879 event
880 .source
881 .as_ref()
882 .is_some_and(|source| addresses.contains(source))
883 });
884 let origin_secs = events
885 .iter()
886 .map(|event| event.time_secs)
887 .fold(f64::INFINITY, f64::min);
888 if origin_secs.is_finite() {
889 selection_origin_secs = origin_secs;
890 for event in &mut events {
891 event.time_secs -= origin_secs;
892 }
893 }
894 if events.is_empty() {
895 diagnostics.push(OfflineRenderDiagnostic {
896 kind: OfflineRenderDiagnosticKind::EmptySelection,
897 source: None,
898 });
899 }
900 }
901 let duration_secs = events
902 .iter()
903 .map(|event| event.time_secs + event.duration_secs)
904 .fold(0.0_f64, f64::max);
905 if !duration_secs.is_finite() || duration_secs < 0.0 {
906 return Err(crate::Error::InvalidOfflineRenderRequest);
907 }
908 let release_tail_frames =
909 (f64::from(request.release_tail_millis) * f64::from(request.sample_rate_hz) / 1_000.0)
910 .ceil();
911 let duration_frames = (duration_secs * f64::from(request.sample_rate_hz)).ceil();
912 if !duration_frames.is_finite() || duration_frames > u64::MAX as f64 {
913 return Err(crate::Error::InvalidOfflineRenderRequest);
914 }
915 if !release_tail_frames.is_finite()
916 || release_tail_frames > u64::MAX as f64
917 || duration_frames > (u64::MAX as f64 - release_tail_frames)
918 {
919 return Err(crate::Error::InvalidOfflineRenderRequest);
920 }
921 let frame_events = events
922 .iter()
923 .cloned()
924 .map(|event| {
925 let start_frame = (event.time_secs * f64::from(request.sample_rate_hz)).round() as u64;
926 let end_frame = ((event.time_secs + event.duration_secs)
927 * f64::from(request.sample_rate_hz))
928 .ceil() as u64;
929 OfflineRenderFrameEvent {
930 duration_frames: end_frame.saturating_sub(start_frame),
931 event,
932 start_frame,
933 end_frame,
934 }
935 })
936 .collect::<Vec<_>>();
937 let mut semantic_events = Vec::new();
938 for event in &frame_events {
939 let source = event.event.source.clone();
940 let timed_pedal_at_note = source
941 .as_ref()
942 .and_then(|address| {
943 let measure = resolved_score
944 .parts
945 .get(address.part)?
946 .staves
947 .get(address.staff)?
948 .measures
949 .get(address.measure)?;
950 let voice = measure.voices.get(address.voice)?;
951 let onset: f64 = voice.iter().take(address.note).map(Note::beats).sum();
952 Some(
953 measure
954 .timed_pedals
955 .iter()
956 .any(|pedal| pedal.down && (pedal.position_beats - onset).abs() < 1e-9),
957 )
958 })
959 .unwrap_or(false);
960 if event.event.pedal && !timed_pedal_at_note {
961 semantic_events.push(OfflineRenderSemanticFrameEvent {
962 kind: OfflineRenderSemanticEventKind::Pedal { down: true },
963 frame: event.start_frame,
964 source: source.clone(),
965 measure: None,
966 });
967 }
968 if source.as_ref().is_some_and(|address| {
969 resolved_score
970 .parts
971 .get(address.part)
972 .and_then(|part| part.staves.get(address.staff))
973 .and_then(|staff| staff.measures.get(address.measure))
974 .and_then(|measure| {
975 measure
976 .voices
977 .get(address.voice)
978 .map(|voice| (measure, voice))
979 })
980 .is_some_and(|(measure, voice)| {
981 let onset: f64 = voice.iter().take(address.note).map(Note::beats).sum();
982 voice.get(address.note).is_some_and(|note| note.pedal_end)
983 && !measure
984 .timed_pedals
985 .iter()
986 .any(|pedal| !pedal.down && (pedal.position_beats - onset).abs() < 1e-9)
987 })
988 }) {
989 semantic_events.push(OfflineRenderSemanticFrameEvent {
990 kind: OfflineRenderSemanticEventKind::Pedal { down: false },
991 frame: event.start_frame,
992 source: source.clone(),
993 measure: None,
994 });
995 }
996 for articulation in &event.event.articulations {
997 semantic_events.push(OfflineRenderSemanticFrameEvent {
998 kind: OfflineRenderSemanticEventKind::Articulation {
999 articulation: articulation.clone(),
1000 },
1001 frame: event.start_frame,
1002 source: source.clone(),
1003 measure: None,
1004 });
1005 }
1006 if !event.event.pitch_bend_curve.is_empty() {
1007 semantic_events.push(OfflineRenderSemanticFrameEvent {
1008 kind: OfflineRenderSemanticEventKind::GuitarBend {
1009 points: event.event.pitch_bend_curve.clone(),
1010 },
1011 frame: event.start_frame,
1012 source,
1013 measure: None,
1014 });
1015 }
1016 }
1017 append_rest_pedal_events(
1018 &resolved_score,
1019 &effective_options,
1020 request.sample_rate_hz,
1021 &request.scope,
1022 selection_origin_secs,
1023 &mut semantic_events,
1024 );
1025 append_measure_semantic_events(
1026 &resolved_score,
1027 &effective_options,
1028 request.sample_rate_hz,
1029 &request.scope,
1030 selection_origin_secs,
1031 &mut semantic_events,
1032 );
1033 let semantic_end_frame = semantic_events
1037 .iter()
1038 .map(|event| event.frame)
1039 .max()
1040 .unwrap_or(0);
1041 let content_duration_frames = (duration_frames as u64).max(semantic_end_frame);
1042 if release_tail_frames > 0.0 {
1043 semantic_events.push(OfflineRenderSemanticFrameEvent {
1044 kind: OfflineRenderSemanticEventKind::ReleaseTail,
1045 frame: content_duration_frames,
1046 source: None,
1047 measure: None,
1048 });
1049 }
1050 let manifest = OfflineRenderManifest {
1051 contract_version: OFFLINE_RENDER_CONTRACT_VERSION,
1052 playback_contract_version: PLAYBACK_COMPARISON_CONTRACT_VERSION,
1053 format: request.format,
1054 sample_rate_hz: request.sample_rate_hz,
1055 channels: request.channels,
1056 provider_identity: request.provider_identity.clone(),
1057 asset_identity: request.asset_identity.clone(),
1058 view_id: request.view_id.clone(),
1059 scope: request.scope.clone(),
1060 navigation_policy: request.navigation_policy,
1061 release_tail_frames: release_tail_frames as u64,
1062 playback_options: effective_options,
1063 duration_frames: content_duration_frames + release_tail_frames as u64,
1064 events,
1065 frame_events,
1066 semantic_events,
1067 diagnostics,
1068 };
1069 manifest.validate()?;
1070 Ok(manifest)
1071}
1072
1073#[derive(Clone, Copy)]
1074struct OfflineMeasureTime {
1075 measure: usize,
1076 start_secs: f64,
1077 bpm: f64,
1078 ramp_to_bpm: Option<f64>,
1079 beats: f64,
1080}
1081
1082fn append_rest_pedal_events(
1085 score: &Score,
1086 options: &PlaybackOptions,
1087 sample_rate_hz: u32,
1088 scope: &OfflineRenderScope,
1089 origin_secs: f64,
1090 events: &mut Vec<OfflineRenderSemanticFrameEvent>,
1091) {
1092 let marks = score_tempo_marks(score);
1093 let holds = score_fermata_holds(score, options);
1094 let swing = options
1095 .swing
1096 .filter(|ratio| ratio.is_finite() && *ratio > 0.5 && *ratio < 1.0);
1097 for (part_index, part) in score.parts.iter().enumerate() {
1098 if options.muted_parts.contains(&part_index) {
1099 continue;
1100 }
1101 for (staff_index, staff) in part.staves.iter().enumerate() {
1102 let timeline = offline_measure_times(score, options, part_index, staff_index, &marks);
1103 for time in timeline {
1104 let measure = &staff.measures[time.measure];
1105 let meter = staff
1106 .meter_at(time.measure, &score.settings.time_signature)
1107 .total_beats();
1108 let grid_offset = if time.measure == 0 && time.beats < meter {
1109 (meter - time.beats).max(0.0)
1110 } else {
1111 0.0
1112 };
1113 for (voice_index, voice) in measure.voices.iter().enumerate() {
1114 let mut local = 0.0;
1115 let mut written = 0.0;
1116 let mut swing_second = false;
1117 for (note_index, note) in voice.iter().enumerate() {
1118 if note.is_grace || note.is_cue {
1119 continue;
1120 }
1121 let address = NoteAddr {
1122 part: part_index,
1123 staff: staff_index,
1124 measure: time.measure,
1125 voice: voice_index,
1126 note: note_index,
1127 };
1128 let selected = match scope {
1129 OfflineRenderScope::Selection { addresses } => {
1130 addresses.contains(&address)
1131 }
1132 _ => true,
1133 };
1134 if note.is_rest && selected && (note.pedal_start || note.pedal_end) {
1135 let secs = time.start_secs
1136 + tempo_ramp_seconds(time.bpm, time.ramp_to_bpm, time.beats, local);
1137 let frame = ((fermata_warp_secs(secs, &holds) - origin_secs).max(0.0)
1138 * f64::from(sample_rate_hz))
1139 .round() as u64;
1140 for (present, down) in
1142 [(note.pedal_end, false), (note.pedal_start, true)]
1143 {
1144 if present
1145 && !measure.timed_pedals.iter().any(|pedal| {
1146 pedal.down == down
1147 && (pedal.position_beats - written).abs() < 1e-9
1148 })
1149 {
1150 events.push(OfflineRenderSemanticFrameEvent {
1151 kind: OfflineRenderSemanticEventKind::Pedal { down },
1152 frame,
1153 source: Some(address.clone()),
1154 measure: None,
1155 });
1156 }
1157 }
1158 }
1159 let dur = if let Some(ratio) = swing {
1160 if swing_second {
1161 swing_second = false;
1162 (1.0 - ratio) * note.beats() * 2.0
1163 } else if starts_swing_pair(
1164 voice,
1165 note_index,
1166 written,
1167 grid_offset,
1168 time.beats,
1169 &options.swing_unit,
1170 ) {
1171 swing_second = true;
1172 ratio * note.beats() * 2.0
1173 } else {
1174 note.beats()
1175 }
1176 } else {
1177 note.beats()
1178 };
1179 written += note.beats();
1180 local += dur;
1181 }
1182 }
1183 }
1184 }
1185 }
1186}
1187
1188fn offline_measure_times(
1189 score: &Score,
1190 options: &PlaybackOptions,
1191 part: usize,
1192 staff: usize,
1193 tempo_marks: &[ScoreTempoMark],
1194) -> Vec<OfflineMeasureTime> {
1195 let Some(staff_ref) = score
1196 .parts
1197 .get(part)
1198 .and_then(|part| part.staves.get(staff))
1199 else {
1200 return Vec::new();
1201 };
1202 let sequence = measure_sequence(score).into_iter().filter(|measure| {
1203 options
1204 .loop_region
1205 .is_none_or(|(start, end)| *measure >= start && *measure <= end)
1206 });
1207 let mut bpm = initial_tempo_bpm(score, options);
1208 let mut secs = 0.0;
1209 let mut result = Vec::new();
1210 for measure_index in sequence {
1211 if staff_ref.measures.get(measure_index).is_none() {
1212 continue;
1213 }
1214 let mark = tempo_marks.get(measure_index).copied().unwrap_or_default();
1215 if let Some(tempo) = mark.bpm.and_then(|tempo| written_tempo_bpm(options, tempo)) {
1216 bpm = tempo;
1217 }
1218 let beats = score.playback_measure_beats(measure_index);
1219 let ramp_to_bpm = written_tempo_ramp_bpm(options, mark.ramp_to_bpm);
1220 result.push(OfflineMeasureTime {
1221 measure: measure_index,
1222 start_secs: secs,
1223 bpm,
1224 ramp_to_bpm,
1225 beats,
1226 });
1227 secs += tempo_ramp_seconds(bpm, ramp_to_bpm, beats, beats);
1228 if let Some(end_bpm) = ramp_to_bpm {
1229 bpm = end_bpm;
1230 }
1231 }
1232 result
1233}
1234
1235fn append_measure_semantic_events(
1236 score: &Score,
1237 options: &PlaybackOptions,
1238 sample_rate_hz: u32,
1239 scope: &OfflineRenderScope,
1240 origin_secs: f64,
1241 events: &mut Vec<OfflineRenderSemanticFrameEvent>,
1242) {
1243 let tempo_marks = score_tempo_marks(score);
1244 let holds = score_fermata_holds(score, options);
1245 for (part_index, part) in score.parts.iter().enumerate() {
1246 let timeline = offline_measure_times(score, options, part_index, 0, &tempo_marks);
1247 let Some(staff) = part.staves.first() else {
1248 continue;
1249 };
1250 let mut tick_starts = Vec::with_capacity(staff.measures.len());
1251 let mut tick = 0u64;
1252 for measure_index in 0..staff.measures.len() {
1253 tick_starts.push(tick);
1254 let beats = score.playback_measure_beats(measure_index);
1255 tick = tick.saturating_add((beats * 480.0).round() as u64);
1256 }
1257 for time in timeline {
1258 if let OfflineRenderScope::Selection { addresses } = scope
1259 && !addresses
1260 .iter()
1261 .any(|address| address.part == part_index && address.measure == time.measure)
1262 {
1263 continue;
1264 }
1265 let address = OfflineRenderMeasureAddress {
1266 part: part_index,
1267 staff: 0,
1268 measure: time.measure,
1269 };
1270 let frame = ((fermata_warp_secs(time.start_secs, &holds) - origin_secs).max(0.0)
1271 * f64::from(sample_rate_hz))
1272 .round() as u64;
1273 let measure = &staff.measures[time.measure];
1274 let mark = tempo_marks.get(time.measure).copied().unwrap_or_default();
1275 if part_index == 0
1276 && options.bpm_override.is_none()
1277 && (mark.bpm.is_some() || mark.ramp_to_bpm.is_some())
1278 {
1279 events.push(OfflineRenderSemanticFrameEvent {
1280 kind: OfflineRenderSemanticEventKind::Tempo {
1281 bpm: semantic_tempo_bpm(time.bpm),
1282 ramp_to_bpm: time.ramp_to_bpm.map(semantic_tempo_bpm),
1283 },
1284 frame,
1285 source: None,
1286 measure: Some(address.clone()),
1287 });
1288 }
1289 if part_index == 0
1290 && let Some(marker) = &measure.navigation
1291 {
1292 events.push(OfflineRenderSemanticFrameEvent {
1293 kind: OfflineRenderSemanticEventKind::Navigation {
1294 marker: marker.clone(),
1295 },
1296 frame,
1297 source: None,
1298 measure: Some(address.clone()),
1299 });
1300 }
1301 if let Some(instrument) = &measure.instrument_change {
1302 events.push(OfflineRenderSemanticFrameEvent {
1303 kind: OfflineRenderSemanticEventKind::InstrumentChange {
1304 instrument_id: instrument.id.clone(),
1305 channel: instrument.midi_channel,
1306 program: instrument.midi_program,
1307 transpose_semitones: instrument.transpose_semitones,
1308 },
1309 frame,
1310 source: None,
1311 measure: Some(address.clone()),
1312 });
1313 }
1314 for (staff_index, event) in part
1315 .staves
1316 .iter()
1317 .enumerate()
1318 .filter_map(|(staff_index, staff)| {
1319 staff
1320 .measures
1321 .get(time.measure)
1322 .map(|measure| (staff_index, measure))
1323 })
1324 .flat_map(|(staff_index, measure)| {
1325 measure
1326 .timed_pedals
1327 .iter()
1328 .map(move |event| (staff_index, event))
1329 })
1330 {
1331 if !event.position_beats.is_finite()
1332 || event.position_beats < 0.0
1333 || event.position_beats > time.beats
1334 {
1335 continue;
1336 }
1337 let secs = time.start_secs
1338 + tempo_ramp_seconds(
1339 time.bpm,
1340 time.ramp_to_bpm,
1341 time.beats,
1342 event.position_beats,
1343 );
1344 events.push(OfflineRenderSemanticFrameEvent {
1345 kind: OfflineRenderSemanticEventKind::Pedal { down: event.down },
1346 frame: ((fermata_warp_secs(secs, &holds) - origin_secs).max(0.0)
1347 * f64::from(sample_rate_hz))
1348 .round() as u64,
1349 source: None,
1350 measure: Some(OfflineRenderMeasureAddress {
1351 part: part_index,
1352 staff: staff_index,
1353 measure: time.measure,
1354 }),
1355 });
1356 }
1357 let start_tick = tick_starts[time.measure];
1358 let end_tick = start_tick.saturating_add((time.beats * 480.0).round() as u64);
1359 for control in &part.midi_control_changes {
1360 if control.tick < start_tick || control.tick >= end_tick {
1361 continue;
1362 }
1363 let local_beats = (control.tick - start_tick) as f64 / 480.0;
1364 let secs = time.start_secs
1365 + tempo_ramp_seconds(time.bpm, time.ramp_to_bpm, time.beats, local_beats);
1366 events.push(OfflineRenderSemanticFrameEvent {
1367 kind: OfflineRenderSemanticEventKind::Controller {
1368 channel: control.channel,
1369 controller: control.controller,
1370 value: control.value,
1371 },
1372 frame: ((fermata_warp_secs(secs, &holds) - origin_secs).max(0.0)
1373 * f64::from(sample_rate_hz))
1374 .round() as u64,
1375 source: None,
1376 measure: Some(address.clone()),
1377 });
1378 }
1379 for bend in &part.midi_pitch_bends {
1380 if bend.tick < start_tick || bend.tick >= end_tick {
1381 continue;
1382 }
1383 let local_beats = (bend.tick - start_tick) as f64 / 480.0;
1384 let secs = time.start_secs
1385 + tempo_ramp_seconds(time.bpm, time.ramp_to_bpm, time.beats, local_beats);
1386 events.push(OfflineRenderSemanticFrameEvent {
1387 kind: OfflineRenderSemanticEventKind::PitchBend {
1388 channel: bend.channel,
1389 value: bend.value,
1390 },
1391 frame: ((fermata_warp_secs(secs, &holds) - origin_secs).max(0.0)
1392 * f64::from(sample_rate_hz))
1393 .round() as u64,
1394 source: None,
1395 measure: Some(address.clone()),
1396 });
1397 }
1398 for program in &part.midi_program_changes {
1399 if program.tick < start_tick || program.tick >= end_tick {
1400 continue;
1401 }
1402 let local_beats = (program.tick - start_tick) as f64 / 480.0;
1403 let secs = time.start_secs
1404 + tempo_ramp_seconds(time.bpm, time.ramp_to_bpm, time.beats, local_beats);
1405 events.push(OfflineRenderSemanticFrameEvent {
1406 kind: OfflineRenderSemanticEventKind::ProgramChange {
1407 channel: program.channel,
1408 program: program.program,
1409 },
1410 frame: ((fermata_warp_secs(secs, &holds) - origin_secs).max(0.0)
1411 * f64::from(sample_rate_hz))
1412 .round() as u64,
1413 source: None,
1414 measure: Some(address.clone()),
1415 });
1416 }
1417 for aftertouch in &part.midi_aftertouch {
1418 if aftertouch.tick < start_tick || aftertouch.tick >= end_tick {
1419 continue;
1420 }
1421 let local_beats = (aftertouch.tick - start_tick) as f64 / 480.0;
1422 let secs = time.start_secs
1423 + tempo_ramp_seconds(time.bpm, time.ramp_to_bpm, time.beats, local_beats);
1424 events.push(OfflineRenderSemanticFrameEvent {
1425 kind: OfflineRenderSemanticEventKind::Aftertouch {
1426 channel: aftertouch.channel,
1427 key: aftertouch.key,
1428 value: aftertouch.value,
1429 },
1430 frame: ((fermata_warp_secs(secs, &holds) - origin_secs).max(0.0)
1431 * f64::from(sample_rate_hz))
1432 .round() as u64,
1433 source: None,
1434 measure: Some(address.clone()),
1435 });
1436 }
1437 }
1438 }
1439 events.sort_by_key(|event| event.frame);
1440}
1441
1442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1444pub struct PlaybackTimingTolerance {
1445 pub start_secs: f64,
1446 pub duration_secs: f64,
1447}
1448
1449impl Default for PlaybackTimingTolerance {
1450 fn default() -> Self {
1451 Self {
1452 start_secs: 0.005,
1453 duration_secs: 0.005,
1454 }
1455 }
1456}
1457
1458#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1460pub enum PlaybackTimingMismatch {
1461 EventCount { expected: usize, actual: usize },
1462 EventIdentity { index: usize },
1463 StartTime { index: usize, error_secs: f64 },
1464 Duration { index: usize, error_secs: f64 },
1465}
1466
1467#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1469pub struct PlaybackTimingReport {
1470 pub contract_version: u16,
1471 pub expected_events: usize,
1472 pub actual_events: usize,
1473 pub matched_events: usize,
1474 pub max_start_error_secs: f64,
1475 pub max_duration_error_secs: f64,
1476 pub within_tolerance: bool,
1477 pub mismatches: Vec<PlaybackTimingMismatch>,
1478}
1479
1480pub fn compare_playback_timing(
1485 expected: &[PlaybackEvent],
1486 actual: &[PlaybackEvent],
1487 tolerance: &PlaybackTimingTolerance,
1488) -> Result<PlaybackTimingReport, crate::Error> {
1489 if !tolerance.start_secs.is_finite()
1490 || tolerance.start_secs < 0.0
1491 || !tolerance.duration_secs.is_finite()
1492 || tolerance.duration_secs < 0.0
1493 {
1494 return Err(crate::Error::InvalidPlaybackComparison);
1495 }
1496 if expected.len() > MAX_PLAYBACK_COMPARISON_EVENTS {
1497 return Err(crate::Error::PlaybackComparisonTooLarge(expected.len()));
1498 }
1499 if actual.len() > MAX_PLAYBACK_COMPARISON_EVENTS {
1500 return Err(crate::Error::PlaybackComparisonTooLarge(actual.len()));
1501 }
1502 let mut mismatches = Vec::new();
1503 if expected.len() != actual.len() {
1504 mismatches.push(PlaybackTimingMismatch::EventCount {
1505 expected: expected.len(),
1506 actual: actual.len(),
1507 });
1508 }
1509 let mut matched_events = 0;
1510 let mut max_start_error_secs: f64 = 0.0;
1511 let mut max_duration_error_secs: f64 = 0.0;
1512 for (index, (expected_event, actual_event)) in expected.iter().zip(actual).enumerate() {
1513 let source_identity_matches = match (&expected_event.source, &actual_event.source) {
1514 (Some(expected), Some(actual)) => expected == actual,
1515 _ => expected_event.address == actual_event.address,
1516 };
1517 let identity_matches = source_identity_matches
1518 && expected_event.pitch_midi_cents == actual_event.pitch_midi_cents
1519 && expected_event.velocity == actual_event.velocity
1520 && expected_event.part_index == actual_event.part_index
1521 && expected_event.channel == actual_event.channel
1522 && expected_event.program == actual_event.program
1523 && expected_event.instrument_id == actual_event.instrument_id
1524 && expected_event.pitch_bend_curve == actual_event.pitch_bend_curve
1525 && expected_event.post_note_pause_beats == actual_event.post_note_pause_beats
1526 && expected_event.articulations == actual_event.articulations
1527 && expected_event.chord_symbol == actual_event.chord_symbol
1528 && expected_event.guitar_technique == actual_event.guitar_technique
1529 && expected_event.is_metronome == actual_event.is_metronome;
1530 let start_error_secs = (expected_event.time_secs - actual_event.time_secs).abs();
1531 let duration_error_secs = (expected_event.duration_secs - actual_event.duration_secs).abs();
1532 if start_error_secs.is_finite() {
1533 max_start_error_secs = max_start_error_secs.max(start_error_secs);
1534 }
1535 if duration_error_secs.is_finite() {
1536 max_duration_error_secs = max_duration_error_secs.max(duration_error_secs);
1537 }
1538 let start_matches =
1539 start_error_secs.is_finite() && start_error_secs <= tolerance.start_secs;
1540 let duration_matches =
1541 duration_error_secs.is_finite() && duration_error_secs <= tolerance.duration_secs;
1542 if identity_matches && start_matches && duration_matches {
1543 matched_events += 1;
1544 continue;
1545 }
1546 if mismatches.len() < MAX_PLAYBACK_MISMATCHES {
1547 if !identity_matches {
1548 mismatches.push(PlaybackTimingMismatch::EventIdentity { index });
1549 }
1550 if !start_matches && mismatches.len() < MAX_PLAYBACK_MISMATCHES {
1551 mismatches.push(PlaybackTimingMismatch::StartTime {
1552 index,
1553 error_secs: start_error_secs,
1554 });
1555 }
1556 if !duration_matches && mismatches.len() < MAX_PLAYBACK_MISMATCHES {
1557 mismatches.push(PlaybackTimingMismatch::Duration {
1558 index,
1559 error_secs: duration_error_secs,
1560 });
1561 }
1562 }
1563 }
1564 Ok(PlaybackTimingReport {
1565 contract_version: PLAYBACK_COMPARISON_CONTRACT_VERSION,
1566 expected_events: expected.len(),
1567 actual_events: actual.len(),
1568 matched_events,
1569 max_start_error_secs,
1570 max_duration_error_secs,
1571 within_tolerance: mismatches.is_empty(),
1572 mismatches,
1573 })
1574}
1575
1576pub const PLAYBACK_TIMING_CORPUS_CONTRACT_VERSION: u16 = 1;
1578const MAX_PLAYBACK_TIMING_CORPUS_CASES: usize = 256;
1579
1580#[derive(Debug, Clone, Serialize, Deserialize)]
1582pub struct PlaybackTimingCase {
1583 pub id: String,
1584 pub score: Score,
1585 #[serde(default)]
1586 pub options: PlaybackOptions,
1587 pub expected_events: Vec<PlaybackEvent>,
1588 #[serde(default)]
1589 pub tolerance: PlaybackTimingTolerance,
1590}
1591
1592#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1594pub struct PlaybackTimingCaseReport {
1595 pub id: String,
1596 pub report: PlaybackTimingReport,
1597}
1598
1599#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1601pub struct PlaybackTimingCorpusReport {
1602 pub contract_version: u16,
1603 pub total_cases: usize,
1604 pub passed_cases: usize,
1605 pub cases: Vec<PlaybackTimingCaseReport>,
1606}
1607
1608pub fn evaluate_playback_timing_corpus(
1613 cases: &[PlaybackTimingCase],
1614) -> Result<PlaybackTimingCorpusReport, crate::Error> {
1615 if cases.is_empty() || cases.len() > MAX_PLAYBACK_TIMING_CORPUS_CASES {
1616 return Err(crate::Error::InvalidPlaybackTimingCorpus);
1617 }
1618 let mut ids = BTreeSet::new();
1619 let mut reports = Vec::with_capacity(cases.len());
1620 let mut passed_cases = 0;
1621 for case in cases {
1622 if !valid_routing_id(&case.id) || !ids.insert(case.id.clone()) {
1623 return Err(crate::Error::InvalidPlaybackTimingCorpus);
1624 }
1625 let actual = to_playback_events_bounded(&case.score, &case.options)?;
1626 let report = compare_playback_timing(&case.expected_events, &actual, &case.tolerance)?;
1627 if report.within_tolerance {
1628 passed_cases += 1;
1629 }
1630 reports.push(PlaybackTimingCaseReport {
1631 id: case.id.clone(),
1632 report,
1633 });
1634 }
1635 Ok(PlaybackTimingCorpusReport {
1636 contract_version: PLAYBACK_TIMING_CORPUS_CONTRACT_VERSION,
1637 total_cases: cases.len(),
1638 passed_cases,
1639 cases: reports,
1640 })
1641}
1642
1643pub const TAB_PERFORMANCE_CONTRACT_VERSION: u16 = 4;
1645
1646#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1647pub struct TablaturePerformanceEvent {
1648 pub playback: PlaybackEvent,
1649 pub string: u8,
1650 pub fret: u8,
1651 #[serde(default)]
1653 pub technique: Option<GuitarTechnique>,
1654 #[serde(default)]
1656 pub bend_alter_cents: Option<i16>,
1657 #[serde(default)]
1660 pub bend_curve: Vec<GuitarBendPoint>,
1661 pub expected_pitch_midi_cents: i32,
1662 pub pitch_error_cents: i32,
1663}
1664
1665#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1666pub enum TablaturePerformanceDiagnostic {
1667 NoTablatureStaff {
1668 address: String,
1669 },
1670 MissingPosition {
1671 address: String,
1672 pitch_index: usize,
1673 },
1674 StringOutOfRange {
1675 address: String,
1676 string: u8,
1677 lines: u8,
1678 },
1679 TuningUnavailable {
1680 address: String,
1681 string: u8,
1682 },
1683 PitchMismatch {
1684 address: String,
1685 pitch_index: usize,
1686 error_cents: i32,
1687 },
1688}
1689
1690#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1691pub struct TablaturePerformanceReport {
1692 pub contract_version: u16,
1693 pub events: Vec<TablaturePerformanceEvent>,
1694 pub diagnostics: Vec<TablaturePerformanceDiagnostic>,
1695}
1696
1697pub const TAB_ROUND_TRIP_CONTRACT_VERSION: u16 = 1;
1699
1700#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1701pub struct TablatureRoundTripReport {
1702 pub contract_version: u16,
1703 pub checked_notes: usize,
1704 pub positioned_notes: usize,
1705 pub equivalent: bool,
1706 pub diagnostics: Vec<TablatureRoundTripDiagnostic>,
1707}
1708
1709#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1710pub enum TablatureRoundTripDiagnostic {
1711 StructureMismatch {
1712 address: String,
1713 },
1714 TablatureConfigMismatch {
1715 address: String,
1716 },
1717 PositionMismatch {
1718 address: String,
1719 pitch_index: usize,
1720 expected: Option<TabPosition>,
1721 actual: Option<TabPosition>,
1722 },
1723}
1724
1725pub fn tablature_round_trip_report(
1730 score: &Score,
1731) -> Result<TablatureRoundTripReport, crate::Error> {
1732 let encoded = serde_json::to_string(score)
1733 .map_err(|error| crate::Error::TabRoundTripSerialization(error.to_string()))?;
1734 let restored: Score = serde_json::from_str(&encoded)
1735 .map_err(|error| crate::Error::TabRoundTripSerialization(error.to_string()))?;
1736 let mut checked_notes = 0;
1737 let mut positioned_notes = 0;
1738 let mut diagnostics = Vec::new();
1739 for (part_index, part) in score.parts.iter().enumerate() {
1740 let Some(restored_part) = restored.parts.get(part_index) else {
1741 diagnostics.push(TablatureRoundTripDiagnostic::StructureMismatch {
1742 address: format!("{part_index}"),
1743 });
1744 continue;
1745 };
1746 for (staff_index, staff) in part.staves.iter().enumerate() {
1747 let address = format!("{part_index}:{staff_index}");
1748 let Some(restored_staff) = restored_part.staves.get(staff_index) else {
1749 diagnostics.push(TablatureRoundTripDiagnostic::StructureMismatch { address });
1750 continue;
1751 };
1752 if staff.tablature != restored_staff.tablature {
1753 diagnostics.push(TablatureRoundTripDiagnostic::TablatureConfigMismatch {
1754 address: address.clone(),
1755 });
1756 }
1757 for (measure_index, measure) in staff.measures.iter().enumerate() {
1758 let Some(restored_measure) = restored_staff.measures.get(measure_index) else {
1759 diagnostics.push(TablatureRoundTripDiagnostic::StructureMismatch {
1760 address: format!("{address}:{measure_index}"),
1761 });
1762 continue;
1763 };
1764 for (voice_index, voice) in measure.voices.iter().enumerate() {
1765 let Some(restored_voice) = restored_measure.voices.get(voice_index) else {
1766 diagnostics.push(TablatureRoundTripDiagnostic::StructureMismatch {
1767 address: format!("{address}:{measure_index}:{voice_index}"),
1768 });
1769 continue;
1770 };
1771 for (note_index, note) in voice.iter().enumerate() {
1772 checked_notes += 1;
1773 if !note.tab_positions.is_empty() || note.tab_position.is_some() {
1774 positioned_notes += 1;
1775 }
1776 let note_address =
1777 format!("{address}:{measure_index}:{voice_index}:{note_index}");
1778 let Some(restored_note) = restored_voice.get(note_index) else {
1779 diagnostics.push(TablatureRoundTripDiagnostic::StructureMismatch {
1780 address: note_address,
1781 });
1782 continue;
1783 };
1784 let pitch_count = note.pitches.len().max(note.tab_positions.len()).max(1);
1785 for pitch_index in 0..pitch_count {
1786 let expected = note
1787 .tab_positions
1788 .get(pitch_index)
1789 .cloned()
1790 .or_else(|| note.tab_position.clone());
1791 let actual = restored_note
1792 .tab_positions
1793 .get(pitch_index)
1794 .cloned()
1795 .or_else(|| restored_note.tab_position.clone());
1796 if expected != actual {
1797 diagnostics.push(TablatureRoundTripDiagnostic::PositionMismatch {
1798 address: note_address.clone(),
1799 pitch_index,
1800 expected,
1801 actual,
1802 });
1803 }
1804 }
1805 }
1806 }
1807 }
1808 }
1809 }
1810 Ok(TablatureRoundTripReport {
1811 contract_version: TAB_ROUND_TRIP_CONTRACT_VERSION,
1812 checked_notes,
1813 positioned_notes,
1814 equivalent: diagnostics.is_empty(),
1815 diagnostics,
1816 })
1817}
1818
1819pub fn project_tablature_performance(
1825 score: &Score,
1826 options: &PlaybackOptions,
1827) -> Result<TablaturePerformanceReport, crate::Error> {
1828 let playback = to_playback_events(score, options);
1829 if playback.len() > MAX_TAB_PERFORMANCE_EVENTS {
1830 return Err(crate::Error::TabPerformanceTooLarge(playback.len()));
1831 }
1832 let mut events = Vec::new();
1833 let mut diagnostics = Vec::new();
1834 for event in playback {
1835 let Some(address) = event.address.as_deref() else {
1836 continue;
1837 };
1838 let Some((part_index, staff_index, measure_index, voice_index, note_index)) =
1839 parse_playback_address(address)
1840 else {
1841 continue;
1842 };
1843 let Some(staff) = score
1844 .parts
1845 .get(part_index)
1846 .and_then(|part| part.staves.get(staff_index))
1847 else {
1848 continue;
1849 };
1850 let Some(note) = staff
1851 .measures
1852 .get(measure_index)
1853 .and_then(|measure| measure.voices.get(voice_index))
1854 .and_then(|voice| voice.get(note_index))
1855 else {
1856 continue;
1857 };
1858 let Some(tab) = staff.tablature_at(measure_index) else {
1859 push_tab_diagnostic(
1860 &mut diagnostics,
1861 TablaturePerformanceDiagnostic::NoTablatureStaff {
1862 address: address.into(),
1863 },
1864 );
1865 continue;
1866 };
1867 let transpose_cents = if event.channel == 9 {
1868 0
1869 } else {
1870 i32::from(staff.transpose_semitones) * 100
1871 };
1872 let written_pitch_cents = event.pitch_midi_cents - transpose_cents;
1873 let pitch_index = note
1874 .pitches
1875 .iter()
1876 .position(|pitch| pitch.to_midi_cents() == written_pitch_cents)
1877 .unwrap_or(0);
1878 let position = note
1879 .tab_positions
1880 .get(pitch_index)
1881 .or(note.tab_position.as_ref());
1882 let Some(position) = position else {
1883 push_tab_diagnostic(
1884 &mut diagnostics,
1885 TablaturePerformanceDiagnostic::MissingPosition {
1886 address: address.into(),
1887 pitch_index,
1888 },
1889 );
1890 continue;
1891 };
1892 if position.string == 0 || position.string > tab.lines {
1893 push_tab_diagnostic(
1894 &mut diagnostics,
1895 TablaturePerformanceDiagnostic::StringOutOfRange {
1896 address: address.into(),
1897 string: position.string,
1898 lines: tab.lines,
1899 },
1900 );
1901 continue;
1902 }
1903 let Some(tuning) = tab.tuning_midi.get(usize::from(position.string - 1)) else {
1904 push_tab_diagnostic(
1905 &mut diagnostics,
1906 TablaturePerformanceDiagnostic::TuningUnavailable {
1907 address: address.into(),
1908 string: position.string,
1909 },
1910 );
1911 continue;
1912 };
1913 let expected_pitch_midi_cents =
1914 tuning.saturating_add(i16::from(position.fret) + i16::from(tab.capo)) as i32 * 100;
1915 let pitch_error_cents = event.pitch_midi_cents - expected_pitch_midi_cents;
1916 if pitch_error_cents != 0 {
1917 push_tab_diagnostic(
1918 &mut diagnostics,
1919 TablaturePerformanceDiagnostic::PitchMismatch {
1920 address: address.into(),
1921 pitch_index,
1922 error_cents: pitch_error_cents,
1923 },
1924 );
1925 }
1926 events.push(TablaturePerformanceEvent {
1927 playback: event,
1928 string: position.string,
1929 fret: position.fret,
1930 technique: note.guitar_technique.clone(),
1931 bend_alter_cents: note.guitar_bend_alter_cents,
1932 bend_curve: note.guitar_bend_curve.clone(),
1933 expected_pitch_midi_cents,
1934 pitch_error_cents,
1935 });
1936 }
1937 Ok(TablaturePerformanceReport {
1938 contract_version: TAB_PERFORMANCE_CONTRACT_VERSION,
1939 events,
1940 diagnostics,
1941 })
1942}
1943
1944fn parse_playback_address(address: &str) -> Option<(usize, usize, usize, usize, usize)> {
1945 let mut fields = address.split(':').map(|field| field.parse::<usize>().ok());
1946 Some((
1947 fields.next()??,
1948 fields.next()??,
1949 fields.next()??,
1950 fields.next()??,
1951 fields.next()??,
1952 ))
1953}
1954
1955fn push_tab_diagnostic(
1956 diagnostics: &mut Vec<TablaturePerformanceDiagnostic>,
1957 diagnostic: TablaturePerformanceDiagnostic,
1958) {
1959 if diagnostics.len() < MAX_TAB_PERFORMANCE_DIAGNOSTICS {
1960 diagnostics.push(diagnostic);
1961 }
1962}
1963
1964type ExpressionKey = (usize, usize, usize, usize, usize);
1965
1966fn expression_key(address: &NoteAddr) -> ExpressionKey {
1967 (
1968 address.part,
1969 address.staff,
1970 address.measure,
1971 address.voice,
1972 address.note,
1973 )
1974}
1975
1976#[derive(Debug, Clone, Default)]
1983pub struct DynamicTimeline {
1984 position_beats: Vec<f64>,
1986 changes: Vec<(f64, u8, u8)>,
1988 ramps: Vec<(f64, f64, u8, u8)>,
1990}
1991
1992#[derive(Clone, Copy)]
1994enum DynamicMark {
1995 Marking(crate::Dynamic, bool),
1996 Level(u8, bool),
1997}
1998
1999fn mark_priority(mark: DynamicMark) -> u8 {
2000 match mark {
2001 DynamicMark::Level(_, false) => 0,
2002 DynamicMark::Level(_, true) => 1,
2003 DynamicMark::Marking(_, false) => 2,
2004 DynamicMark::Marking(_, true) => 3,
2005 }
2006}
2007
2008fn order_dynamic_marks(marks: &mut [(f64, DynamicMark)]) {
2011 marks.sort_by(|a, b| a.0.total_cmp(&b.0));
2012 let mut group_start = 0;
2013 while group_start < marks.len() {
2014 let time = marks[group_start].0;
2015 let mut group_end = group_start + 1;
2016 while group_end < marks.len() && marks[group_end].0 <= time + 1e-9 {
2017 group_end += 1;
2018 }
2019 for mark in &mut marks[group_start..group_end] {
2020 mark.0 = time;
2021 }
2022 marks[group_start..group_end].sort_by_key(|(_, mark)| mark_priority(*mark));
2023 group_start = group_end;
2024 }
2025}
2026
2027impl DynamicTimeline {
2028 pub const DEFAULT_VELOCITY: u8 = 64;
2030 const HAIRPIN_STEP: i16 = 24;
2032
2033 pub fn for_staff(staff: &crate::Staff, order: &[usize]) -> Self {
2036 Self::for_staves(std::slice::from_ref(staff), order, None, None)
2037 }
2038
2039 fn for_staves(
2040 staves: &[crate::Staff],
2041 order: &[usize],
2042 measure_lengths: Option<&[f64]>,
2043 shared: Option<(
2044 usize,
2045 usize,
2046 &BTreeSet<ExpressionKey>,
2047 &BTreeSet<ExpressionKey>,
2048 )>,
2049 ) -> Self {
2050 let mut position_beats = Vec::with_capacity(order.len());
2051 let mut marks: Vec<(f64, DynamicMark)> = Vec::new();
2052 let mut hairpins: Vec<(f64, f64, bool, bool)> = Vec::new();
2053 let mut open_hairpins = vec![[None; 4]; staves.len()];
2055 let mut elapsed = 0.0;
2056 for &measure_index in order {
2057 position_beats.push(elapsed);
2058 let mut length: f64 = 0.0;
2059 for (staff_index, staff) in staves.iter().enumerate() {
2060 let Some(measure) = staff.measures.get(measure_index) else {
2061 continue;
2062 };
2063 for (voice_index, voice) in measure.voices.iter().enumerate() {
2064 let mut beats = 0.0;
2065 for (note_index, note) in voice.iter().enumerate() {
2066 let onset = elapsed + beats;
2067 let local = shared.is_none_or(|(_, target, _, _)| staff_index == target);
2068 let key = shared.map(|(part, _, _, _)| {
2069 (part, staff_index, measure_index, voice_index, note_index)
2070 });
2071 let shared_dynamic = shared.is_some_and(|(_, _, dynamics, _)| {
2072 key.is_some_and(|key| dynamics.contains(&key))
2073 });
2074 let shared_hairpin = shared.is_some_and(|(_, _, _, hairpins)| {
2075 key.is_some_and(|key| hairpins.contains(&key))
2076 });
2077 if (local || shared_dynamic)
2078 && let Some(dynamic) = note.dynamic
2079 && !note.is_rest
2080 {
2081 marks.push((onset, DynamicMark::Marking(dynamic, local)));
2082 }
2083 let end = onset + note.beats();
2084 let slot = &mut open_hairpins[staff_index][voice_index];
2085 let ends_earlier = note.hairpin_end && slot.is_some();
2086 if ends_earlier && let Some((start, up, source_local)) = slot.take() {
2087 hairpins.push((start, end, up, source_local));
2088 }
2089 if (local || shared_hairpin)
2090 && let Some(kind) = note.hairpin_start
2091 {
2092 *slot = Some((onset, kind == crate::HairpinKind::Crescendo, local));
2093 }
2094 if note.hairpin_end
2095 && !ends_earlier
2096 && let Some((start, up, source_local)) = slot.take()
2097 {
2098 hairpins.push((start, end, up, source_local));
2099 }
2100 beats += note.beats();
2101 }
2102 length = length.max(beats);
2103 }
2104 }
2105 elapsed += measure_lengths
2106 .and_then(|values| values.get(measure_index))
2107 .copied()
2108 .unwrap_or(length);
2109 }
2110 order_dynamic_marks(&mut marks);
2111 let written = Self::resolve(&marks);
2112 let mut ramps = Vec::new();
2113 hairpins.sort_by_key(|(_, _, _, local)| *local);
2115 for (start, end, up, local) in hairpins {
2116 if end <= start {
2117 continue;
2118 }
2119 let from = Self::level_at(&written, start);
2120 let first = written.partition_point(|&(time, _, _)| time < end - 1e-9);
2122 let next = written
2123 .get(first)
2124 .and_then(|&(time, _, _)| {
2125 if time > end + 1.0 {
2126 return None;
2127 }
2128 let last = written
2129 .partition_point(|&(other, _, _)| other <= time + 1e-9)
2130 .saturating_sub(1);
2131 written.get(last).map(|&(_, attack, _)| attack)
2132 })
2133 .filter(|&attack| if up { attack > from } else { attack < from });
2134 let to = next.unwrap_or_else(|| {
2135 let step = if up {
2136 Self::HAIRPIN_STEP
2137 } else {
2138 -Self::HAIRPIN_STEP
2139 };
2140 (i16::from(from) + step).clamp(1, 127) as u8
2141 });
2142 if next.is_none() {
2143 marks.push((end, DynamicMark::Level(to, local)));
2144 }
2145 ramps.push((start, end, from, to));
2146 }
2147 order_dynamic_marks(&mut marks);
2149 Self {
2150 position_beats,
2151 changes: Self::resolve(&marks),
2152 ramps,
2153 }
2154 }
2155
2156 fn resolve(marks: &[(f64, DynamicMark)]) -> Vec<(f64, u8, u8)> {
2157 let mut level = Self::DEFAULT_VELOCITY;
2158 marks
2159 .iter()
2160 .map(|&(time, mark)| match mark {
2161 DynamicMark::Marking(dynamic, _) => {
2162 if let Some(next) = dynamic.sustained_level() {
2163 level = next.to_velocity();
2164 }
2165 (time, dynamic.to_velocity(), level)
2166 }
2167 DynamicMark::Level(velocity, _) => {
2168 level = velocity;
2169 (time, velocity, level)
2170 }
2171 })
2172 .collect()
2173 }
2174
2175 fn level_at(changes: &[(f64, u8, u8)], time: f64) -> u8 {
2176 let before = changes.partition_point(|&(t, _, _)| t <= time + 1e-9);
2177 before
2178 .checked_sub(1)
2179 .map_or(Self::DEFAULT_VELOCITY, |index| changes[index].2)
2180 }
2181
2182 pub fn velocity(&self, position: usize, beats: f64, own: Option<crate::Dynamic>) -> u8 {
2186 if let Some(dynamic) = own {
2187 return dynamic.to_velocity();
2188 }
2189 let time = self.position_beats.get(position).copied().unwrap_or(0.0) + beats;
2190 let before = self.changes.partition_point(|&(t, _, _)| t <= time + 1e-9);
2191 let current = before.checked_sub(1).map(|index| self.changes[index]);
2192 if let Some((t, attack, _)) = current
2193 && (t - time).abs() <= 1e-9
2194 {
2195 return attack;
2196 }
2197 if let Some(&(start, end, from, to)) = self
2198 .ramps
2199 .iter()
2200 .rev()
2201 .find(|&&(start, end, _, _)| start <= time + 1e-9 && time < end - 1e-9)
2202 {
2203 let fraction = ((time - start) / (end - start)).clamp(0.0, 1.0);
2204 let value = f64::from(from) + (f64::from(to) - f64::from(from)) * fraction;
2205 return value.round().clamp(1.0, 127.0) as u8;
2206 }
2207 current.map_or(Self::DEFAULT_VELOCITY, |(_, _, level)| level)
2208 }
2209}
2210
2211pub fn to_playback_events(score: &Score, options: &PlaybackOptions) -> Vec<PlaybackEvent> {
2217 let materialized = score.with_legacy_spanner_flags();
2219 let score: &Score = &materialized;
2220 let bpm = initial_tempo_bpm(score, options);
2221 let tempo_marks = score_tempo_marks(score);
2222 let measure_lengths: Vec<f64> = (0..tempo_marks.len())
2223 .map(|index| score.playback_measure_beats(index))
2224 .collect();
2225 let mut measure_tick_starts = Vec::with_capacity(measure_lengths.len());
2226 let mut next_tick = 0_u64;
2227 for beats in &measure_lengths {
2228 measure_tick_starts.push(next_tick);
2229 next_tick = next_tick.saturating_add((beats * 480.0).round() as u64);
2230 }
2231 let swing_ratio = options
2232 .swing
2233 .filter(|ratio| ratio.is_finite() && *ratio > 0.5 && *ratio < 1.0);
2234 let full_seq = measure_sequence(score);
2235 let seq: Vec<usize> = if let Some((lo, hi)) = options.loop_region {
2236 full_seq
2237 .iter()
2238 .copied()
2239 .filter(|&idx| idx >= lo && idx <= hi)
2240 .collect()
2241 } else {
2242 full_seq.clone()
2243 };
2244 let mut events: Vec<PlaybackEvent> = Vec::new();
2245
2246 for (part_index, part) in score.parts.iter().enumerate() {
2247 if options.muted_parts.contains(&part_index) {
2248 continue;
2249 }
2250 let selected_note = |address: &NoteAddr| {
2251 (address.part == part_index).then_some(())?;
2252 part.staves
2253 .get(address.staff)?
2254 .measures
2255 .get(address.measure)?
2256 .voices
2257 .get(address.voice)?
2258 .get(address.note)
2259 };
2260 let shared_dynamic_keys: BTreeSet<ExpressionKey> = options
2261 .shared_dynamics
2262 .iter()
2263 .filter(|address| {
2264 selected_note(address).is_some_and(|note| !note.is_rest && note.dynamic.is_some())
2265 })
2266 .map(expression_key)
2267 .collect();
2268 let shared_hairpin_keys: BTreeSet<ExpressionKey> = options
2269 .shared_hairpins
2270 .iter()
2271 .filter(|address| {
2272 selected_note(address).is_some_and(|note| note.hairpin_start.is_some())
2273 })
2274 .map(expression_key)
2275 .collect();
2276 for (staff_index, staff) in part.staves.iter().enumerate() {
2277 let has_shared_foreign = shared_dynamic_keys
2278 .iter()
2279 .chain(&shared_hairpin_keys)
2280 .any(|key| key.1 != staff_index);
2281 let dynamics = if !has_shared_foreign {
2282 DynamicTimeline::for_staff(staff, &seq)
2283 } else {
2284 DynamicTimeline::for_staves(
2285 &part.staves,
2286 &seq,
2287 Some(&measure_lengths),
2288 Some((
2289 part_index,
2290 staff_index,
2291 &shared_dynamic_keys,
2292 &shared_hairpin_keys,
2293 )),
2294 )
2295 };
2296 for voice_idx in 0..4usize {
2297 let mut time_beats = 0.0f64;
2298 let mut time_secs_cursor = 0.0f64;
2299 let mut current_bpm = bpm;
2300 let mut prior_note_events = 0..0;
2301 let mut active_named_channel = None;
2304 if let Some(&first) = seq.first() {
2305 for &idx in full_seq.iter().take_while(|&&idx| idx != first) {
2306 if let Some(measure) = staff.measures.get(idx) {
2307 let mut switches: Vec<_> = measure
2308 .channel_switches
2309 .iter()
2310 .filter(|switch| {
2311 switch.voice == voice_idx && switch.position_beats.is_finite()
2312 })
2313 .collect();
2314 switches.sort_by(|a, b| a.position_beats.total_cmp(&b.position_beats));
2315 for switch in switches {
2316 if let Some(channel) = part
2317 .named_channels
2318 .iter()
2319 .find(|channel| channel.name == switch.channel_name)
2320 {
2321 active_named_channel = Some(channel);
2322 }
2323 }
2324 }
2325 }
2326 }
2327 for (seq_position, &idx) in seq.iter().enumerate() {
2328 let measure = match staff.measures.get(idx) {
2329 Some(m) => m,
2330 None => continue,
2331 };
2332 let instrument = measure
2333 .instrument_change
2334 .as_ref()
2335 .or(part.instrument.as_ref());
2336 let base_channel =
2337 instrument.map_or(part.midi_channel, |value| value.midi_channel);
2338 let default_program =
2339 instrument.map_or(part.midi_program, |value| value.midi_program);
2340 let instrument_id = instrument.map(|value| value.id.clone());
2341 let mark = tempo_marks.get(idx).copied().unwrap_or_default();
2342 if let Some(b) = mark.bpm.and_then(|tempo| written_tempo_bpm(options, tempo)) {
2343 current_bpm = b;
2344 }
2345 let measure_beats = measure_lengths[idx];
2346 let grid_offset_beats = if idx == 0
2347 && measure_beats
2348 < staff
2349 .meter_at(idx, &score.settings.time_signature)
2350 .total_beats()
2351 {
2352 (staff
2353 .meter_at(idx, &score.settings.time_signature)
2354 .total_beats()
2355 - measure_beats)
2356 .max(0.0)
2357 } else {
2358 0.0
2359 };
2360 let ramp_end_bpm = written_tempo_ramp_bpm(options, mark.ramp_to_bpm);
2361 let measure_start_beats = time_beats;
2362 let measure_start_secs = time_secs_cursor;
2363 let mut local_beats = 0.0f64;
2364 let mut written_beats = 0.0f64;
2366 let mut swing_second = false;
2367 let mut switches: Vec<_> = measure
2368 .channel_switches
2369 .iter()
2370 .filter(|switch| {
2371 switch.voice == voice_idx && switch.position_beats.is_finite()
2372 })
2373 .collect();
2374 switches.sort_by(|a, b| a.position_beats.total_cmp(&b.position_beats));
2375 let mut next_switch = 0;
2376 for (note_index, note) in measure.voices[voice_idx].iter().enumerate() {
2377 if note.is_grace || note.is_cue {
2378 continue;
2379 }
2380 let onset_written_beats = written_beats;
2381 while let Some(switch) = switches
2382 .get(next_switch)
2383 .filter(|switch| switch.position_beats <= onset_written_beats + 1e-9)
2384 {
2385 if let Some(channel) = part
2386 .named_channels
2387 .iter()
2388 .find(|channel| channel.name == switch.channel_name)
2389 {
2390 active_named_channel = Some(channel);
2391 }
2392 next_switch += 1;
2393 }
2394 let channel =
2395 active_named_channel.map_or(base_channel, |value| value.midi_channel);
2396 written_beats += note.beats();
2397 let dur = if let Some(ratio) = swing_ratio {
2398 if swing_second {
2399 swing_second = false;
2400 (1.0 - ratio) * note.beats() * 2.0
2401 } else if starts_swing_pair(
2402 &measure.voices[voice_idx],
2403 note_index,
2404 onset_written_beats,
2405 grid_offset_beats,
2406 measure_beats,
2407 &options.swing_unit,
2408 ) {
2409 swing_second = true;
2410 ratio * note.beats() * 2.0
2411 } else {
2412 note.beats()
2413 }
2414 } else {
2415 note.beats()
2416 };
2417 if !note.is_rest {
2418 let source_tick = measure_tick_starts[idx]
2422 .saturating_add((onset_written_beats * 480.0).round() as u64);
2423 let mut program = default_program;
2424 let mut latest_change_tick = 0_u64;
2425 for change in &part.midi_program_changes {
2426 if change.channel == channel
2427 && change.tick <= source_tick
2428 && change.tick >= latest_change_tick
2429 {
2430 program = change.program;
2431 latest_change_tick = change.tick;
2432 }
2433 }
2434 if let Some(named) = active_named_channel {
2435 program = named.program;
2436 }
2437 let mut velocity =
2438 dynamics.velocity(seq_position, onset_written_beats, note.dynamic);
2439 let mut sounding_dur = dur;
2440 for art in ¬e.articulations {
2441 match art {
2442 Articulation::Staccato | Articulation::Staccatissimo => {
2443 sounding_dur *= 0.5;
2444 }
2445 Articulation::Accent | Articulation::Marcato => {
2446 velocity = velocity.saturating_add(20).min(127);
2447 }
2448 Articulation::Fermata
2451 if options.fermata_multiplier.is_finite()
2452 && options.fermata_multiplier < 1.0 =>
2453 {
2454 sounding_dur *= options.fermata_multiplier.max(0.0);
2455 }
2456 _ => {}
2457 }
2458 }
2459 let pedal = note.pedal_start
2462 || measure.timed_pedals.iter().any(|pedal| {
2463 pedal.down
2464 && (pedal.position_beats - onset_written_beats).abs() < 1e-9
2465 });
2466 let fermata_hold_beats = if options.fermata_hold_beats.is_finite() {
2467 options.fermata_hold_beats.max(0.0)
2468 } else {
2469 0.0
2470 };
2471 let post_note_pause_beats =
2472 note.articulations
2473 .iter()
2474 .fold(0.0f64, |pause, articulation| {
2475 pause.max(match articulation {
2476 Articulation::BreathMark => 0.25,
2477 Articulation::Caesura => 0.5,
2478 Articulation::Fermata => fermata_hold_beats,
2479 _ => 0.0,
2480 })
2481 });
2482 let transpose = if channel == 9 {
2483 0i8
2484 } else {
2485 staff.transpose_semitones.saturating_add(
2486 instrument.map_or(0, |value| value.transpose_semitones),
2487 )
2488 };
2489 let main_event_start = events.len();
2490 for (pitch_index, pitch) in note.pitches.iter().enumerate() {
2491 let percussion = part.percussion_key(note, pitch_index);
2493 let transpose = if percussion.is_some() { 0 } else { transpose };
2494 let midi = percussion.map_or_else(
2495 || (pitch.to_midi() + transpose as i16).clamp(0, 127) as u8,
2496 |key| key.min(127),
2497 );
2498 events.push(PlaybackEvent {
2499 address: Some(format!(
2500 "{part_index}:{staff_index}:{idx}:{voice_idx}:{note_index}"
2501 )),
2502 source: Some(NoteAddr {
2503 part: part_index,
2504 staff: staff_index,
2505 measure: idx,
2506 voice: voice_idx,
2507 note: note_index,
2508 }),
2509 source_voice_number: measure.source_voice_numbers[voice_idx],
2510 time_beats: measure_start_beats + local_beats,
2511 time_secs: measure_start_secs
2512 + tempo_ramp_seconds(
2513 current_bpm,
2514 ramp_end_bpm,
2515 measure_beats,
2516 local_beats,
2517 ),
2518 pitch_midi: midi,
2519 pitch_midi_cents: if percussion.is_some() {
2520 i32::from(midi) * 100
2521 } else {
2522 pitch.to_midi_cents() + transpose as i32 * 100
2523 },
2524 pitch_bend_curve: note.guitar_bend_curve.clone(),
2525 post_note_pause_beats,
2526 articulations: note.articulations.clone(),
2527 chord_symbol: note.chord_symbol.clone(),
2528 guitar_technique: note.guitar_technique.clone(),
2529 velocity,
2530 duration_beats: sounding_dur,
2531 duration_secs: tempo_ramp_seconds(
2532 current_bpm,
2533 ramp_end_bpm,
2534 measure_beats,
2535 local_beats + sounding_dur,
2536 ) - tempo_ramp_seconds(
2537 current_bpm,
2538 ramp_end_bpm,
2539 measure_beats,
2540 local_beats,
2541 ),
2542 pedal,
2543 part_index,
2544 channel,
2545 program,
2546 instrument_id: instrument_id.clone(),
2547 is_metronome: false,
2548 });
2549 }
2550 let main_event_end = events.len();
2551 let voice = &measure.voices[voice_idx];
2552 let mut grace_start = note_index;
2553 while grace_start > 0 && voice[grace_start - 1].is_grace {
2554 grace_start -= 1;
2555 }
2556 if grace_start < note_index && main_event_start < main_event_end {
2557 let graces = &voice[grace_start..note_index];
2558 let written: Vec<f64> = graces
2562 .iter()
2563 .map(|grace| {
2564 (grace.duration.beats(grace.dot_count) * 0.25).min(0.25)
2565 })
2566 .collect();
2567 let requested: f64 = written.iter().sum();
2568 let prior_onset = prior_note_events
2569 .clone()
2570 .next()
2571 .map(|index| events[index].time_beats - measure_start_beats)
2572 .unwrap_or(0.0);
2573 let before = requested
2574 .min(local_beats * 0.5)
2575 .min((local_beats - prior_onset).max(0.0) * 0.5);
2576 let take_from_previous = before > 0.0;
2577 let grace_total = if take_from_previous {
2578 before
2579 } else {
2580 requested.min(sounding_dur * 0.5)
2581 };
2582 if grace_total > 0.0 {
2583 let grace_start_beat = if take_from_previous {
2584 local_beats - grace_total
2585 } else {
2586 local_beats
2587 };
2588 let grace_start_secs = measure_start_secs
2589 + tempo_ramp_seconds(
2590 current_bpm,
2591 ramp_end_bpm,
2592 measure_beats,
2593 grace_start_beat,
2594 );
2595 if take_from_previous {
2596 for event in &mut events[prior_note_events.clone()] {
2597 if event.time_beats + event.duration_beats
2598 > measure_start_beats + grace_start_beat
2599 {
2600 event.duration_beats = measure_start_beats
2601 + grace_start_beat
2602 - event.time_beats;
2603 event.duration_secs =
2604 grace_start_secs - event.time_secs;
2605 }
2606 }
2607 } else {
2608 let shifted_beat = local_beats + grace_total;
2609 let shifted_secs = measure_start_secs
2610 + tempo_ramp_seconds(
2611 current_bpm,
2612 ramp_end_bpm,
2613 measure_beats,
2614 shifted_beat,
2615 );
2616 for event in &mut events[main_event_start..main_event_end] {
2617 event.time_beats = measure_start_beats + shifted_beat;
2618 event.time_secs = shifted_secs;
2619 event.duration_beats -= grace_total;
2620 event.duration_secs = tempo_ramp_seconds(
2621 current_bpm,
2622 ramp_end_bpm,
2623 measure_beats,
2624 local_beats + sounding_dur,
2625 ) - tempo_ramp_seconds(
2626 current_bpm,
2627 ramp_end_bpm,
2628 measure_beats,
2629 shifted_beat,
2630 );
2631 }
2632 }
2633 let mut cursor = grace_start_beat;
2634 for (offset, grace) in graces.iter().enumerate() {
2635 let grace_beats = grace_total * written[offset] / requested;
2636 let grace_end = cursor + grace_beats;
2637 for (pitch_index, pitch) in grace.pitches.iter().enumerate()
2638 {
2639 let percussion =
2640 part.percussion_key(grace, pitch_index);
2641 let pitch_transpose =
2642 if percussion.is_some() { 0 } else { transpose };
2643 let mut event = events[main_event_start].clone();
2644 event.address = Some(format!(
2645 "{part_index}:{staff_index}:{idx}:{voice_idx}:{}",
2646 grace_start + offset
2647 ));
2648 event.source = Some(NoteAddr {
2649 part: part_index,
2650 staff: staff_index,
2651 measure: idx,
2652 voice: voice_idx,
2653 note: grace_start + offset,
2654 });
2655 event.pitch_midi = percussion.map_or_else(
2656 || {
2657 (pitch.to_midi() + pitch_transpose as i16)
2658 .clamp(0, 127)
2659 as u8
2660 },
2661 |key| key.min(127),
2662 );
2663 event.pitch_midi_cents = if percussion.is_some() {
2664 i32::from(event.pitch_midi) * 100
2665 } else {
2666 pitch.to_midi_cents()
2667 + i32::from(pitch_transpose) * 100
2668 };
2669 event.time_beats = measure_start_beats + cursor;
2670 event.time_secs = measure_start_secs
2671 + tempo_ramp_seconds(
2672 current_bpm,
2673 ramp_end_bpm,
2674 measure_beats,
2675 cursor,
2676 );
2677 event.duration_beats = grace_beats;
2678 event.duration_secs = tempo_ramp_seconds(
2679 current_bpm,
2680 ramp_end_bpm,
2681 measure_beats,
2682 grace_end,
2683 ) - tempo_ramp_seconds(
2684 current_bpm,
2685 ramp_end_bpm,
2686 measure_beats,
2687 cursor,
2688 );
2689 event.articulations = grace.articulations.clone();
2690 event.pitch_bend_curve =
2691 grace.guitar_bend_curve.clone();
2692 event.guitar_technique = grace.guitar_technique.clone();
2693 event.chord_symbol = grace.chord_symbol.clone();
2694 event.post_note_pause_beats = 0.0;
2695 event.velocity = dynamics.velocity(
2696 seq_position,
2697 onset_written_beats,
2698 grace.dynamic,
2699 );
2700 event.pedal = grace.pedal_start;
2701 events.push(event);
2702 }
2703 cursor = grace_end;
2704 }
2705 }
2706 }
2707 prior_note_events = main_event_start..main_event_end;
2708 }
2709 local_beats += dur;
2710 }
2711 for switch in switches
2714 .iter()
2715 .skip(next_switch)
2716 .filter(|switch| switch.position_beats <= measure_beats + 1e-9)
2717 {
2718 if let Some(channel) = part
2719 .named_channels
2720 .iter()
2721 .find(|channel| channel.name == switch.channel_name)
2722 {
2723 active_named_channel = Some(channel);
2724 }
2725 }
2726 time_beats = measure_start_beats + measure_beats;
2730 time_secs_cursor = measure_start_secs
2731 + tempo_ramp_seconds(
2732 current_bpm,
2733 ramp_end_bpm,
2734 measure_beats,
2735 measure_beats,
2736 );
2737 if let Some(end_bpm) = ramp_end_bpm {
2738 current_bpm = end_bpm;
2739 }
2740 }
2741 }
2742 }
2743 }
2744
2745 if let Some(ref metro) = options.metronome {
2746 let mut cursor_secs = 0.0f64;
2747 let mut cursor_beats = 0.0f64;
2748 let mut metro_bpm = bpm;
2749 for &idx in &seq {
2750 let first_staff = score.parts.first().and_then(|p| p.staves.first());
2751 let mark = tempo_marks.get(idx).copied().unwrap_or_default();
2752 if let Some(t) = mark.bpm.and_then(|tempo| written_tempo_bpm(options, tempo)) {
2753 metro_bpm = t;
2754 }
2755 let ts = first_staff.map_or(&score.settings.time_signature, |s| {
2756 s.meter_at(idx, &score.settings.time_signature)
2757 });
2758 let measure_beats = measure_lengths[idx];
2759 let ramp_end_bpm = written_tempo_ramp_bpm(options, mark.ramp_to_bpm);
2760 let beat_unit = ts.beat_unit_beats();
2761 let num_beats = (measure_beats / beat_unit).round() as u32;
2762 for b in 0..num_beats {
2763 let is_accent = b == 0;
2764 let beat_offset_beats = b as f64 * beat_unit;
2765 let beat_offset_secs =
2766 tempo_ramp_seconds(metro_bpm, ramp_end_bpm, measure_beats, beat_offset_beats);
2767 events.push(PlaybackEvent {
2768 address: None,
2769 source: None,
2770 source_voice_number: None,
2771 time_beats: cursor_beats + b as f64 * beat_unit,
2772 time_secs: cursor_secs + beat_offset_secs,
2773 pitch_midi: if is_accent {
2774 metro.accent_pitch
2775 } else {
2776 metro.beat_pitch
2777 },
2778 pitch_midi_cents: i32::from(if is_accent {
2779 metro.accent_pitch
2780 } else {
2781 metro.beat_pitch
2782 }) * 100,
2783 pitch_bend_curve: Vec::new(),
2784 post_note_pause_beats: 0.0,
2785 articulations: Vec::new(),
2786 chord_symbol: None,
2787 guitar_technique: None,
2788 velocity: if is_accent {
2789 metro.accent_velocity
2790 } else {
2791 metro.beat_velocity
2792 },
2793 duration_beats: beat_unit * 0.1,
2794 duration_secs: tempo_ramp_seconds(
2795 metro_bpm,
2796 ramp_end_bpm,
2797 measure_beats,
2798 beat_offset_beats + beat_unit * 0.1,
2799 ) - beat_offset_secs,
2800 pedal: false,
2801 part_index: usize::MAX,
2802 channel: metro.channel,
2803 program: 0,
2804 instrument_id: None,
2805 is_metronome: true,
2806 });
2807 }
2808 cursor_secs +=
2809 tempo_ramp_seconds(metro_bpm, ramp_end_bpm, measure_beats, measure_beats);
2810 cursor_beats += measure_beats;
2811 if let Some(end_bpm) = ramp_end_bpm {
2812 metro_bpm = end_bpm;
2813 }
2814 }
2815 }
2816
2817 events.sort_by(|a, b| {
2820 a.time_beats
2821 .partial_cmp(&b.time_beats)
2822 .unwrap_or(std::cmp::Ordering::Equal)
2823 });
2824 events = merge_tied_events(score, options, events);
2825 events = realize_single_tremolos(score, options, events);
2826 apply_fermata_holds(&mut events, &score_fermata_holds(score, options));
2827 events = apply_realization_profile(score, events, options.realization_profile);
2828 events.sort_by(|a, b| {
2829 a.time_beats
2830 .partial_cmp(&b.time_beats)
2831 .unwrap_or(std::cmp::Ordering::Equal)
2832 });
2833 events
2834}
2835
2836fn realize_single_tremolos(
2839 score: &Score,
2840 options: &PlaybackOptions,
2841 events: Vec<PlaybackEvent>,
2842) -> Vec<PlaybackEvent> {
2843 let mut realized = Vec::with_capacity(events.len());
2844 let mut clock = None;
2845 for event in events {
2846 let strokes = event.articulations.iter().find_map(|mark| match mark {
2847 Articulation::Tremolo(n @ 1..=8) => Some(*n),
2848 _ => None,
2849 });
2850 let note = event.source.as_ref().and_then(|source| {
2851 score
2852 .parts
2853 .get(source.part)
2854 .and_then(|part| part.staves.get(source.staff))
2855 .and_then(|staff| staff.measures.get(source.measure))
2856 .and_then(|measure| measure.voices.get(source.voice))
2857 .and_then(|voice| voice.get(source.note))
2858 });
2859 let (Some(strokes), Some(note)) = (strokes, note) else {
2860 realized.push(event);
2861 continue;
2862 };
2863 if note.tremolo_pair.is_some() {
2864 realized.push(event);
2865 continue;
2866 }
2867 let tuplet_scale = note.tuplet.as_ref().map_or(1.0, |tuplet| {
2868 f64::from(tuplet.normal_notes) / f64::from(tuplet.actual_notes.max(1))
2869 });
2870 let attack_beats =
2871 note.duration.beats(0).min(1.0) * tuplet_scale / f64::from(1_u32 << strokes);
2872 if !attack_beats.is_finite() || attack_beats <= 0.0 {
2873 realized.push(event);
2874 continue;
2875 }
2876 if !event.duration_beats.is_finite() || event.duration_beats <= 0.0 {
2877 realized.push(event);
2878 continue;
2879 }
2880 let count = (event.duration_beats / attack_beats - 1e-9).ceil() as usize;
2881 if count <= 1 || count > 65_536 {
2883 realized.push(event);
2884 continue;
2885 }
2886 let timeline = clock.get_or_insert_with(|| playback_measure_clock(score, options));
2887 for ordinal in 0..count {
2888 let start = event.time_beats + ordinal as f64 * attack_beats;
2889 let end = (start + attack_beats).min(event.time_beats + event.duration_beats);
2890 let mut attack = event.clone();
2891 attack.time_beats = start;
2892 attack.duration_beats = end - start;
2893 let start_secs = playback_secs_at_beat(timeline, start);
2894 let end_secs = playback_secs_at_beat(timeline, end);
2895 if let (Some(start_secs), Some(end_secs)) = (start_secs, end_secs) {
2896 attack.time_secs = start_secs;
2897 attack.duration_secs = end_secs - start_secs;
2898 } else {
2899 attack.time_secs +=
2900 event.duration_secs * (start - event.time_beats) / event.duration_beats;
2901 attack.duration_secs =
2902 event.duration_secs * attack.duration_beats / event.duration_beats;
2903 }
2904 attack
2905 .articulations
2906 .retain(|mark| !matches!(mark, Articulation::Tremolo(_)));
2907 if ordinal > 0 {
2908 attack.pedal = false;
2909 attack.chord_symbol = None;
2910 }
2911 if ordinal + 1 < count {
2912 attack.post_note_pause_beats = 0.0;
2913 }
2914 realized.push(attack);
2915 }
2916 }
2917 realized
2918}
2919
2920const ORNAMENT_ATTACK_COUNT: usize = 4;
2921const ARPEGGIO_MAX_SPREAD_BEATS: f64 = 0.18;
2922
2923fn apply_realization_profile(
2924 score: &Score,
2925 mut events: Vec<PlaybackEvent>,
2926 profile: PlaybackRealizationProfile,
2927) -> Vec<PlaybackEvent> {
2928 if profile == PlaybackRealizationProfile::Authored {
2929 return events;
2930 }
2931
2932 let mut chord_groups: BTreeMap<(String, u64), Vec<usize>> = BTreeMap::new();
2933 for (index, event) in events.iter().enumerate() {
2934 if !event.is_metronome
2935 && let Some(address) = &event.address
2936 {
2937 chord_groups
2938 .entry((address.clone(), event.time_beats.to_bits()))
2939 .or_default()
2940 .push(index);
2941 }
2942 }
2943 for indices in chord_groups.into_values() {
2944 let Some(source) = events[indices[0]].source.as_ref() else {
2945 continue;
2946 };
2947 let Some(direction) = score
2948 .parts
2949 .get(source.part)
2950 .and_then(|part| part.staves.get(source.staff))
2951 .and_then(|staff| staff.measures.get(source.measure))
2952 .and_then(|measure| measure.voices.get(source.voice))
2953 .and_then(|voice| voice.get(source.note))
2954 .and_then(|note| note.arpeggiate)
2955 else {
2956 continue;
2957 };
2958 if indices.len() < 2 {
2959 continue;
2960 }
2961 let mut ordered = indices;
2962 ordered.sort_by_key(|&index| events[index].pitch_midi_cents);
2963 if !direction {
2964 ordered.reverse();
2965 }
2966 let max_spread = events[ordered[0]]
2967 .duration_beats
2968 .max(0.0)
2969 .mul_add(0.5, 0.0)
2970 .min(ARPEGGIO_MAX_SPREAD_BEATS);
2971 let step = max_spread / (ordered.len() - 1) as f64;
2972 for (ordinal, index) in ordered.into_iter().enumerate() {
2973 shift_event_time(&mut events[index], step * ordinal as f64);
2974 }
2975 }
2976
2977 let mut realized = Vec::with_capacity(events.len());
2978 for event in events {
2979 let offsets = ornament_offsets(&event.articulations);
2980 if offsets.is_empty()
2981 || event.is_metronome
2982 || event.duration_beats <= 0.0
2983 || event
2984 .articulations
2985 .iter()
2986 .filter(|mark| is_playback_ornament(mark))
2987 .count()
2988 != 1
2989 {
2990 realized.push(event);
2991 continue;
2992 }
2993 let attack_beats = event.duration_beats / ORNAMENT_ATTACK_COUNT as f64;
2994 let attack_secs = event.duration_secs / ORNAMENT_ATTACK_COUNT as f64;
2995 for (ordinal, semitones) in offsets.into_iter().enumerate() {
2996 let mut attack = event.clone();
2997 let displacement = i32::from(semitones) * 100;
2998 attack.pitch_midi_cents = (attack.pitch_midi_cents + displacement).clamp(0, 12_700);
2999 attack.pitch_midi =
3000 ((i32::from(attack.pitch_midi) + i32::from(semitones)).clamp(0, 127)) as u8;
3001 attack.time_beats += attack_beats * ordinal as f64;
3002 attack.time_secs += attack_secs * ordinal as f64;
3003 attack.duration_beats = attack_beats;
3004 attack.duration_secs = attack_secs;
3005 attack
3008 .articulations
3009 .retain(|mark| !is_playback_ornament(mark));
3010 if ordinal > 0 {
3011 attack.chord_symbol = None;
3012 }
3013 realized.push(attack);
3014 }
3015 }
3016 realized
3017}
3018
3019fn shift_event_time(event: &mut PlaybackEvent, offset_beats: f64) {
3020 if offset_beats <= 0.0 || event.duration_beats <= 0.0 {
3021 return;
3022 }
3023 event.time_beats += offset_beats;
3024 event.time_secs += event.duration_secs * (offset_beats / event.duration_beats);
3025}
3026
3027fn is_playback_ornament(mark: &Articulation) -> bool {
3028 matches!(
3029 mark,
3030 Articulation::Trill
3031 | Articulation::Shake
3032 | Articulation::Mordent
3033 | Articulation::InvertedMordent
3034 | Articulation::Turn
3035 | Articulation::InvertedTurn
3036 )
3037}
3038
3039fn ornament_offsets(articulations: &[Articulation]) -> Vec<i8> {
3040 if articulations
3041 .iter()
3042 .any(|articulation| matches!(articulation, Articulation::Trill | Articulation::Shake))
3043 {
3044 return vec![0, 1, 0, 1];
3045 }
3046 if articulations
3047 .iter()
3048 .any(|articulation| matches!(articulation, Articulation::Mordent))
3049 {
3050 return vec![0, 1, 0, 0];
3051 }
3052 if articulations
3053 .iter()
3054 .any(|articulation| matches!(articulation, Articulation::InvertedMordent))
3055 {
3056 return vec![0, -1, 0, 0];
3057 }
3058 if articulations
3059 .iter()
3060 .any(|articulation| matches!(articulation, Articulation::Turn))
3061 {
3062 return vec![1, 0, -1, 0];
3063 }
3064 if articulations
3065 .iter()
3066 .any(|articulation| matches!(articulation, Articulation::InvertedTurn))
3067 {
3068 return vec![-1, 0, 1, 0];
3069 }
3070 Vec::new()
3071}
3072
3073fn tempo_ramp_seconds(start_bpm: f64, end_bpm: Option<f64>, measure_beats: f64, beats: f64) -> f64 {
3075 let beats = beats.max(0.0);
3076 let Some(end_bpm) = end_bpm else {
3077 return beats / start_bpm * 60.0;
3078 };
3079 if measure_beats <= 0.0 || (end_bpm - start_bpm).abs() < f64::EPSILON {
3080 return beats / start_bpm * 60.0;
3081 }
3082 let delta = end_bpm - start_bpm;
3083 let bpm_at_beats = start_bpm + delta * beats / measure_beats;
3084 if bpm_at_beats <= 0.0 {
3085 return beats / start_bpm * 60.0;
3086 }
3087 60.0 * measure_beats / delta * (bpm_at_beats / start_bpm).ln()
3088}
3089
3090fn tempo_ramp_beats_at_seconds(
3095 start_bpm: f64,
3096 end_bpm: Option<f64>,
3097 measure_beats: f64,
3098 seconds: f64,
3099) -> f64 {
3100 let seconds = seconds.max(0.0);
3101 let Some(end_bpm) = end_bpm else {
3102 return seconds * start_bpm / 60.0;
3103 };
3104 if measure_beats <= 0.0 || (end_bpm - start_bpm).abs() < f64::EPSILON {
3105 return seconds * start_bpm / 60.0;
3106 }
3107 let delta = end_bpm - start_bpm;
3108 let exponent = seconds * delta / (60.0 * measure_beats);
3109 let ratio = exponent.exp();
3110 if !ratio.is_finite() {
3111 return measure_beats;
3112 }
3113 let beats = measure_beats * start_bpm * (ratio - 1.0) / delta;
3114 if beats.is_finite() {
3115 beats
3116 } else {
3117 measure_beats
3118 }
3119}
3120
3121fn merge_tied_events(
3127 score: &Score,
3128 options: &PlaybackOptions,
3129 events: Vec<PlaybackEvent>,
3130) -> Vec<PlaybackEvent> {
3131 use std::collections::HashMap;
3132
3133 struct PendingTie {
3134 index: usize,
3135 staff: usize,
3136 voice: usize,
3137 notated_end_beats: f64,
3138 staccato: bool,
3139 }
3140
3141 let mut merged = Vec::with_capacity(events.len());
3142 let mut pending: HashMap<(usize, i32), Vec<PendingTie>> = HashMap::new();
3143 let mut clock = None;
3144 for event in events {
3145 let Some(source) = event.source.as_ref() else {
3146 merged.push(event);
3147 continue;
3148 };
3149 let tied_staff = score
3150 .parts
3151 .get(source.part)
3152 .and_then(|part| part.staves.get(source.staff));
3153 let tied_note = tied_staff
3154 .and_then(|staff| staff.measures.get(source.measure))
3155 .and_then(|measure| measure.voices.get(source.voice))
3156 .and_then(|voice| voice.get(source.note));
3157 let (Some(staff), Some(note)) = (tied_staff, tied_note) else {
3158 merged.push(event);
3159 continue;
3160 };
3161 let transpose_cents = i32::from(staff.transpose_semitones) * 100;
3163 let pitch_index = note
3164 .pitches
3165 .iter()
3166 .position(|pitch| pitch.to_midi_cents() + transpose_cents == event.pitch_midi_cents);
3167 let (tie_start, tie_end) = match pitch_index {
3168 Some(index) => (note.pitch_tie_start(index), note.pitch_tie_end(index)),
3169 None => (note.tie_start, note.tie_end),
3170 };
3171 let mut articulation_scale = 1.0;
3172 let mut staccato = false;
3173 for articulation in ¬e.articulations {
3174 match articulation {
3175 Articulation::Staccato | Articulation::Staccatissimo => {
3176 articulation_scale *= 0.5;
3177 staccato = true;
3178 }
3179 Articulation::Fermata
3180 if options.fermata_multiplier.is_finite()
3181 && options.fermata_multiplier < 1.0 =>
3182 {
3183 articulation_scale *= options.fermata_multiplier.max(0.0)
3184 }
3185 _ => {}
3186 }
3187 }
3188 let notated_duration = if articulation_scale.is_finite()
3189 && articulation_scale > 0.0
3190 && event.duration_beats.is_finite()
3191 {
3192 event.duration_beats / articulation_scale
3193 } else {
3194 note.beats()
3195 };
3196 let source_staff = source.staff;
3197 let source_voice = source.voice;
3198 let key = (source.part, event.pitch_midi_cents);
3199 if let Some(candidates) = pending.get_mut(&key) {
3200 candidates.retain(|candidate| candidate.notated_end_beats >= event.time_beats - 1e-9);
3201 if tie_end {
3202 let matching = candidates
3203 .iter()
3204 .enumerate()
3205 .filter(|(_, candidate)| {
3206 (candidate.notated_end_beats - event.time_beats).abs() < 1e-9
3207 })
3208 .max_by_key(|(_, candidate)| {
3209 (
3210 candidate.staff == source.staff,
3211 candidate.voice == source.voice,
3212 )
3213 })
3214 .map(|(position, _)| position);
3215 if let Some(position) = matching {
3216 let mut candidate = candidates.remove(position);
3217 let previous = &mut merged[candidate.index];
3218 for articulation in event.articulations.iter().filter(|articulation| {
3219 matches!(
3220 articulation,
3221 Articulation::Staccato | Articulation::Staccatissimo
3222 )
3223 }) {
3224 if !previous.articulations.contains(articulation) {
3225 previous.articulations.push(articulation.clone());
3226 }
3227 }
3228 candidate.notated_end_beats = event.time_beats + notated_duration;
3229 candidate.staccato |= staccato;
3230 previous.duration_beats += event.duration_beats;
3231 previous.duration_secs += event.duration_secs;
3232 if candidate.staccato {
3233 previous.duration_beats =
3234 (candidate.notated_end_beats - previous.time_beats) * 0.5;
3235 let timeline =
3236 clock.get_or_insert_with(|| playback_measure_clock(score, options));
3237 if let Some(end_secs) = playback_secs_at_beat(
3238 timeline,
3239 previous.time_beats + previous.duration_beats,
3240 ) {
3241 previous.duration_secs = end_secs - previous.time_secs;
3242 }
3243 }
3244 if tie_start {
3245 candidate.staff = source.staff;
3246 candidate.voice = source.voice;
3247 candidates.push(candidate);
3248 }
3249 continue;
3250 }
3251 }
3252 }
3253
3254 let index = merged.len();
3255 merged.push(event);
3256 if tie_start {
3257 pending.entry(key).or_default().push(PendingTie {
3258 index,
3259 staff: source_staff,
3260 voice: source_voice,
3261 notated_end_beats: merged[index].time_beats + notated_duration,
3262 staccato,
3263 });
3264 }
3265 }
3266 merged
3267}
3268
3269#[derive(Clone, Copy)]
3270struct PlaybackClockMeasure {
3271 start_beats: f64,
3272 start_secs: f64,
3273 bpm: f64,
3274 ramp_to_bpm: Option<f64>,
3275 beats: f64,
3276}
3277
3278fn playback_measure_clock(score: &Score, options: &PlaybackOptions) -> Vec<PlaybackClockMeasure> {
3279 let marks = score_tempo_marks(score);
3280 let mut bpm = initial_tempo_bpm(score, options);
3281 let mut start_beats = 0.0;
3282 let mut start_secs = 0.0;
3283 let mut clock = Vec::new();
3284 for index in measure_sequence(score).into_iter().filter(|index| {
3285 options
3286 .loop_region
3287 .is_none_or(|(start, end)| *index >= start && *index <= end)
3288 }) {
3289 let mark = marks.get(index).copied().unwrap_or_default();
3290 if let Some(next) = mark.bpm.and_then(|tempo| written_tempo_bpm(options, tempo)) {
3291 bpm = next;
3292 }
3293 let beats = score.playback_measure_beats(index);
3294 let ramp_to_bpm = written_tempo_ramp_bpm(options, mark.ramp_to_bpm);
3295 clock.push(PlaybackClockMeasure {
3296 start_beats,
3297 start_secs,
3298 bpm,
3299 ramp_to_bpm,
3300 beats,
3301 });
3302 start_beats += beats;
3303 start_secs += tempo_ramp_seconds(bpm, ramp_to_bpm, beats, beats);
3304 if let Some(next) = ramp_to_bpm {
3305 bpm = next;
3306 }
3307 }
3308 clock
3309}
3310
3311#[derive(Clone, Copy, Debug)]
3313struct FermataHold {
3314 at_beat: f64,
3315 at_secs: f64,
3316 extra_beats: f64,
3317 extra_secs: f64,
3318 cumulative_beats: f64,
3319 cumulative_secs: f64,
3320}
3321
3322fn score_fermata_holds(score: &Score, options: &PlaybackOptions) -> Vec<FermataHold> {
3323 let multiplier = options.fermata_multiplier;
3324 if !multiplier.is_finite()
3325 || multiplier <= 1.0
3326 || !score
3327 .parts
3328 .iter()
3329 .flat_map(|part| &part.staves)
3330 .flat_map(|staff| &staff.measures)
3331 .flat_map(|measure| &measure.voices)
3332 .flatten()
3333 .any(|note| note.articulations.contains(&Articulation::Fermata))
3334 {
3335 return Vec::new();
3336 }
3337 let sequence = measure_sequence(score).into_iter().filter(|index| {
3338 options
3339 .loop_region
3340 .is_none_or(|(start, end)| *index >= start && *index <= end)
3341 });
3342 let clock = playback_measure_clock(score, options);
3343 let swing = options
3344 .swing
3345 .filter(|ratio| ratio.is_finite() && *ratio > 0.5 && *ratio < 1.0);
3346 let mut holds = Vec::new();
3347 for (index, measure_clock) in sequence.zip(clock.iter()) {
3348 for staff in score.parts.iter().flat_map(|part| &part.staves) {
3349 let Some(measure) = staff.measures.get(index) else {
3350 continue;
3351 };
3352 let grid_offset = if index == 0 {
3353 let meter = staff
3354 .meter_at(index, &score.settings.time_signature)
3355 .total_beats();
3356 (meter - measure_clock.beats).max(0.0)
3357 } else {
3358 0.0
3359 };
3360 for voice in &measure.voices {
3361 let (mut local, mut written) = (0.0, 0.0);
3362 let mut swing_second = false;
3363 for (note_index, note) in voice.iter().enumerate() {
3364 if note.is_grace || note.is_cue {
3365 continue;
3366 }
3367 let duration = if let Some(ratio) = swing {
3368 if swing_second {
3369 swing_second = false;
3370 (1.0 - ratio) * note.beats() * 2.0
3371 } else if starts_swing_pair(
3372 voice,
3373 note_index,
3374 written,
3375 grid_offset,
3376 measure_clock.beats,
3377 &options.swing_unit,
3378 ) {
3379 swing_second = true;
3380 ratio * note.beats() * 2.0
3381 } else {
3382 note.beats()
3383 }
3384 } else {
3385 note.beats()
3386 };
3387 written += note.beats();
3388 local += duration;
3389 if note.articulations.contains(&Articulation::Fermata) && duration > 0.0 {
3390 let at_beat = measure_clock.start_beats + local;
3391 let Some(at_secs) = playback_secs_at_beat(&clock, at_beat) else {
3392 continue;
3393 };
3394 let bpm = measure_clock.ramp_to_bpm.map_or(measure_clock.bpm, |end| {
3395 measure_clock.bpm
3396 + (end - measure_clock.bpm)
3397 * (local / measure_clock.beats.max(f64::EPSILON))
3398 .clamp(0.0, 1.0)
3399 });
3400 let extra_beats = duration * (multiplier - 1.0);
3401 holds.push(FermataHold {
3402 at_beat,
3403 at_secs,
3404 extra_beats,
3405 extra_secs: extra_beats * 60.0 / bpm,
3406 cumulative_beats: 0.0,
3407 cumulative_secs: 0.0,
3408 });
3409 }
3410 }
3411 }
3412 }
3413 }
3414 holds.sort_by(|a, b| a.at_beat.total_cmp(&b.at_beat));
3415 let mut unique: Vec<FermataHold> = Vec::new();
3416 for hold in holds {
3417 if let Some(last) = unique.last_mut()
3418 && (last.at_beat - hold.at_beat).abs() < 1e-9
3419 {
3420 if hold.extra_beats > last.extra_beats {
3421 *last = hold;
3422 }
3423 } else {
3424 unique.push(hold);
3425 }
3426 }
3427 let (mut beats, mut seconds) = (0.0, 0.0);
3428 for hold in &mut unique {
3429 beats += hold.extra_beats;
3430 seconds += hold.extra_secs;
3431 hold.cumulative_beats = beats;
3432 hold.cumulative_secs = seconds;
3433 }
3434 unique
3435}
3436
3437pub(super) fn fermata_total_secs(score: &Score, region: Option<(usize, usize)>) -> f64 {
3438 let options = PlaybackOptions {
3439 loop_region: region,
3440 ..Default::default()
3441 };
3442 score_fermata_holds(score, &options)
3443 .last()
3444 .map_or(0.0, |hold| hold.cumulative_secs)
3445}
3446
3447pub fn playback_fermata_pauses(score: &Score, options: &PlaybackOptions) -> Vec<(f64, f64)> {
3450 score_fermata_holds(score, options)
3451 .into_iter()
3452 .map(|hold| (hold.at_beat, hold.extra_beats))
3453 .collect()
3454}
3455
3456fn apply_fermata_holds(events: &mut [PlaybackEvent], holds: &[FermataHold]) {
3457 let before = |index: usize| index.checked_sub(1).and_then(|i| holds.get(i));
3458 for event in events {
3459 let start = event.time_beats;
3460 let end = start + event.duration_beats;
3461 let first = holds.partition_point(|hold| hold.at_beat <= start + 1e-9);
3462 let last = holds.partition_point(|hold| hold.at_beat <= end + 1e-9);
3463 let start_beats = before(first).map_or(0.0, |hold| hold.cumulative_beats);
3464 let start_secs = before(first).map_or(0.0, |hold| hold.cumulative_secs);
3465 let end_beats = before(last).map_or(0.0, |hold| hold.cumulative_beats);
3466 let end_secs = before(last).map_or(0.0, |hold| hold.cumulative_secs);
3467 event.time_beats += start_beats;
3468 event.time_secs += start_secs;
3469 event.duration_beats += end_beats - start_beats;
3470 event.duration_secs += end_secs - start_secs;
3471 }
3472}
3473
3474fn fermata_warp_secs(seconds: f64, holds: &[FermataHold]) -> f64 {
3475 let count = holds.partition_point(|hold| hold.at_secs <= seconds + 1e-9);
3476 seconds
3477 + count
3478 .checked_sub(1)
3479 .map_or(0.0, |index| holds[index].cumulative_secs)
3480}
3481
3482fn fermata_unwarp_secs(seconds: f64, holds: &[FermataHold]) -> f64 {
3483 let count = holds
3484 .partition_point(|hold| hold.at_secs + hold.cumulative_secs - hold.extra_secs <= seconds);
3485 if count == 0 {
3486 return seconds;
3487 }
3488 let hold = holds[count - 1];
3489 let start = hold.at_secs + hold.cumulative_secs - hold.extra_secs;
3490 if seconds < start + hold.extra_secs {
3491 hold.at_secs
3492 } else {
3493 seconds - hold.cumulative_secs
3494 }
3495}
3496
3497fn playback_secs_at_beat(clock: &[PlaybackClockMeasure], beat: f64) -> Option<f64> {
3498 let index = clock.partition_point(|measure| measure.start_beats <= beat);
3499 let measure = clock.get(index.saturating_sub(1))?;
3500 let offset = beat - measure.start_beats;
3501 if offset > measure.beats + 1e-9 {
3502 return None;
3503 }
3504 Some(
3505 measure.start_secs
3506 + tempo_ramp_seconds(
3507 measure.bpm,
3508 measure.ramp_to_bpm,
3509 measure.beats,
3510 offset.clamp(0.0, measure.beats),
3511 ),
3512 )
3513}
3514
3515pub fn to_playback_events_bounded(
3517 score: &Score,
3518 options: &PlaybackOptions,
3519) -> Result<Vec<PlaybackEvent>, crate::Error> {
3520 let events = to_playback_events(score, options);
3521 if events.len() > MAX_PLAYBACK_COMPARISON_EVENTS {
3522 return Err(crate::Error::PlaybackComparisonTooLarge(events.len()));
3523 }
3524 Ok(events)
3525}
3526
3527#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3529pub struct SharedUnisonPlaybackEvent {
3530 pub event: PlaybackEvent,
3531 pub sources: Vec<NoteAddr>,
3532}
3533
3534pub fn coalesce_shared_unisons(events: Vec<PlaybackEvent>) -> Vec<SharedUnisonPlaybackEvent> {
3541 use std::collections::HashMap;
3542 let mut groups: Vec<SharedUnisonPlaybackEvent> = Vec::with_capacity(events.len());
3543 let mut buckets: HashMap<(usize, u64, i32, u8, u8), Vec<usize>> = HashMap::new();
3544 for event in events {
3545 let Some(source) = event.source.clone().filter(|_| !event.is_metronome) else {
3546 groups.push(SharedUnisonPlaybackEvent {
3547 event,
3548 sources: Vec::new(),
3549 });
3550 continue;
3551 };
3552 let key = (
3553 event.part_index,
3554 event.time_beats.to_bits(),
3555 event.pitch_midi_cents,
3556 event.channel,
3557 event.program,
3558 );
3559 let mut normalized = event.clone();
3560 normalized.address = None;
3561 normalized.source = None;
3562 normalized.source_voice_number = None;
3563 let candidates = buckets.entry(key).or_default();
3564 let matching = candidates.iter().take(256).copied().find(|&index| {
3566 let group = &groups[index];
3567 if group.sources.len() >= 256
3568 || group
3569 .sources
3570 .iter()
3571 .any(|other| other.staff == source.staff && other.voice == source.voice)
3572 {
3573 return false;
3574 }
3575 let mut other = group.event.clone();
3576 other.address = None;
3577 other.source = None;
3578 other.source_voice_number = None;
3579 normalized == other
3580 });
3581 if let Some(index) = matching {
3582 groups[index].sources.push(source);
3583 } else {
3584 if candidates.len() < 256 {
3585 candidates.push(groups.len());
3586 }
3587 groups.push(SharedUnisonPlaybackEvent {
3588 event,
3589 sources: vec![source],
3590 });
3591 }
3592 }
3593 groups
3594}
3595
3596#[derive(Debug, Clone, Serialize, Deserialize)]
3600pub struct PlaybackPosition {
3601 pub measure_index: usize,
3603 pub beat: f64,
3605}
3606
3607struct MeasureSegment {
3608 measure_idx: usize,
3609 start_secs: f64,
3610 duration_secs: f64,
3611 beats: f64,
3612 bpm: f64,
3613 ramp_to_bpm: Option<f64>,
3614}
3615
3616fn build_measure_segments(score: &Score, options: &PlaybackOptions) -> Vec<MeasureSegment> {
3617 let init_bpm = initial_tempo_bpm(score, options);
3618 let tempo_marks = score_tempo_marks(score);
3619 let full_seq = measure_sequence(score);
3620 let seq: Vec<usize> = if let Some((lo, hi)) = options.loop_region {
3621 full_seq
3622 .into_iter()
3623 .filter(|&i| i >= lo && i <= hi)
3624 .collect()
3625 } else {
3626 full_seq
3627 };
3628
3629 let mut segments = Vec::with_capacity(seq.len());
3630 let mut cursor_secs = 0.0f64;
3631 let mut current_bpm = init_bpm;
3632
3633 for idx in seq {
3634 let mark = tempo_marks.get(idx).copied().unwrap_or_default();
3635 if let Some(t) = mark.bpm.and_then(|tempo| written_tempo_bpm(options, tempo)) {
3636 current_bpm = t;
3637 }
3638 let beats = score.playback_measure_beats(idx);
3639 let ramp_to_bpm = written_tempo_ramp_bpm(options, mark.ramp_to_bpm);
3640 let duration_secs = tempo_ramp_seconds(current_bpm, ramp_to_bpm, beats, beats);
3641
3642 segments.push(MeasureSegment {
3643 measure_idx: idx,
3644 start_secs: cursor_secs,
3645 duration_secs,
3646 beats,
3647 bpm: current_bpm,
3648 ramp_to_bpm,
3649 });
3650 cursor_secs += duration_secs;
3651 if let Some(end_bpm) = ramp_to_bpm {
3652 current_bpm = end_bpm;
3653 }
3654 }
3655 segments
3656}
3657
3658pub fn compute_playback_position(
3664 score: &Score,
3665 options: &PlaybackOptions,
3666 elapsed_secs: f64,
3667) -> Option<PlaybackPosition> {
3668 if elapsed_secs < 0.0 {
3669 return None;
3670 }
3671 let holds = score_fermata_holds(score, options);
3672 let elapsed_secs = fermata_unwarp_secs(elapsed_secs, &holds);
3673 let segments = build_measure_segments(score, options);
3674 for seg in &segments {
3675 if elapsed_secs < seg.start_secs + seg.duration_secs + 1e-9 {
3676 let beat = tempo_ramp_beats_at_seconds(
3677 seg.bpm,
3678 seg.ramp_to_bpm,
3679 seg.beats,
3680 elapsed_secs - seg.start_secs,
3681 )
3682 .clamp(0.0, seg.beats);
3683 return Some(PlaybackPosition {
3684 measure_index: seg.measure_idx,
3685 beat,
3686 });
3687 }
3688 }
3689 None
3690}
3691
3692#[cfg(test)]
3693mod tests {
3694 use super::*;
3695 use crate::model::{
3696 duration::Duration,
3697 pitch::{Pitch, Step},
3698 score::{MidiAftertouch, MidiControlChange, MidiPitchBend, MidiProgramChange, Note, Score},
3699 };
3700
3701 fn opts(bpm: Option<u16>) -> PlaybackOptions {
3702 PlaybackOptions {
3703 bpm_override: bpm,
3704 ..Default::default()
3705 }
3706 }
3707
3708 #[test]
3709 fn shared_unison_policy_never_merges_different_routing_or_same_voice() {
3710 let mut score = Score::new("unison", 120, 4, 4, 0, 1);
3711 let note = Note::new(Pitch::new(Step::A, 3), Duration::Whole);
3712 score.parts[0].staves[0].measures[0].voices[0] = vec![note.clone()];
3713 score.parts[0].staves[0].measures[0].voices[1] = vec![note];
3714 let events = to_playback_events(&score, &PlaybackOptions::default());
3715 assert_eq!(coalesce_shared_unisons(events.clone()).len(), 1);
3716 let mut different_program = events.clone();
3717 different_program[1].program += 1;
3718 assert_eq!(coalesce_shared_unisons(different_program).len(), 2);
3719 let mut different_expression = events.clone();
3720 different_expression[1].velocity += 1;
3721 assert_eq!(coalesce_shared_unisons(different_expression).len(), 2);
3722 let mut same_voice = events;
3723 let voice = same_voice[0].source.as_ref().unwrap().voice;
3724 same_voice[1].source.as_mut().unwrap().voice = voice;
3725 assert_eq!(coalesce_shared_unisons(same_voice).len(), 2);
3726 }
3727
3728 #[test]
3729 fn rest_pedal_controls_follow_swing_and_muted_parts() {
3730 let mut score = Score::new("rest pedal", 120, 2, 4, 0, 1);
3731 let first = Note::rest(Duration::Eighth);
3732 let mut second = Note::rest(Duration::Eighth);
3733 second.pedal_start = true;
3734 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3735 note.pedal_end = true;
3736 score.parts[0].staves[0].measures[0].voices[0] = vec![first, second, note];
3737 let mut options = PlaybackOptions {
3738 swing: Some(0.67),
3739 ..Default::default()
3740 };
3741 let manifest =
3742 build_offline_render_manifest(&score, &options, &OfflineRenderRequest::default())
3743 .unwrap();
3744 let controls: Vec<_> = manifest
3745 .semantic_events
3746 .iter()
3747 .filter_map(|event| match event.kind {
3748 OfflineRenderSemanticEventKind::Pedal { down } => Some((event.frame, down)),
3749 _ => None,
3750 })
3751 .collect();
3752 assert_eq!(controls, vec![(16080, true), (24000, false)]);
3753 options.muted_parts.push(0);
3754 let muted =
3755 build_offline_render_manifest(&score, &options, &OfflineRenderRequest::default())
3756 .unwrap();
3757 assert!(
3758 !muted
3759 .semantic_events
3760 .iter()
3761 .any(|event| matches!(event.kind, OfflineRenderSemanticEventKind::Pedal { .. }))
3762 );
3763 }
3764
3765 #[test]
3766 fn playback_options_accepts_an_empty_json_object_as_defaults() {
3767 let parsed: PlaybackOptions = serde_json::from_str("{}").expect("default playback options");
3768 assert_eq!(parsed, PlaybackOptions::default());
3769 }
3770
3771 #[test]
3772 fn bounded_playback_events_match_unbounded_schedule_within_limit() {
3773 let score = Score::new("bounded", 120, 4, 4, 0, 1);
3774 let options = opts(Some(120));
3775 let unbounded = to_playback_events(&score, &options);
3776 let bounded = to_playback_events_bounded(&score, &options).expect("bounded schedule");
3777 assert_eq!(bounded, unbounded);
3778 }
3779
3780 #[test]
3781 fn bars_after_a_meter_change_keep_the_new_meter() {
3782 use crate::model::duration::Duration;
3783 use crate::{Note, Pitch, Step, TimeSignature};
3784 let mut score = Score::new("meter", 120, 4, 4, 0, 3);
3785 let staff = &mut score.parts[0].staves[0];
3786 staff.measures[1].time_sig = Some(TimeSignature {
3787 numerator: 3,
3788 denominator: 4,
3789 });
3790 for measure in &mut staff.measures {
3791 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3792 }
3793 let onsets: Vec<f64> = to_playback_events(&score, &opts(Some(120)))
3794 .iter()
3795 .filter(|event| event.address.is_some())
3796 .map(|event| event.time_beats)
3797 .collect();
3798 assert_eq!(onsets, vec![0.0, 4.0, 7.0]);
3800 }
3801
3802 #[test]
3803 fn tempo_written_on_either_part_governs_the_whole_score() {
3804 let mut score = Score::new("shared tempo", 120, 4, 4, 0, 2);
3805 let mut second = score.parts[0].clone();
3806 second.id = "second".into();
3807 score.parts.push(second);
3808 for part in &mut score.parts {
3809 for measure in &mut part.staves[0].measures {
3810 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3811 }
3812 }
3813 score.parts[0].staves[0].measures[0].tempo = Some(92);
3814 score.parts[1].staves[0].measures[1].tempo = Some(60);
3815 let options = PlaybackOptions {
3816 metronome: Some(MetronomeConfig::default()),
3817 ..Default::default()
3818 };
3819 let events = to_playback_events(&score, &options);
3820 for part_index in 0..2 {
3821 let first = events
3822 .iter()
3823 .find(|event| {
3824 event.part_index == part_index
3825 && event
3826 .source
3827 .as_ref()
3828 .is_some_and(|source| source.measure == 0)
3829 })
3830 .unwrap();
3831 let second = events
3832 .iter()
3833 .find(|event| {
3834 event.part_index == part_index
3835 && event
3836 .source
3837 .as_ref()
3838 .is_some_and(|source| source.measure == 1)
3839 })
3840 .unwrap();
3841 assert!((first.duration_secs - 60.0 / 92.0).abs() < 1e-9);
3842 assert!((second.time_secs - 4.0 * 60.0 / 92.0).abs() < 1e-9);
3843 assert!((second.duration_secs - 1.0).abs() < 1e-9);
3844 }
3845 let clicks: Vec<_> = events.iter().filter(|event| event.is_metronome).collect();
3846 assert!((clicks[1].time_secs - 60.0 / 92.0).abs() < 1e-9);
3847 assert!((clicks[4].time_secs - 4.0 * 60.0 / 92.0).abs() < 1e-9);
3848 let position = compute_playback_position(&score, &options, 1.5 * 60.0 / 92.0).unwrap();
3849 assert_eq!(position.measure_index, 0);
3850 assert!((position.beat - 1.5).abs() < 1e-9);
3851 let timeline_1 = offline_measure_times(&score, &options, 1, 0, &score_tempo_marks(&score));
3852 assert!((timeline_1[0].bpm - 92.0).abs() < 1e-9);
3853 assert!((timeline_1[1].bpm - 60.0).abs() < 1e-9);
3854 }
3855
3856 #[test]
3857 fn empty_score_no_events() {
3858 let score = Score::new("T", 120, 4, 4, 0, 1);
3859 assert!(to_playback_events(&score, &opts(None)).is_empty());
3860 }
3861
3862 #[test]
3863 fn single_note_at_beat_zero() {
3864 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3865 score.parts[0].staves[0].measures[0].voices[0] =
3866 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3867 let events = to_playback_events(&score, &opts(None));
3868 assert_eq!(events.len(), 1);
3869 assert_eq!(events[0].address.as_deref(), Some("0:0:0:0:0"));
3870 assert_eq!(
3871 events[0].source,
3872 Some(NoteAddr {
3873 part: 0,
3874 staff: 0,
3875 measure: 0,
3876 voice: 0,
3877 note: 0,
3878 })
3879 );
3880 assert!((events[0].time_beats).abs() < 1e-9);
3881 assert_eq!(events[0].pitch_midi, 60);
3882 assert_eq!(events[0].velocity, 64);
3883 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
3884 assert_eq!(events[0].part_index, 0);
3885 }
3886
3887 #[test]
3888 fn tied_notes_are_one_continuous_playback_event() {
3889 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3890 let mut first = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3891 first.tie_start = true;
3892 let mut second = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3893 second.tie_end = true;
3894 score.parts[0].staves[0].measures[0].voices[0] = vec![first, second];
3895
3896 let events = to_playback_events(&score, &opts(None));
3897 assert_eq!(events.len(), 1);
3898 assert_eq!(
3899 events[0].source.as_ref().map(|address| address.note),
3900 Some(0)
3901 );
3902 assert!((events[0].time_beats).abs() < 1e-9);
3903 assert!((events[0].duration_beats - 2.0).abs() < 1e-9);
3904 assert!((events[0].duration_secs - 1.0).abs() < 1e-9);
3905 }
3906
3907 #[test]
3908 fn staccato_tied_across_barline_has_one_half_length_attack() {
3909 for marks in [[true, false], [false, true], [true, true]] {
3910 let mut score = Score::new("tied staccato", 120, 4, 4, 0, 2);
3911 let mut first = Note::new(Pitch::new(Step::D, 5), Duration::Eighth);
3912 first.tie_start = true;
3913 let mut second = Note::new(Pitch::new(Step::D, 5), Duration::Eighth);
3914 second.tie_end = true;
3915 if marks[0] {
3916 first.articulations.push(Articulation::Staccato);
3917 }
3918 if marks[1] {
3919 second.articulations.push(Articulation::Staccato);
3920 }
3921 score.parts[0].staves[0].measures[0].voices[0] = vec![
3922 Note::rest(Duration::Half),
3923 Note::rest(Duration::Quarter),
3924 Note::rest(Duration::Eighth),
3925 first,
3926 ];
3927 score.parts[0].staves[0].measures[1].voices[0] = vec![second];
3928 let events = to_playback_events(&score, &opts(None));
3929 assert_eq!(events.len(), 1, "articulations: {marks:?}");
3930 assert_eq!(events[0].pitch_midi, 74);
3931 assert_eq!(events[0].time_beats, 3.5);
3932 assert!(events[0].articulations.contains(&Articulation::Staccato));
3933 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
3934 assert!((events[0].duration_secs - 0.25).abs() < 1e-9);
3935 }
3936 }
3937
3938 #[test]
3939 fn tied_staccato_uses_tempo_at_sounding_end() {
3940 let mut score = Score::new("tied tempo", 120, 4, 4, 0, 2);
3941 let mut first = Note::new(Pitch::new(Step::D, 5), Duration::Eighth);
3942 first.tie_start = true;
3943 first.articulations.push(Articulation::Staccato);
3944 let mut second = Note::new(Pitch::new(Step::D, 5), Duration::Quarter);
3945 second.dot_count = 1;
3946 second.tie_end = true;
3947 score.parts[0].staves[0].measures[0].voices[0] = vec![
3948 Note::rest(Duration::Half),
3949 Note::rest(Duration::Quarter),
3950 Note::rest(Duration::Eighth),
3951 first,
3952 ];
3953 score.parts[0].staves[0].measures[1].tempo = Some(60);
3954 score.parts[0].staves[0].measures[1].voices[0] = vec![second];
3955 let events = to_playback_events(&score, &opts(None));
3956 assert_eq!(events.len(), 1);
3957 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
3958 assert!((events[0].duration_secs - 0.75).abs() < 1e-9);
3959 }
3960
3961 #[test]
3962 fn a_tie_on_one_chord_pitch_holds_only_that_pitch() {
3963 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3964 let chord = || {
3966 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3967 note.pitches.push(Pitch::new(Step::G, 4));
3968 note
3969 };
3970 let mut first = chord();
3971 first.set_pitch_ties(&[true, false], &[false, false]);
3972 let mut second = chord();
3973 second.set_pitch_ties(&[false, false], &[true, false]);
3974 assert!(first.tie_start && second.tie_end);
3975 score.parts[0].staves[0].measures[0].voices[0] = vec![first, second];
3976
3977 let events = to_playback_events(&score, &opts(None));
3978 let durations = |midi: u8| {
3979 events
3980 .iter()
3981 .filter(|event| event.pitch_midi == midi)
3982 .map(|event| event.duration_beats)
3983 .collect::<Vec<_>>()
3984 };
3985 assert_eq!(durations(60), vec![2.0]);
3986 assert_eq!(durations(67), vec![1.0, 1.0]);
3987 }
3988
3989 #[test]
3990 fn ties_cross_measure_boundaries_and_tempo_changes_without_retriggering() {
3991 let mut score = Score::new("T", 120, 4, 4, 0, 2);
3992 let first_measure = &mut score.parts[0].staves[0].measures[0];
3993 let mut first = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
3994 first.tie_start = true;
3995 first_measure.voices[0] = vec![first];
3996
3997 let second_measure = &mut score.parts[0].staves[0].measures[1];
3998 second_measure.tempo = Some(60);
3999 let mut second = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
4000 second.tie_end = true;
4001 second_measure.voices[0] = vec![second];
4002
4003 let events = to_playback_events(&score, &opts(None));
4004 assert_eq!(events.len(), 1);
4005 assert_eq!(
4006 events[0].source.as_ref().map(|address| address.measure),
4007 Some(0)
4008 );
4009 assert!((events[0].time_beats).abs() < 1e-9);
4010 assert!((events[0].duration_beats - 8.0).abs() < 1e-9);
4011 assert!((events[0].duration_secs - 6.0).abs() < 1e-9);
4012 }
4013
4014 #[test]
4015 fn ties_continue_across_voice_and_staff_changes() {
4016 let whole_c = || Note::new(Pitch::new(Step::C, 4), Duration::Whole);
4017
4018 let mut voice_score = Score::new("voice change", 120, 4, 4, 0, 2);
4019 let mut start = whole_c();
4020 start.tie_start = true;
4021 let mut end = whole_c();
4022 end.tie_end = true;
4023 voice_score.parts[0].staves[0].measures[0].voices[0] = vec![start.clone()];
4024 voice_score.parts[0].staves[0].measures[1].voices[1] = vec![end.clone()];
4025 let events = to_playback_events(&voice_score, &opts(None));
4026 assert_eq!(events.len(), 1);
4027 assert_eq!(events[0].time_beats, 0.0);
4028 assert_eq!(events[0].duration_beats, 8.0);
4029
4030 let mut staff_score = Score::new("staff change", 120, 4, 4, 0, 2);
4031 let second_staff = staff_score.parts[0].staves[0].clone();
4032 staff_score.parts[0].staves.push(second_staff);
4033 staff_score.parts[0].staves[0].measures[0].voices[0] = vec![start];
4034 staff_score.parts[0].staves[1].measures[1].voices[0] = vec![end];
4035 let events = to_playback_events(&staff_score, &opts(None));
4036 assert_eq!(events.len(), 1);
4037 assert_eq!(events[0].time_beats, 0.0);
4038 assert_eq!(events[0].duration_beats, 8.0);
4039 }
4040
4041 #[test]
4042 fn cross_voice_tie_keeps_independent_unison_attacks() {
4043 let mut score = Score::new("unison", 120, 4, 4, 0, 2);
4044 let mut start = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
4045 start.tie_start = true;
4046 let mut end = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
4047 end.tie_end = true;
4048 score.parts[0].staves[0].measures[0].voices[0] = vec![start];
4049 score.parts[0].staves[0].measures[0].voices[1] =
4050 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4051 score.parts[0].staves[0].measures[1].voices[1] = vec![end];
4052 score.parts[0].staves[0].measures[1].voices[2] =
4053 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4054
4055 let mut durations: Vec<_> = to_playback_events(&score, &opts(None))
4056 .into_iter()
4057 .map(|event| event.duration_beats)
4058 .collect();
4059 durations.sort_by(f64::total_cmp);
4060 assert_eq!(durations, vec![4.0, 4.0, 8.0]);
4061 }
4062
4063 #[test]
4064 fn malformed_tie_end_does_not_drop_playback_event() {
4065 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4066 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4067 note.tie_end = true;
4068 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4069 let events = to_playback_events(&score, &opts(None));
4070 assert_eq!(events.len(), 1);
4071 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
4072 }
4073
4074 #[test]
4075 fn microtonal_playback_event_keeps_exact_midi_cents() {
4076 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4077 score.parts[0].staves[0].measures[0].voices[0] = vec![Note::new(
4078 Pitch::with_microtone(Step::C, 4, 0, 50),
4079 Duration::Quarter,
4080 )];
4081 let events = to_playback_events(&score, &opts(None));
4082 assert_eq!(events[0].pitch_midi, 61);
4083 assert_eq!(events[0].pitch_midi_cents, 6050);
4084 }
4085
4086 #[test]
4087 fn chord_expands_to_multiple_events() {
4088 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4089 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4090 note.pitches.push(Pitch::new(Step::E, 4));
4091 note.pitches.push(Pitch::new(Step::G, 4));
4092 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4093 let events = to_playback_events(&score, &opts(None));
4094 assert_eq!(events.len(), 3);
4095 assert!(
4096 events
4097 .iter()
4098 .all(|event| event.address.as_deref() == Some("0:0:0:0:0"))
4099 );
4100 assert!(events.iter().all(|e| e.time_beats.abs() < 1e-9));
4101 }
4102
4103 #[test]
4104 fn playback_events_preserve_authored_chord_symbol_without_inventing_notes() {
4105 let mut score = Score::new("Harmony", 120, 4, 4, 0, 1);
4106 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4107 note.chord_symbol = Some(ChordSymbol {
4108 root: "C".into(),
4109 kind: "major-seventh".into(),
4110 bass: Some("E".into()),
4111 placement: None,
4112 extender: false,
4113 harmonic_degree: None,
4114 harmony_function: None,
4115 harmony_type: None,
4116 chord_ref: None,
4117 range_end: None,
4118 degrees: Vec::new(),
4119 });
4120 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4121
4122 let events = to_playback_events(&score, &PlaybackOptions::default());
4123 assert_eq!(events.len(), 1);
4124 assert_eq!(events[0].pitch_midi, 60);
4125 assert_eq!(
4126 events[0]
4127 .chord_symbol
4128 .as_ref()
4129 .map(ChordSymbol::display_text),
4130 Some("Cmaj7/E".into())
4131 );
4132 }
4133
4134 #[test]
4135 fn authored_realization_default_keeps_ornament_as_one_semantic_event() {
4136 let mut score = Score::new("authored ornament", 120, 4, 4, 0, 1);
4137 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4138 note.articulations.push(Articulation::Trill);
4139 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4140
4141 let events = to_playback_events(&score, &PlaybackOptions::default());
4142 assert_eq!(events.len(), 1);
4143 assert_eq!(events[0].pitch_midi, 60);
4144 assert_eq!(events[0].articulations, vec![Articulation::Trill]);
4145 }
4146
4147 #[test]
4148 fn ornament_arpeggio_profile_realizes_trill_with_pinned_timing() {
4149 let mut score = Score::new("realized ornament", 120, 4, 4, 0, 1);
4150 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4151 note.articulations.push(Articulation::Trill);
4152 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4153
4154 let events = to_playback_events(
4155 &score,
4156 &PlaybackOptions {
4157 realization_profile: PlaybackRealizationProfile::OrnamentArpeggioV1,
4158 ..Default::default()
4159 },
4160 );
4161 assert_eq!(events.len(), 4);
4162 assert_eq!(
4163 events
4164 .iter()
4165 .map(|event| event.pitch_midi)
4166 .collect::<Vec<_>>(),
4167 vec![60, 61, 60, 61]
4168 );
4169 for (index, event) in events.iter().enumerate() {
4170 assert!((event.time_beats - index as f64 * 0.25).abs() < 1e-9);
4171 assert!((event.duration_beats - 0.25).abs() < 1e-9);
4172 assert!((event.time_secs - index as f64 * 0.125).abs() < 1e-9);
4173 assert!((event.duration_secs - 0.125).abs() < 1e-9);
4174 assert!(!event.articulations.contains(&Articulation::Trill));
4175 }
4176 }
4177
4178 #[test]
4179 fn ornament_arpeggio_profile_staggers_chord_in_authored_direction() {
4180 let mut score = Score::new("realized arpeggio", 120, 4, 4, 0, 1);
4181 let mut chord = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4182 chord.pitches.push(Pitch::new(Step::E, 4));
4183 chord.pitches.push(Pitch::new(Step::G, 4));
4184 chord.arpeggiate = Some(false);
4185 score.parts[0].staves[0].measures[0].voices[0] = vec![chord];
4186
4187 let events = to_playback_events(
4188 &score,
4189 &PlaybackOptions {
4190 realization_profile: PlaybackRealizationProfile::OrnamentArpeggioV1,
4191 ..Default::default()
4192 },
4193 );
4194 assert_eq!(events.len(), 3);
4195 let by_pitch: BTreeMap<_, _> = events
4196 .iter()
4197 .map(|event| (event.pitch_midi, event.time_beats))
4198 .collect();
4199 assert!((by_pitch[&67] - 0.0).abs() < 1e-9);
4200 assert!((by_pitch[&64] - 0.09).abs() < 1e-9);
4201 assert!((by_pitch[&60] - 0.18).abs() < 1e-9);
4202 }
4203
4204 #[test]
4205 fn written_grace_steals_time_from_preceding_note() {
4206 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4207 let previous = Note::new(Pitch::new(Step::F, 4), Duration::Half);
4208 let mut grace = Note::new(Pitch::new(Step::B, 4), Duration::Eighth);
4209 grace.is_grace = true;
4210 let regular = Note::new(Pitch::new(Step::A, 4), Duration::Quarter);
4211 score.parts[0].staves[0].measures[0].voices[0] = vec![previous, grace, regular];
4212 let events = to_playback_events(&score, &opts(None));
4213 assert_eq!(events.len(), 3);
4214 assert_eq!(
4215 events
4216 .iter()
4217 .map(|event| event.pitch_midi)
4218 .collect::<Vec<_>>(),
4219 vec![65, 71, 69]
4220 );
4221 assert!((events[0].duration_beats - 1.875).abs() < 1e-9);
4222 assert!((events[1].time_beats - 1.875).abs() < 1e-9);
4223 assert!((events[1].duration_beats - 0.125).abs() < 1e-9);
4224 assert!((events[2].time_beats - 2.0).abs() < 1e-9);
4225 assert!((events[1].time_secs - 0.9375).abs() < 1e-9);
4226 }
4227
4228 #[test]
4229 fn opening_grace_borrows_from_main_note_without_moving_later_notes() {
4230 let mut score = Score::new("opening grace", 120, 4, 4, 0, 1);
4231 let mut grace = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
4232 grace.is_grace = true;
4233 let main = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4234 let next = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
4235 score.parts[0].staves[0].measures[0].voices[0] = vec![grace, main, next];
4236 let events = to_playback_events(&score, &opts(None));
4237 assert_eq!(events.len(), 3);
4238 assert_eq!(
4239 events
4240 .iter()
4241 .map(|event| event.pitch_midi)
4242 .collect::<Vec<_>>(),
4243 vec![62, 60, 64]
4244 );
4245 assert!((events[0].time_beats).abs() < 1e-9);
4246 assert!((events[0].duration_beats - 0.125).abs() < 1e-9);
4247 assert!((events[1].time_beats - 0.125).abs() < 1e-9);
4248 assert!((events[1].duration_beats - 0.875).abs() < 1e-9);
4249 assert!((events[2].time_beats - 1.0).abs() < 1e-9);
4250 }
4251
4252 #[test]
4253 fn consecutive_graces_are_ordered_and_cue_notes_remain_silent() {
4254 let mut score = Score::new("two graces", 120, 4, 4, 0, 1);
4255 let mut first_grace = Note::new(Pitch::new(Step::D, 4), Duration::Sixteenth);
4256 first_grace.is_grace = true;
4257 let mut second_grace = Note::new(Pitch::new(Step::E, 4), Duration::Sixteenth);
4258 second_grace.is_grace = true;
4259 let mut cue = Note::new(Pitch::new(Step::B, 4), Duration::Eighth);
4260 cue.is_cue = true;
4261 score.parts[0].staves[0].measures[0].voices[0] = vec![
4262 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
4263 first_grace,
4264 second_grace,
4265 Note::new(Pitch::new(Step::F, 4), Duration::Quarter),
4266 cue,
4267 ];
4268 let events = to_playback_events(&score, &opts(None));
4269 assert_eq!(
4270 events
4271 .iter()
4272 .map(|event| event.pitch_midi)
4273 .collect::<Vec<_>>(),
4274 vec![60, 62, 64, 65]
4275 );
4276 assert_eq!(
4277 events
4278 .iter()
4279 .map(|event| event.time_beats)
4280 .collect::<Vec<_>>(),
4281 vec![0.0, 0.875, 0.9375, 1.0]
4282 );
4283 assert!((events[0].duration_beats - 0.875).abs() < 1e-9);
4284 }
4285
4286 #[test]
4287 fn metronome_events_have_no_source_address() {
4288 let score = Score::new("T", 120, 4, 4, 0, 1);
4289 let options = PlaybackOptions {
4290 metronome: Some(MetronomeConfig::default()),
4291 ..Default::default()
4292 };
4293 let events = to_playback_events(&score, &options);
4294 assert!(!events.is_empty());
4295 assert!(events.iter().all(|event| event.address.is_none()));
4296 }
4297
4298 #[test]
4299 fn second_note_has_correct_time() {
4300 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4301 score.parts[0].staves[0].measures[0].voices[0] = vec![
4302 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
4303 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
4304 ];
4305 let events = to_playback_events(&score, &opts(None));
4306 assert_eq!(events.len(), 2);
4307 assert!((events[0].time_beats).abs() < 1e-9);
4308 assert!((events[1].time_beats - 1.0).abs() < 1e-9);
4309 }
4310
4311 #[test]
4312 fn sparse_voice_keeps_measure_boundaries() {
4313 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4314 let note = || Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4315 score.parts[0].staves[0].measures[0].voices[1] = vec![note()];
4316 score.parts[0].staves[0].measures[1].voices[1] = vec![note()];
4317
4318 let events = to_playback_events(&score, &opts(None));
4319 assert_eq!(events.len(), 2);
4320 assert!((events[0].time_beats).abs() < 1e-9);
4321 assert!((events[1].time_beats - 4.0).abs() < 1e-9);
4322 assert!((events[1].time_secs - 2.0).abs() < 1e-9);
4323 }
4324
4325 #[test]
4326 fn time_secs_120_bpm_quarter_note_is_half_second() {
4327 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4328 score.parts[0].staves[0].measures[0].voices[0] =
4329 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4330 let events = to_playback_events(&score, &opts(None));
4331 assert!((events[0].time_secs).abs() < 1e-9);
4332 assert!((events[0].duration_secs - 0.5).abs() < 1e-9);
4333 }
4334
4335 #[test]
4336 fn bpm_override_changes_time_secs() {
4337 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4338 score.parts[0].staves[0].measures[0].voices[0] = vec![
4339 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
4340 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
4341 ];
4342 let events = to_playback_events(&score, &opts(Some(60)));
4343 assert!((events[0].time_secs).abs() < 1e-9);
4344 assert!((events[1].time_secs - 1.0).abs() < 1e-9);
4345 }
4346
4347 #[test]
4348 fn tempo_multiplier_preserves_written_tempo_changes_and_ramps() {
4349 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4350 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
4351 score.parts[0].staves[0].measures[1].tempo = Some(90);
4352 score.parts[0].staves[0].measures[0].voices[0] =
4353 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4354 score.parts[0].staves[0].measures[1].voices[0] =
4355 vec![Note::new(Pitch::new(Step::D, 4), Duration::Quarter)];
4356 let options = PlaybackOptions {
4357 tempo_multiplier: 0.5,
4358 ..Default::default()
4359 };
4360
4361 let events = to_playback_events(&score, &options);
4362 let second_measure_start = tempo_ramp_seconds(60.0, Some(30.0), 4.0, 4.0);
4363 assert!((events[1].time_secs - second_measure_start).abs() < 1e-9);
4364
4365 let one_beat_into_ramp = tempo_ramp_seconds(60.0, Some(30.0), 4.0, 1.0);
4366 let position = compute_playback_position(&score, &options, one_beat_into_ramp).unwrap();
4367 assert_eq!(position.measure_index, 0);
4368 assert!((position.beat - 1.0).abs() < 1e-9);
4369
4370 let second_measure_position =
4371 compute_playback_position(&score, &options, second_measure_start + 60.0 / 45.0)
4372 .unwrap();
4373 assert_eq!(second_measure_position.measure_index, 1);
4374 assert!((second_measure_position.beat - 1.0).abs() < 1e-9);
4375 }
4376
4377 #[test]
4378 fn bpm_override_suppresses_written_tempo_changes_and_ramps() {
4379 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4380 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
4381 score.parts[0].staves[0].measures[1].tempo = Some(90);
4382 score.parts[0].staves[0].measures[0].voices[0] =
4383 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4384 score.parts[0].staves[0].measures[1].voices[0] =
4385 vec![Note::new(Pitch::new(Step::D, 4), Duration::Quarter)];
4386 let options = PlaybackOptions {
4387 bpm_override: Some(120),
4388 ..Default::default()
4389 };
4390
4391 let events = to_playback_events(&score, &options);
4392 assert!((events[1].time_secs - 2.0).abs() < 1e-9);
4393 let position = compute_playback_position(&score, &options, 2.5).unwrap();
4394 assert_eq!(position.measure_index, 1);
4395 assert!((position.beat - 1.0).abs() < 1e-9);
4396 }
4397
4398 #[test]
4399 fn playback_position_tracks_repeats_rests_and_meter_changes_with_a_tempo_multiplier() {
4400 use crate::model::notation::{Barline, TimeSignature};
4401
4402 let mut score = Score::new("transport", 120, 4, 4, 0, 3);
4403 let staff = &mut score.parts[0].staves[0];
4404 staff.measures[0].barline_left = Barline::RepeatStart;
4405 staff.measures[1].time_sig = Some(TimeSignature {
4406 numerator: 3,
4407 denominator: 4,
4408 });
4409 staff.measures[2].barline_right = Barline::RepeatEnd;
4410 staff.measures[0].voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4411 staff.measures[1].voices[0] = vec![Note::rest(Duration::Quarter)];
4412 staff.measures[2].voices[0] = vec![Note::new(Pitch::new(Step::D, 4), Duration::Quarter)];
4413 let options = PlaybackOptions {
4414 tempo_multiplier: 0.5,
4415 ..Default::default()
4416 };
4417
4418 let events = to_playback_events(&score, &options);
4419 let note_starts: Vec<f64> = events
4420 .iter()
4421 .filter(|event| event.address.is_some())
4422 .map(|event| event.time_secs)
4423 .collect();
4424 assert_eq!(note_starts, vec![0.0, 7.0, 10.0, 17.0]);
4426
4427 let middle = compute_playback_position(&score, &options, 5.0).unwrap();
4428 assert_eq!(middle.measure_index, 1);
4429 assert!((middle.beat - 1.0).abs() < 1e-9);
4430
4431 let repeated = compute_playback_position(&score, &options, 10.5).unwrap();
4432 assert_eq!(repeated.measure_index, 0);
4433 assert!((repeated.beat - 0.5).abs() < 1e-9);
4434 }
4435
4436 #[test]
4437 fn transpose_semitones_shifts_midi_output() {
4438 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4439 score.parts[0].staves[0].transpose_semitones = -2;
4440 score.parts[0].staves[0].measures[0].voices[0] =
4441 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4442 let events = to_playback_events(&score, &opts(None));
4443 assert_eq!(events.len(), 1);
4444 assert_eq!(events[0].pitch_midi, 58);
4445 }
4446
4447 #[test]
4448 fn percussion_channel_9_ignores_transpose_semitones() {
4449 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4450 score.parts[0].midi_channel = 9;
4451 score.parts[0].staves[0].transpose_semitones = -2;
4452 score.parts[0].staves[0].measures[0].voices[0] =
4453 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4454 let events = to_playback_events(&score, &opts(None));
4455 assert_eq!(events.len(), 1);
4456 assert_eq!(events[0].pitch_midi, 60);
4457 }
4458
4459 #[test]
4460 fn staccato_halves_duration_beats() {
4461 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4462 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4463 note.articulations
4464 .push(crate::model::notation::Articulation::Staccato);
4465 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4466 let events = to_playback_events(&score, &opts(None));
4467 assert_eq!(events.len(), 1);
4468 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
4469 }
4470
4471 #[test]
4472 fn staccatissimo_also_halves_duration() {
4473 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4474 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4475 note.articulations
4476 .push(crate::model::notation::Articulation::Staccatissimo);
4477 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4478 let events = to_playback_events(&score, &opts(None));
4479 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
4480 }
4481
4482 #[test]
4483 fn staccato_does_not_shift_next_note_time() {
4484 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4485 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4486 n1.articulations
4487 .push(crate::model::notation::Articulation::Staccato);
4488 let n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
4489 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
4490 let events = to_playback_events(&score, &opts(None));
4491 assert_eq!(events.len(), 2);
4492 assert!((events[1].time_beats - 1.0).abs() < 1e-9);
4493 }
4494
4495 #[test]
4496 fn dynamics_hold_until_the_next_marking_across_voices_and_bars() {
4497 use crate::model::notation::Dynamic;
4498 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4499 let quarters = |dynamics: [Option<Dynamic>; 4]| -> Vec<Note> {
4500 dynamics
4501 .into_iter()
4502 .map(|dynamic| {
4503 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4504 note.dynamic = dynamic;
4505 note
4506 })
4507 .collect()
4508 };
4509 score.parts[0].staves[0].measures[0].voices[0] =
4512 quarters([Some(Dynamic::P), None, Some(Dynamic::Sf), None]);
4513 score.parts[0].staves[0].measures[0].voices[1] = vec![
4514 Note::new(Pitch::new(Step::C, 3), Duration::Half),
4515 Note::new(Pitch::new(Step::C, 3), Duration::Half),
4516 ];
4517 score.parts[0].staves[0].measures[1].voices[0] =
4518 quarters([Some(Dynamic::Fp), None, Some(Dynamic::Pf), None]);
4519 let events = to_playback_events(&score, &opts(None));
4520 let velocities = |pitch: u8| -> Vec<u8> {
4521 events
4522 .iter()
4523 .filter(|event| event.pitch_midi == pitch)
4524 .map(|event| event.velocity)
4525 .collect()
4526 };
4527 let (p, f, sf) = (
4528 Dynamic::P.to_velocity(),
4529 Dynamic::F.to_velocity(),
4530 Dynamic::Sf.to_velocity(),
4531 );
4532 assert_eq!(velocities(60), vec![p, p, sf, p, f, p, p, f]);
4533 assert_eq!(velocities(48), vec![p, sf]);
4534 assert_eq!(
4536 DynamicTimeline::default().velocity(0, 0.0, None),
4537 DynamicTimeline::DEFAULT_VELOCITY
4538 );
4539 }
4540
4541 #[test]
4542 fn hairpins_ramp_towards_the_following_marking_or_two_steps() {
4543 use crate::model::notation::{Dynamic, HairpinKind};
4544 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4545 let mut bar = |index: usize, dynamics: [Option<Dynamic>; 4], end: Option<Dynamic>| {
4546 let mut notes: Vec<Note> = dynamics
4547 .into_iter()
4548 .map(|dynamic| {
4549 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4550 note.dynamic = dynamic;
4551 note
4552 })
4553 .collect();
4554 notes[0].hairpin_start = Some(HairpinKind::Crescendo);
4555 notes[2].hairpin_end = true;
4556 notes[3].dynamic = end;
4557 score.parts[0].staves[0].measures[index].voices[0] = notes;
4558 };
4559 bar(0, [Some(Dynamic::P), None, None, None], Some(Dynamic::F));
4561 bar(1, [None, None, None, None], None);
4562 let events = to_playback_events(&score, &opts(None));
4563 let velocities: Vec<u8> = events.iter().map(|event| event.velocity).collect();
4564 assert_eq!(&velocities[..4], &[48, 60, 72, 84]);
4565 assert_eq!(&velocities[4..], &[84, 92, 100, 108]);
4566 }
4567
4568 #[test]
4569 fn explicit_dynamic_at_hairpin_end_overrides_terminal_level_in_all_voices() {
4570 use crate::{Dynamic, HairpinKind};
4571 for (hairpin, dynamic, attack, sustain) in [
4572 (HairpinKind::Crescendo, Dynamic::P, 48, 48),
4573 (HairpinKind::Decrescendo, Dynamic::F, 84, 84),
4574 (
4575 HairpinKind::Decrescendo,
4576 Dynamic::Sfz,
4577 Dynamic::Sfz.to_velocity(),
4578 40,
4579 ),
4580 (
4581 HairpinKind::Crescendo,
4582 Dynamic::Pf,
4583 Dynamic::Pf.to_velocity(),
4584 84,
4585 ),
4586 ] {
4587 let mut score = Score::new("endpoint", 120, 4, 4, 0, 2);
4588 let mut first = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
4589 first.hairpin_start = Some(hairpin);
4590 first.hairpin_end = true;
4591 score.parts[0].staves[0].measures[0].voices[0] = vec![first];
4592 let mut upper = Note::new(Pitch::new(Step::C, 5), Duration::Half);
4593 upper.dynamic = Some(dynamic);
4594 score.parts[0].staves[0].measures[1].voices[0] =
4595 vec![upper, Note::new(Pitch::new(Step::D, 5), Duration::Half)];
4596 score.parts[0].staves[0].measures[1].voices[1] = vec![
4597 Note::new(Pitch::new(Step::E, 4), Duration::Half),
4598 Note::new(Pitch::new(Step::F, 4), Duration::Half),
4599 ];
4600 let events = to_playback_events(&score, &opts(None));
4601 let at_start: Vec<_> = events
4602 .iter()
4603 .filter(|e| (e.time_beats - 4.0).abs() < 1e-9)
4604 .map(|e| e.velocity)
4605 .collect();
4606 let later: Vec<_> = events
4607 .iter()
4608 .filter(|e| (e.time_beats - 6.0).abs() < 1e-9)
4609 .map(|e| e.velocity)
4610 .collect();
4611 assert_eq!(at_start, vec![attack, attack]);
4612 assert_eq!(later, vec![sustain, sustain]);
4613 }
4614 }
4615
4616 #[test]
4617 fn dynamic_names_round_trip_and_compounds_continue_at_their_second_level() {
4618 use crate::model::notation::Dynamic;
4619 for dynamic in Dynamic::ALL {
4620 assert_eq!(
4621 Dynamic::from_musicxml_str(dynamic.to_musicxml_str()),
4622 Some(dynamic)
4623 );
4624 }
4625 assert_eq!(Dynamic::from_musicxml_str("fffff"), Some(Dynamic::Ffff));
4626 assert_eq!(Dynamic::Sfpp.sustained_level(), Some(Dynamic::Pp));
4627 assert_eq!(Dynamic::Sfz.sustained_level(), None);
4628 assert_eq!(Dynamic::from_musicxml_str("other"), None);
4629 }
4630
4631 #[test]
4632 fn accent_boosts_velocity_clamped() {
4633 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4634 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4635 note.articulations
4636 .push(crate::model::notation::Articulation::Accent);
4637 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4638 let events = to_playback_events(&score, &opts(None));
4639 assert_eq!(events[0].velocity, 84);
4640 }
4641
4642 #[test]
4643 fn accent_clamped_at_127() {
4644 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4645 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4646 note.dynamic = Some(crate::model::notation::Dynamic::Ffff);
4647 note.articulations
4648 .push(crate::model::notation::Articulation::Accent);
4649 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4650 let events = to_playback_events(&score, &opts(None));
4651 assert_eq!(events[0].velocity, 127);
4652 }
4653
4654 #[test]
4655 fn tenuto_keeps_full_duration() {
4656 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4657 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4658 note.articulations
4659 .push(crate::model::notation::Articulation::Tenuto);
4660 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4661 let events = to_playback_events(&score, &opts(None));
4662 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
4663 }
4664
4665 #[test]
4666 fn pedal_start_sets_pedal_field() {
4667 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4668 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4669 note.pedal_start = true;
4670 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4671 let events = to_playback_events(&score, &opts(None));
4672 assert!(events[0].pedal);
4673 }
4674
4675 #[test]
4676 fn no_pedal_start_pedal_is_false() {
4677 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4678 score.parts[0].staves[0].measures[0].voices[0] =
4679 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4680 let events = to_playback_events(&score, &opts(None));
4681 assert!(!events[0].pedal);
4682 }
4683
4684 #[test]
4685 fn set_tempo_at_measure_changes_time_secs() {
4686 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4687 score.parts[0].staves[0].measures[0].voices[0] =
4688 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4689 score.parts[0].staves[0].measures[1].tempo = Some(60);
4690 score.parts[0].staves[0].measures[1].voices[0] =
4691 vec![Note::new(Pitch::new(Step::D, 4), Duration::Whole)];
4692 let events = to_playback_events(&score, &opts(None));
4693 assert_eq!(events.len(), 2);
4694 assert!((events[0].time_secs).abs() < 1e-9);
4695 assert!((events[0].duration_secs - 2.0).abs() < 1e-9);
4696 assert!((events[1].time_secs - 2.0).abs() < 1e-9);
4697 assert!((events[1].duration_secs - 4.0).abs() < 1e-9);
4698 }
4699
4700 #[test]
4701 fn muted_part_produces_no_events() {
4702 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4703 score.parts[0].staves[0].measures[0].voices[0] =
4704 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4705 let options = PlaybackOptions {
4706 muted_parts: vec![0],
4707 ..Default::default()
4708 };
4709 assert!(to_playback_events(&score, &options).is_empty());
4710 }
4711
4712 #[test]
4713 fn part_index_field_set_correctly() {
4714 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4715 score.parts[0].staves[0].measures[0].voices[0] =
4716 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4717 let events = to_playback_events(&score, &opts(None));
4718 assert_eq!(events[0].part_index, 0);
4719 }
4720
4721 #[test]
4724 fn loop_region_filters_measures() {
4725 let mut score = Score::new("T", 120, 4, 4, 0, 3);
4727 for mi in 0..3 {
4728 score.parts[0].staves[0].measures[mi].voices[0] =
4729 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4730 }
4731 let options = PlaybackOptions {
4732 loop_region: Some((1, 2)),
4733 ..Default::default()
4734 };
4735 let events = to_playback_events(&score, &options);
4736 assert_eq!(events.len(), 2);
4737 assert!((events[0].time_beats).abs() < 1e-9);
4739 }
4740
4741 #[test]
4742 fn loop_region_none_plays_all_measures() {
4743 let mut score = Score::new("T", 120, 4, 4, 0, 3);
4744 for mi in 0..3 {
4745 score.parts[0].staves[0].measures[mi].voices[0] =
4746 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4747 }
4748 let events = to_playback_events(&score, &opts(None));
4749 assert_eq!(events.len(), 3);
4750 }
4751
4752 #[test]
4755 fn fermata_holds_a_shared_clock_across_voices_and_repeat_traversal() {
4756 use crate::model::notation::Barline;
4757 let mut score = Score::new("Shared fermata", 120, 3, 4, 0, 1);
4758 let measure = &mut score.parts[0].staves[0].measures[0];
4759 measure.barline_left = Barline::RepeatStart;
4760 measure.barline_right = Barline::RepeatEnd;
4761 let mut held = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4762 held.articulations.push(Articulation::Fermata);
4763 measure.voices[0] = vec![
4764 held,
4765 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
4766 Note::new(Pitch::new(Step::E, 4), Duration::Quarter),
4767 ];
4768 measure.voices[1] = vec![
4769 Note::new(Pitch::new(Step::C, 3), Duration::Half),
4770 Note::new(Pitch::new(Step::G, 3), Duration::Quarter),
4771 ];
4772 let events = to_playback_events(&score, &PlaybackOptions::default());
4773 let onsets = |pitch| {
4774 events
4775 .iter()
4776 .filter(|event| event.pitch_midi == pitch)
4777 .map(|event| event.time_beats)
4778 .collect::<Vec<_>>()
4779 };
4780 assert_eq!(onsets(60), vec![0.0, 3.5]);
4781 assert_eq!(onsets(62), vec![1.5, 5.0]);
4782 assert_eq!(onsets(64), vec![2.5, 6.0]);
4783 assert_eq!(onsets(55), vec![2.5, 6.0]);
4784 let bass = events.iter().find(|event| event.pitch_midi == 48).unwrap();
4785 assert_eq!(bass.duration_beats, 2.5);
4786 assert_eq!(bass.duration_secs, 1.25);
4787 assert!((super::super::score::score_duration_secs(&score) - 3.5).abs() < 1e-9);
4788 assert!(
4789 (super::super::score::score_duration_secs_region(&score, (0, 0)) - 3.5).abs() < 1e-9
4790 );
4791 }
4792
4793 #[test]
4794 fn simultaneous_fermatas_hold_once_and_delay_rest_pedal_and_position() {
4795 let mut score = Score::new("Two staves", 120, 3, 4, 0, 1);
4796 let mut held = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4797 held.articulations.push(Articulation::Fermata);
4798 score.parts[0].staves[0].measures[0].voices[0] = vec![
4799 held.clone(),
4800 Note::new(Pitch::new(Step::D, 4), Duration::Half),
4801 ];
4802 let mut lower = score.parts[0].staves[0].clone();
4803 held.pitches[0] = Pitch::new(Step::C, 3);
4804 let mut rest = Note::rest(Duration::Quarter);
4805 rest.pedal_start = true;
4806 lower.measures[0].voices[0] = vec![
4807 held,
4808 rest,
4809 Note::new(Pitch::new(Step::E, 3), Duration::Quarter),
4810 ];
4811 lower.measures[0].voices[1] = vec![
4812 Note::new(Pitch::new(Step::C, 2), Duration::Half),
4813 Note::rest(Duration::Quarter),
4814 ];
4815 score.parts[0].staves.push(lower);
4816 let opts = PlaybackOptions::default();
4817 let events = to_playback_events(&score, &opts);
4818 assert_eq!(
4819 events
4820 .iter()
4821 .find(|event| event.pitch_midi == 62)
4822 .unwrap()
4823 .time_beats,
4824 1.5
4825 );
4826 assert_eq!(
4827 events
4828 .iter()
4829 .find(|event| event.pitch_midi == 36)
4830 .unwrap()
4831 .duration_beats,
4832 2.5
4833 );
4834 let manifest = build_offline_render_manifest(
4835 &score,
4836 &opts,
4837 &OfflineRenderRequest {
4838 sample_rate_hz: 48_000,
4839 ..Default::default()
4840 },
4841 )
4842 .unwrap();
4843 assert!(manifest.semantic_events.iter().any(|event| matches!(
4844 event.kind,
4845 OfflineRenderSemanticEventKind::Pedal { down: true }
4846 ) && event.frame == 36_000));
4847 let position = compute_playback_position(&score, &opts, 0.625).unwrap();
4848 assert!((position.beat - 1.0).abs() < 1e-9);
4849 let position = compute_playback_position(&score, &opts, 1.0).unwrap();
4850 assert!((position.beat - 1.5).abs() < 1e-9);
4851 }
4852
4853 #[test]
4854 fn fermata_multiplier_extends_duration() {
4855 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4856 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4857 note.articulations
4858 .push(crate::model::notation::Articulation::Fermata);
4859 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4860 let options = PlaybackOptions {
4861 fermata_multiplier: 2.0,
4862 ..Default::default()
4863 };
4864 let events = to_playback_events(&score, &options);
4865 assert_eq!(events.len(), 1);
4866 assert!((events[0].duration_beats - 2.0).abs() < 1e-9);
4867 }
4868
4869 #[test]
4870 fn fermata_default_multiplier_is_1_5() {
4871 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4872 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4873 note.articulations
4874 .push(crate::model::notation::Articulation::Fermata);
4875 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4876 let events = to_playback_events(&score, &PlaybackOptions::default());
4877 assert_eq!(events.len(), 1);
4878 assert!((events[0].duration_beats - 1.5).abs() < 1e-9);
4879 }
4880
4881 #[test]
4882 fn fermata_hold_is_an_explicit_post_note_request_separate_from_extension() {
4883 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4884 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4885 note.articulations
4886 .push(crate::model::notation::Articulation::Fermata);
4887 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4888 let options = PlaybackOptions {
4889 fermata_multiplier: 2.0,
4890 fermata_hold_beats: 0.75,
4891 ..Default::default()
4892 };
4893 let events = to_playback_events(&score, &options);
4894 assert!((events[0].duration_beats - 2.0).abs() < 1e-9);
4895 assert!((events[0].post_note_pause_beats - 0.75).abs() < 1e-9);
4896 }
4897
4898 #[test]
4899 fn invalid_fermata_hold_is_safely_ignored() {
4900 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4901 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4902 note.articulations
4903 .push(crate::model::notation::Articulation::Fermata);
4904 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4905 let options = PlaybackOptions {
4906 fermata_hold_beats: f64::NAN,
4907 ..Default::default()
4908 };
4909 assert_eq!(
4910 to_playback_events(&score, &options)[0].post_note_pause_beats,
4911 0.0
4912 );
4913 }
4914
4915 #[test]
4916 fn non_fermata_note_unaffected_by_fermata_multiplier() {
4917 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4918 score.parts[0].staves[0].measures[0].voices[0] =
4919 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4920 let options = PlaybackOptions {
4921 fermata_multiplier: 3.0,
4922 ..Default::default()
4923 };
4924 let events = to_playback_events(&score, &options);
4925 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
4926 }
4927
4928 #[test]
4931 fn swing_triplet_first_eighth_is_long() {
4932 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4934 score.parts[0].staves[0].measures[0].voices[0] = vec![
4935 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
4936 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
4937 ];
4938 let options = PlaybackOptions {
4939 swing: Some(0.67),
4940 ..Default::default()
4941 };
4942 let events = to_playback_events(&score, &options);
4943 let e_c = events.iter().find(|e| e.pitch_midi == 60).unwrap();
4945 let e_d = events.iter().find(|e| e.pitch_midi == 62).unwrap();
4946 assert!(
4947 (e_c.duration_beats - 0.67).abs() < 1e-9,
4948 "first eighth should be 0.67"
4949 );
4950 assert!(
4951 (e_d.duration_beats - 0.33).abs() < 1e-9,
4952 "second eighth should be 0.33"
4953 );
4954 assert!(
4956 (e_d.time_beats - 0.67).abs() < 1e-9,
4957 "second note start should be at 0.67"
4958 );
4959 }
4960
4961 #[test]
4962 fn swing_does_not_stretch_an_unpaired_eighth() {
4963 let mut score = Score::new("unpaired", 120, 4, 4, 0, 1);
4964 score.parts[0].staves[0].measures[0].voices[0] = vec![
4965 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
4966 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
4967 ];
4968 let events = to_playback_events(
4969 &score,
4970 &PlaybackOptions {
4971 swing: Some(2.0 / 3.0),
4972 ..Default::default()
4973 },
4974 );
4975 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
4976 assert!((events[1].time_beats - 0.5).abs() < 1e-9);
4977 }
4978
4979 #[test]
4980 fn cue_note_does_not_sound_or_pair_with_a_written_eighth() {
4981 let mut score = Score::new("cue", 120, 4, 4, 0, 1);
4982 let mut cue = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
4983 cue.is_cue = true;
4984 score.parts[0].staves[0].measures[0].voices[0] = vec![
4985 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
4986 cue,
4987 Note::new(Pitch::new(Step::E, 4), Duration::Eighth),
4988 ];
4989 let events = to_playback_events(
4990 &score,
4991 &PlaybackOptions {
4992 swing: Some(2.0 / 3.0),
4993 ..Default::default()
4994 },
4995 );
4996 assert_eq!(events.len(), 2);
4997 assert_eq!(events[0].pitch_midi, 60);
4998 assert_eq!(events[0].duration_beats, 0.5);
4999 assert_eq!(events[1].pitch_midi, 64);
5000 assert_eq!(events[1].time_beats, 0.5);
5001 }
5002
5003 #[test]
5004 fn swing_requires_metric_alignment_and_a_matching_next_note() {
5005 let mut score = Score::new("metric boundary", 120, 4, 4, 0, 1);
5006 score.parts[0].staves[0].measures[0].voices[0] = vec![
5007 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
5008 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
5009 Note::new(Pitch::new(Step::E, 4), Duration::Eighth),
5010 Note::new(Pitch::new(Step::F, 4), Duration::Eighth),
5011 Note::new(Pitch::new(Step::G, 4), Duration::Eighth),
5012 Note::new(Pitch::new(Step::A, 4), Duration::Eighth),
5013 ];
5014 let events = to_playback_events(
5015 &score,
5016 &PlaybackOptions {
5017 swing: Some(2.0 / 3.0),
5018 ..Default::default()
5019 },
5020 );
5021 assert!((events[0].time_beats).abs() < 1e-9);
5022 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
5023 assert!((events[2].time_beats - 1.5).abs() < 1e-9);
5024 assert!((events[2].duration_beats - 0.5).abs() < 1e-9);
5025 assert!((events[3].time_beats - 2.0).abs() < 1e-9);
5026 assert!((events[3].duration_beats - 2.0 / 3.0).abs() < 1e-9);
5027 assert!((events[4].time_beats - 8.0 / 3.0).abs() < 1e-9);
5028 assert!((events[4].duration_beats - 1.0 / 3.0).abs() < 1e-9);
5029 assert!((events[5].time_beats - 3.0).abs() < 1e-9);
5030 assert!((events[5].duration_beats - 0.5).abs() < 1e-9);
5031 }
5032
5033 #[test]
5034 fn swing_keeps_the_offbeat_delay_after_an_eighth_rest() {
5035 let mut score = Score::new("rest swing", 120, 4, 4, 0, 1);
5036 score.parts[0].staves[0].measures[0].voices[0] = vec![
5037 Note::rest(Duration::Eighth),
5038 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
5039 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
5040 ];
5041 let events = to_playback_events(
5042 &score,
5043 &PlaybackOptions {
5044 swing: Some(2.0 / 3.0),
5045 ..Default::default()
5046 },
5047 );
5048 assert!((events[0].time_beats - 2.0 / 3.0).abs() < 1e-9);
5049 assert!((events[0].duration_beats - 1.0 / 3.0).abs() < 1e-9);
5050 assert!((events[1].time_beats - 1.0).abs() < 1e-9);
5051 }
5052
5053 #[test]
5054 fn invalid_swing_ratios_keep_straight_finite_timing() {
5055 let mut score = Score::new("invalid swing", 120, 4, 4, 0, 1);
5056 score.parts[0].staves[0].measures[0].voices[0] = vec![
5057 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
5058 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
5059 ];
5060 for ratio in [f64::NAN, f64::INFINITY, -1.0, 0.5, 1.0] {
5061 let events = to_playback_events(
5062 &score,
5063 &PlaybackOptions {
5064 swing: Some(ratio),
5065 ..Default::default()
5066 },
5067 );
5068 assert_eq!(events.len(), 2);
5069 assert_eq!(events[0].duration_beats, 0.5);
5070 assert_eq!(events[1].time_beats, 0.5);
5071 assert_eq!(events[1].duration_beats, 0.5);
5072 assert!(events.iter().all(|event| event.time_secs.is_finite()));
5073 }
5074 }
5075
5076 #[test]
5077 fn pickup_swing_uses_metric_position_in_the_full_bar() {
5078 let mut score = Score::new("pickup swing", 120, 4, 4, 0, 1);
5079 let measure = &mut score.parts[0].staves[0].measures[0];
5080 measure.actual_length = Some(crate::MeasureLength {
5081 numerator: 3,
5082 denominator: 8,
5083 });
5084 measure.voices[0] = vec![
5085 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
5086 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
5087 Note::new(Pitch::new(Step::E, 4), Duration::Eighth),
5088 ];
5089 let events = to_playback_events(
5090 &score,
5091 &PlaybackOptions {
5092 swing: Some(2.0 / 3.0),
5093 ..Default::default()
5094 },
5095 );
5096 assert_eq!(events[0].duration_beats, 0.5);
5097 assert!((events[1].time_beats - 0.5).abs() < 1e-9);
5098 assert!((events[1].duration_beats - 2.0 / 3.0).abs() < 1e-9);
5099 assert!((events[2].time_beats - 7.0 / 6.0).abs() < 1e-9);
5100 assert!((events[2].duration_beats - 1.0 / 3.0).abs() < 1e-9);
5101 }
5102
5103 #[test]
5104 fn swing_non_eighth_not_affected() {
5105 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5106 score.parts[0].staves[0].measures[0].voices[0] =
5107 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5108 let options = PlaybackOptions {
5109 swing: Some(0.67),
5110 ..Default::default()
5111 };
5112 let events = to_playback_events(&score, &options);
5113 assert!(
5114 (events[0].duration_beats - 1.0).abs() < 1e-9,
5115 "quarter note unaffected"
5116 );
5117 }
5118
5119 #[test]
5120 fn swing_none_is_straight() {
5121 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5122 score.parts[0].staves[0].measures[0].voices[0] =
5123 vec![Note::new(Pitch::new(Step::C, 4), Duration::Eighth)];
5124 let options = PlaybackOptions {
5125 swing: None,
5126 ..Default::default()
5127 };
5128 let events = to_playback_events(&score, &options);
5129 assert!(
5130 (events[0].duration_beats - 0.5).abs() < 1e-9,
5131 "no swing = straight eighth"
5132 );
5133 }
5134
5135 #[test]
5136 fn channel_matches_part_midi_channel() {
5137 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5138 score.parts[0].midi_channel = 3;
5139 score.parts[0].staves[0].measures[0].voices[0] =
5140 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5141 let events = to_playback_events(&score, &opts(None));
5142 assert_eq!(events[0].channel, 3);
5143 }
5144
5145 #[test]
5146 fn swing_unit_default_is_eighth() {
5147 assert_eq!(PlaybackOptions::default().swing_unit, Duration::Eighth);
5148 }
5149
5150 #[test]
5151 fn swing_unit_sixteenth() {
5152 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5153 score.parts[0].staves[0].measures[0].voices[0] = vec![
5154 Note::new(Pitch::new(Step::C, 4), Duration::Sixteenth),
5155 Note::new(Pitch::new(Step::D, 4), Duration::Sixteenth),
5156 ];
5157 let options = PlaybackOptions {
5158 swing: Some(0.67),
5159 swing_unit: Duration::Sixteenth,
5160 ..Default::default()
5161 };
5162 let events = to_playback_events(&score, &options);
5163 let e_c = events.iter().find(|e| e.pitch_midi == 60).unwrap();
5164 let e_d = events.iter().find(|e| e.pitch_midi == 62).unwrap();
5165 assert!(
5166 (e_c.duration_beats - 0.335).abs() < 1e-9,
5167 "first 16th should be 0.335"
5168 );
5169 assert!(
5170 (e_d.duration_beats - 0.165).abs() < 1e-9,
5171 "second 16th should be 0.165"
5172 );
5173 }
5174
5175 #[test]
5176 fn swing_resets_per_measure() {
5177 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5179 let pair = || {
5180 vec![
5181 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
5182 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
5183 ]
5184 };
5185 score.parts[0].staves[0].measures[0].voices[0] = pair();
5186 score.parts[0].staves[0].measures[1].voices[0] = pair();
5187 let options = PlaybackOptions {
5188 swing: Some(0.67),
5189 ..Default::default()
5190 };
5191 let events = to_playback_events(&score, &options);
5192 let durations: Vec<f64> = events.iter().map(|e| e.duration_beats).collect();
5194 assert!((durations[0] - 0.67).abs() < 1e-9, "m0 first note long");
5196 assert!((durations[1] - 0.33).abs() < 1e-9, "m0 second note short");
5198 assert!(
5200 (durations[2] - 0.67).abs() < 1e-9,
5201 "m1 first note long (reset)"
5202 );
5203 assert!((durations[3] - 0.33).abs() < 1e-9, "m1 second note short");
5205 }
5206
5207 #[test]
5208 fn multi_voice_events_preserve_source_voice_addresses() {
5209 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5210 score.parts[0].staves[0].measures[0].voices[0] =
5211 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5212 score.parts[0].staves[0].measures[0].voices[1] =
5213 vec![Note::new(Pitch::new(Step::E, 4), Duration::Quarter)];
5214
5215 let events = to_playback_events(&score, &PlaybackOptions::default());
5216 let addresses: Vec<&str> = events
5217 .iter()
5218 .filter_map(|event| event.address.as_deref())
5219 .collect();
5220 assert!(addresses.contains(&"0:0:0:0:0"));
5221 assert!(addresses.contains(&"0:0:0:1:0"));
5222 }
5223
5224 #[test]
5225 fn playback_exposes_original_musicxml_voice_number_alongside_slot_address() {
5226 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5227 let measure = &mut score.parts[0].staves[0].measures[0];
5228 measure.voices[1] = vec![Note::new(Pitch::new(Step::E, 4), Duration::Quarter)];
5229 measure.source_voice_numbers[1] = Some(5);
5230
5231 let events = to_playback_events(&score, &PlaybackOptions::default());
5232 let event = events
5233 .iter()
5234 .find(|event| event.address.as_deref() == Some("0:0:0:1:0"))
5235 .expect("event from slot 1");
5236 assert_eq!(event.source.as_ref().map(|source| source.voice), Some(1));
5237 assert_eq!(event.source_voice_number, Some(5));
5238 }
5239
5240 #[test]
5243 fn playback_position_at_zero_is_measure_0_beat_0() {
5244 let score = Score::new("T", 120, 4, 4, 0, 4);
5245 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 0.0).unwrap();
5246 assert_eq!(pos.measure_index, 0);
5247 assert!(pos.beat.abs() < 1e-9);
5248 }
5249
5250 #[test]
5251 fn playback_position_at_half_measure_is_beat_2() {
5252 let score = Score::new("T", 120, 4, 4, 0, 4);
5254 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 0.5).unwrap();
5255 assert_eq!(pos.measure_index, 0);
5256 assert!(
5257 (pos.beat - 1.0).abs() < 1e-9,
5258 "expected beat 1.0, got {}",
5259 pos.beat
5260 );
5261 }
5262
5263 #[test]
5264 fn playback_position_beyond_score_is_none() {
5265 let score = Score::new("T", 120, 4, 4, 0, 1);
5267 assert!(compute_playback_position(&score, &PlaybackOptions::default(), 10.0).is_none());
5268 }
5269
5270 #[test]
5271 fn playback_position_tempo_change_takes_effect() {
5272 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5275 score.parts[0].staves[0].measures[1].tempo = Some(60);
5276 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 2.5).unwrap();
5277 assert_eq!(pos.measure_index, 1);
5278 assert!(
5279 (pos.beat - 0.5).abs() < 1e-9,
5280 "expected beat 0.5, got {}",
5281 pos.beat
5282 );
5283 }
5284
5285 #[test]
5286 fn playback_position_loop_region_starts_at_zero() {
5287 let score = Score::new("T", 120, 4, 4, 0, 4);
5289 let options = PlaybackOptions {
5290 loop_region: Some((1, 2)),
5291 ..Default::default()
5292 };
5293 let pos = compute_playback_position(&score, &options, 0.0).unwrap();
5294 assert_eq!(pos.measure_index, 1);
5295 assert!(pos.beat.abs() < 1e-9);
5296 }
5297
5298 #[test]
5301 fn metronome_injects_beat_events() {
5302 let score = Score::new("T", 120, 4, 4, 0, 1);
5304 let options = PlaybackOptions {
5305 metronome: Some(MetronomeConfig::default()),
5306 ..Default::default()
5307 };
5308 let events = to_playback_events(&score, &options);
5309 let metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
5310 assert_eq!(metro_events.len(), 4, "expected 4 metronome clicks in 4/4");
5311 }
5312
5313 #[test]
5314 fn metronome_accent_is_first_beat() {
5315 let score = Score::new("T", 120, 4, 4, 0, 1);
5316 let metro = MetronomeConfig::default();
5317 let options = PlaybackOptions {
5318 metronome: Some(metro.clone()),
5319 ..Default::default()
5320 };
5321 let events = to_playback_events(&score, &options);
5322 let mut metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
5323 metro_events.sort_by(|a, b| a.time_beats.partial_cmp(&b.time_beats).unwrap());
5324 assert_eq!(metro_events[0].pitch_midi, metro.accent_pitch);
5325 assert_eq!(metro_events[0].velocity, metro.accent_velocity);
5326 }
5327
5328 #[test]
5329 fn metronome_regular_beat_pitch() {
5330 let score = Score::new("T", 120, 4, 4, 0, 1);
5331 let metro = MetronomeConfig::default();
5332 let options = PlaybackOptions {
5333 metronome: Some(metro.clone()),
5334 ..Default::default()
5335 };
5336 let events = to_playback_events(&score, &options);
5337 let mut metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
5338 metro_events.sort_by(|a, b| a.time_beats.partial_cmp(&b.time_beats).unwrap());
5339 for ev in &metro_events[1..] {
5340 assert_eq!(ev.pitch_midi, metro.beat_pitch);
5341 assert_eq!(ev.velocity, metro.beat_velocity);
5342 }
5343 }
5344
5345 #[test]
5346 fn metronome_integrates_measure_tempo_ramp_and_carries_ending_tempo() {
5347 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5348 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
5349 let options = PlaybackOptions {
5350 metronome: Some(MetronomeConfig::default()),
5351 ..Default::default()
5352 };
5353
5354 let mut clicks: Vec<_> = to_playback_events(&score, &options)
5355 .into_iter()
5356 .filter(|event| event.is_metronome)
5357 .collect();
5358 clicks.sort_by(|left, right| left.time_beats.total_cmp(&right.time_beats));
5359
5360 let expected_second_beat = tempo_ramp_seconds(120.0, Some(60.0), 4.0, 1.0);
5361 let expected_second_measure = tempo_ramp_seconds(120.0, Some(60.0), 4.0, 4.0);
5362 assert!((clicks[1].time_secs - expected_second_beat).abs() < 1e-9);
5363 assert!((clicks[4].time_secs - expected_second_measure).abs() < 1e-9);
5364 assert!((clicks[5].time_secs - (expected_second_measure + 1.0)).abs() < 1e-9);
5365 }
5366
5367 #[test]
5368 fn metronome_events_are_marked() {
5369 let score = Score::new("T", 120, 4, 4, 0, 1);
5370 let options = PlaybackOptions {
5371 metronome: Some(MetronomeConfig::default()),
5372 ..Default::default()
5373 };
5374 let events = to_playback_events(&score, &options);
5375 assert!(events.iter().any(|e| e.is_metronome));
5376 }
5377
5378 #[test]
5379 fn metronome_none_produces_no_extra_events() {
5380 let score = Score::new("T", 120, 4, 4, 0, 1);
5381 let events = to_playback_events(&score, &PlaybackOptions::default());
5382 assert!(events.iter().all(|e| !e.is_metronome));
5383 }
5384
5385 #[test]
5386 fn offline_render_manifest_uses_event_end_as_exact_frame_length() {
5387 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5388 score.parts[0].staves[0].measures[0].voices[0][0] =
5389 Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5390 let request = OfflineRenderRequest {
5391 format: OfflineRenderFormat::Flac,
5392 sample_rate_hz: 48_000,
5393 channels: 2,
5394 provider_identity: Some("test-provider@1".into()),
5395 ..Default::default()
5396 };
5397
5398 let manifest = build_offline_render_manifest(&score, &PlaybackOptions::default(), &request)
5399 .expect("valid render manifest");
5400 assert_eq!(manifest.contract_version, OFFLINE_RENDER_CONTRACT_VERSION);
5401 assert!(manifest.validate().is_ok());
5402 assert_eq!(manifest.format, OfflineRenderFormat::Flac);
5403 assert_eq!(manifest.duration_frames, 24_000);
5404 assert_eq!(manifest.events.len(), 1);
5405 assert_eq!(manifest.frame_events.len(), 1);
5406 assert_eq!(manifest.frame_events[0].start_frame, 0);
5407 assert_eq!(manifest.frame_events[0].duration_frames, 24_000);
5408 assert_eq!(manifest.frame_events[0].end_frame, 24_000);
5409 assert_eq!(manifest.playback_options, PlaybackOptions::default());
5410 assert_eq!(
5411 manifest.provider_identity.as_deref(),
5412 Some("test-provider@1")
5413 );
5414 }
5415
5416 #[test]
5417 fn offline_render_manifest_rejects_out_of_range_audio_shape() {
5418 let score = Score::new("T", 120, 4, 4, 0, 1);
5419 let request = OfflineRenderRequest {
5420 sample_rate_hz: 0,
5421 ..Default::default()
5422 };
5423 assert!(matches!(
5424 build_offline_render_manifest(&score, &PlaybackOptions::default(), &request),
5425 Err(crate::Error::InvalidOfflineRenderRequest)
5426 ));
5427 }
5428
5429 #[test]
5430 fn offline_render_manifest_selection_keeps_source_address_and_release_tail() {
5431 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5432 score.parts[0].staves[0].measures[0].voices[0] = vec![
5433 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
5434 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
5435 ];
5436 let events = to_playback_events(&score, &PlaybackOptions::default());
5437 score.parts[0].midi_control_changes.push(MidiControlChange {
5438 tick: 480,
5439 channel: 0,
5440 controller: 7,
5441 value: 100,
5442 });
5443 let request = OfflineRenderRequest {
5444 sample_rate_hz: 8_000,
5445 release_tail_millis: 125,
5446 scope: OfflineRenderScope::Selection {
5447 addresses: vec![events[1].source.clone().expect("source address")],
5448 },
5449 ..Default::default()
5450 };
5451 let manifest = build_offline_render_manifest(&score, &PlaybackOptions::default(), &request)
5452 .expect("selection manifest");
5453 assert_eq!(manifest.events.len(), 1);
5454 assert_eq!(manifest.events[0].source, events[1].source);
5455 assert_eq!(manifest.frame_events[0].start_frame, 0);
5456 assert!(manifest.semantic_events.iter().any(|event| matches!(
5457 event.kind,
5458 OfflineRenderSemanticEventKind::Controller {
5459 channel: 0,
5460 controller: 7,
5461 value: 100
5462 }
5463 ) && event.frame == 0));
5464 assert_eq!(manifest.release_tail_frames, 1_000);
5465 assert_eq!(manifest.duration_frames, 5_000);
5466 }
5467
5468 #[test]
5469 fn offline_render_request_owned_options_override_legacy_argument() {
5470 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5471 score.parts[0].staves[0].measures[0].voices[0][0] =
5472 Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5473 let request_options = PlaybackOptions {
5474 bpm_override: Some(60),
5475 ..Default::default()
5476 };
5477 let manifest = build_offline_render_manifest(
5478 &score,
5479 &PlaybackOptions::default(),
5480 &OfflineRenderRequest {
5481 sample_rate_hz: 48_000,
5482 playback_options: Some(request_options.clone()),
5483 ..Default::default()
5484 },
5485 )
5486 .expect("request-owned playback options");
5487 assert_eq!(manifest.playback_options, request_options);
5488 assert_eq!(manifest.frame_events[0].duration_frames, 48_000);
5489 }
5490
5491 #[test]
5492 fn offline_manifest_projects_typed_midi_events_to_exact_frames() {
5493 let mut score = Score::new("MIDI semantics", 120, 4, 4, 0, 1);
5494 score.parts[0].midi_pitch_bends.push(MidiPitchBend {
5495 tick: 480,
5496 channel: 2,
5497 value: -512,
5498 });
5499 score.parts[0].midi_program_changes.push(MidiProgramChange {
5500 tick: 480,
5501 channel: 2,
5502 program: 41,
5503 });
5504 score.parts[0].midi_aftertouch.push(MidiAftertouch {
5505 tick: 480,
5506 channel: 2,
5507 key: Some(64),
5508 value: 88,
5509 });
5510 let manifest = build_offline_render_manifest(
5511 &score,
5512 &PlaybackOptions::default(),
5513 &OfflineRenderRequest {
5514 sample_rate_hz: 48_000,
5515 ..Default::default()
5516 },
5517 )
5518 .expect("manifest builds");
5519 for kind in [
5520 OfflineRenderSemanticEventKind::PitchBend {
5521 channel: 2,
5522 value: -512,
5523 },
5524 OfflineRenderSemanticEventKind::ProgramChange {
5525 channel: 2,
5526 program: 41,
5527 },
5528 OfflineRenderSemanticEventKind::Aftertouch {
5529 channel: 2,
5530 key: Some(64),
5531 value: 88,
5532 },
5533 ] {
5534 assert!(manifest.semantic_events.iter().any(|event| {
5535 event.kind == kind
5536 && event.frame == 24_000
5537 && event
5538 .measure
5539 .as_ref()
5540 .is_some_and(|address| address.part == 0 && address.measure == 0)
5541 }));
5542 }
5543 }
5544
5545 #[test]
5546 fn offline_manifest_frames_cover_repeat_ramp_fermata_pedal_bend_and_instrument_change() {
5547 let mut score = Score::new("fixture", 120, 4, 4, 0, 2);
5548 let mut first = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5549 first.pedal_start = true;
5550 first.articulations = vec![Articulation::Fermata, Articulation::BreathMark];
5551 first.guitar_bend_curve = vec![GuitarBendPoint {
5552 position_per_mille: 500,
5553 alter_cents: 100,
5554 }];
5555 score.parts[0].staves[0].measures[0].voices[0] = vec![first];
5556 score.parts[0].staves[0].measures[0].barline_left =
5557 super::super::notation::Barline::RepeatStart;
5558 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
5559 score.parts[0].staves[0].measures[0].navigation = Some("Segno".to_string());
5560 score.parts[0].midi_control_changes.push(MidiControlChange {
5561 tick: 480,
5562 channel: 0,
5563 controller: 11,
5564 value: 96,
5565 });
5566
5567 let mut second = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
5568 second.pedal_end = true;
5569 score.parts[0].staves[0].measures[1].voices[0] = vec![second];
5570 score.parts[0].staves[0].measures[1].barline_right =
5571 super::super::notation::Barline::RepeatEnd;
5572 let mut flute = crate::InstrumentDefinition::new("flute", "Flute");
5573 flute.midi_channel = 2;
5574 flute.midi_program = 73;
5575 score.parts[0].staves[0].measures[1].instrument_change = Some(flute);
5576
5577 let request = OfflineRenderRequest {
5578 sample_rate_hz: 48_000,
5579 playback_options: Some(PlaybackOptions {
5580 fermata_multiplier: 1.5,
5581 fermata_hold_beats: 0.25,
5582 ..Default::default()
5583 }),
5584 ..Default::default()
5585 };
5586 let manifest = build_offline_render_manifest(&score, &PlaybackOptions::default(), &request)
5587 .expect("fixture manifest");
5588 assert_eq!(manifest.frame_events.len(), 4);
5589 let measures: Vec<usize> = manifest
5590 .frame_events
5591 .iter()
5592 .map(|event| event.event.source.as_ref().unwrap().measure)
5593 .collect();
5594 assert_eq!(measures, vec![0, 1, 0, 1]);
5595 assert!(manifest.frame_events[0].event.pedal);
5596 assert_eq!(manifest.frame_events[0].event.post_note_pause_beats, 0.25);
5597 assert_eq!(manifest.frame_events[0].event.pitch_bend_curve.len(), 1);
5598 assert_eq!(
5599 manifest.frame_events[1].event.instrument_id.as_deref(),
5600 Some("flute")
5601 );
5602 assert_eq!(manifest.frame_events[3].event.program, 73);
5603 assert!(manifest.semantic_events.iter().any(|event| {
5604 matches!(
5605 &event.kind,
5606 OfflineRenderSemanticEventKind::InstrumentChange {
5607 instrument_id,
5608 channel: 2,
5609 program: 73,
5610 transpose_semitones: 0,
5611 } if instrument_id == "flute"
5612 ) && event
5613 .measure
5614 .as_ref()
5615 .is_some_and(|address| address.measure == 1)
5616 && event.frame == manifest.frame_events[1].start_frame
5617 }));
5618 assert!(manifest.semantic_events.iter().any(|event| matches!(
5619 event.kind,
5620 OfflineRenderSemanticEventKind::Pedal { down: true }
5621 ) && event.frame
5622 == manifest.frame_events[0].start_frame));
5623 assert!(manifest.semantic_events.iter().any(|event| matches!(
5624 event.kind,
5625 OfflineRenderSemanticEventKind::Pedal { down: false }
5626 ) && event.frame
5627 == manifest.frame_events[1].start_frame));
5628 assert!(manifest.semantic_events.iter().any(|event| matches!(
5629 event.kind,
5630 OfflineRenderSemanticEventKind::Articulation {
5631 articulation: Articulation::Fermata
5632 }
5633 )));
5634 assert!(manifest.semantic_events.iter().any(|event| matches!(
5635 event.kind,
5636 OfflineRenderSemanticEventKind::GuitarBend { .. }
5637 )));
5638 let tempo_events = manifest
5639 .semantic_events
5640 .iter()
5641 .filter(|event| matches!(event.kind, OfflineRenderSemanticEventKind::Tempo { .. }))
5642 .count();
5643 let navigation_events = manifest
5644 .semantic_events
5645 .iter()
5646 .filter(|event| {
5647 matches!(
5648 event.kind,
5649 OfflineRenderSemanticEventKind::Navigation { .. }
5650 )
5651 })
5652 .count();
5653 let control_frames: Vec<u64> = manifest
5654 .semantic_events
5655 .iter()
5656 .filter_map(|event| {
5657 matches!(
5658 event.kind,
5659 OfflineRenderSemanticEventKind::Controller {
5660 channel: 0,
5661 controller: 11,
5662 value: 96
5663 }
5664 )
5665 .then_some(event.frame)
5666 })
5667 .collect();
5668 assert_eq!(tempo_events, 2);
5669 assert_eq!(navigation_events, 2);
5670 assert_eq!(control_frames.len(), 2);
5671 let ramp_duration = tempo_ramp_seconds(120.0, Some(60.0), 4.0, 4.0);
5672 let first_hold = 0.5 * 60.0 / 105.0;
5675 let second_hold = 0.5;
5676 assert_eq!(
5677 control_frames[0],
5678 ((tempo_ramp_seconds(120.0, Some(60.0), 4.0, 1.0) + first_hold) * 48_000.0).round()
5679 as u64
5680 );
5681 assert_eq!(
5682 control_frames[1],
5683 ((ramp_duration + 5.0 + first_hold + second_hold) * 48_000.0).round() as u64
5684 );
5685
5686 let expected_starts = [
5687 0.0,
5688 ramp_duration + first_hold,
5689 ramp_duration + first_hold + 4.0,
5690 ramp_duration + first_hold + 8.0 + second_hold,
5691 ];
5692 for (frame_event, expected_secs) in manifest.frame_events.iter().zip(expected_starts) {
5693 assert_eq!(
5694 frame_event.start_frame,
5695 (expected_secs * 48_000.0).round() as u64
5696 );
5697 }
5698 }
5699
5700 #[test]
5701 fn offline_render_manifest_reports_unresolved_selection_address() {
5702 let score = Score::new("T", 120, 4, 4, 0, 1);
5703 let request = OfflineRenderRequest {
5704 scope: OfflineRenderScope::Selection {
5705 addresses: vec![NoteAddr {
5706 part: 0,
5707 staff: 0,
5708 measure: 0,
5709 voice: 0,
5710 note: 99,
5711 }],
5712 },
5713 ..Default::default()
5714 };
5715 let manifest = build_offline_render_manifest(&score, &PlaybackOptions::default(), &request)
5716 .expect("empty selection is representable");
5717 assert_eq!(manifest.events.len(), 0);
5718 assert_eq!(
5719 manifest.diagnostics,
5720 vec![
5721 OfflineRenderDiagnostic {
5722 kind: OfflineRenderDiagnosticKind::UnresolvedSelectionAddress,
5723 source: Some(NoteAddr {
5724 part: 0,
5725 staff: 0,
5726 measure: 0,
5727 voice: 0,
5728 note: 99,
5729 }),
5730 },
5731 OfflineRenderDiagnostic {
5732 kind: OfflineRenderDiagnosticKind::EmptySelection,
5733 source: None,
5734 },
5735 ]
5736 );
5737 }
5738
5739 #[test]
5740 fn offline_render_manifest_v1_json_migrates_with_v2_defaults() {
5741 let legacy = serde_json::json!({
5742 "contract_version": 1,
5743 "playback_contract_version": 2,
5744 "format": "Wav",
5745 "sample_rate_hz": 48_000,
5746 "channels": 2,
5747 "duration_frames": 0,
5748 "events": []
5749 });
5750 let manifest: OfflineRenderManifest =
5751 serde_json::from_value(legacy).expect("v1 manifest remains readable");
5752 assert_eq!(manifest.contract_version, 1);
5753 assert_eq!(manifest.scope, OfflineRenderScope::FullScore);
5754 assert_eq!(manifest.release_tail_frames, 0);
5755 assert_eq!(manifest.playback_options, PlaybackOptions::default());
5756 assert!(manifest.frame_events.is_empty());
5757 assert!(manifest.diagnostics.is_empty());
5758 assert!(manifest.validate().is_ok());
5759 }
5760
5761 #[test]
5762 fn offline_render_manifest_validation_rejects_tampered_frame_and_tail_boundaries() {
5763 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5764 score.parts[0].staves[0].measures[0].voices[0] =
5765 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5766 let manifest = build_offline_render_manifest(
5767 &score,
5768 &PlaybackOptions::default(),
5769 &OfflineRenderRequest {
5770 sample_rate_hz: 48_000,
5771 release_tail_millis: 100,
5772 ..Default::default()
5773 },
5774 )
5775 .expect("manifest builds");
5776 assert!(manifest.validate().is_ok());
5777
5778 let mut invalid_tail = manifest.clone();
5779 invalid_tail.duration_frames = invalid_tail.duration_frames.saturating_sub(1);
5780 assert!(matches!(
5781 invalid_tail.validate(),
5782 Err(crate::Error::InvalidOfflineRenderRequest)
5783 ));
5784
5785 let mut invalid_frame = manifest;
5786 invalid_frame.frame_events[0].end_frame =
5787 invalid_frame.frame_events[0].end_frame.saturating_add(1);
5788 assert!(matches!(
5789 invalid_frame.validate(),
5790 Err(crate::Error::InvalidOfflineRenderRequest)
5791 ));
5792 }
5793
5794 #[test]
5795 fn routing_manifest_prefers_instrument_bus_and_isolates_metronome() {
5796 let mut score = Score::new("routing", 120, 4, 4, 0, 1);
5797 let mut instrument = crate::InstrumentDefinition::new("flute", "Flute");
5798 instrument.midi_channel = 2;
5799 instrument.midi_program = 73;
5800 score.parts[0].instrument = Some(instrument);
5801 score.parts[0].staves[0].measures[0].voices[0] =
5802 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5803 let options = PlaybackOptions {
5804 metronome: Some(MetronomeConfig::default()),
5805 ..Default::default()
5806 };
5807 let mut config = PlaybackRoutingConfig {
5808 provider_identity: Some("sf2-host@1".into()),
5809 ..Default::default()
5810 };
5811 config.part_buses.insert(0, "parts".into());
5812 config
5813 .instrument_buses
5814 .insert("flute".into(), "winds".into());
5815 config.aux_sends.push(PlaybackAuxSend {
5816 source_bus: "winds".into(),
5817 destination_bus: "reverb".into(),
5818 gain_db: -12.0,
5819 });
5820 config.effect_routes.push(PlaybackEffectRoute {
5821 bus_id: "reverb".into(),
5822 effect_id: "convolution".into(),
5823 enabled: true,
5824 });
5825
5826 let manifest = build_playback_routing_manifest(&score, &options, &config)
5827 .expect("valid routing manifest");
5828 assert_eq!(manifest.contract_version, PLAYBACK_ROUTING_CONTRACT_VERSION);
5829 assert_eq!(manifest.provider_identity.as_deref(), Some("sf2-host@1"));
5830 assert!(manifest.routes.iter().any(|route| {
5831 route.instrument_id.as_deref() == Some("flute") && route.bus_id == "winds"
5832 }));
5833 assert!(
5834 manifest
5835 .routes
5836 .iter()
5837 .any(|route| route.is_metronome && route.bus_id == "metronome")
5838 );
5839 assert_eq!(manifest.aux_sends.len(), 1);
5840 assert_eq!(manifest.effect_routes.len(), 1);
5841 }
5842
5843 #[test]
5844 fn routing_manifest_rejects_unsafe_bus_and_non_finite_send_gain() {
5845 let score = Score::new("routing", 120, 4, 4, 0, 1);
5846 let config = PlaybackRoutingConfig {
5847 master_bus: "not a bus".into(),
5848 ..Default::default()
5849 };
5850 assert!(matches!(
5851 build_playback_routing_manifest(&score, &PlaybackOptions::default(), &config),
5852 Err(crate::Error::InvalidPlaybackRouting)
5853 ));
5854
5855 let mut config = PlaybackRoutingConfig::default();
5856 config.aux_sends.push(PlaybackAuxSend {
5857 source_bus: "part".into(),
5858 destination_bus: "master".into(),
5859 gain_db: f64::NAN,
5860 });
5861 assert!(matches!(
5862 build_playback_routing_manifest(&score, &PlaybackOptions::default(), &config),
5863 Err(crate::Error::InvalidPlaybackRouting)
5864 ));
5865 }
5866
5867 #[test]
5868 fn timing_corpus_reports_score_schedule_results_without_audio_claims() {
5869 let mut score = Score::new("corpus", 120, 4, 4, 0, 1);
5870 score.parts[0].staves[0].measures[0].voices[0] =
5871 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5872 let expected_events = to_playback_events(&score, &PlaybackOptions::default());
5873 let report = evaluate_playback_timing_corpus(&[PlaybackTimingCase {
5874 id: "quarter-note".into(),
5875 score,
5876 options: PlaybackOptions::default(),
5877 expected_events,
5878 tolerance: PlaybackTimingTolerance::default(),
5879 }])
5880 .expect("valid timing corpus");
5881 assert_eq!(
5882 report.contract_version,
5883 PLAYBACK_TIMING_CORPUS_CONTRACT_VERSION
5884 );
5885 assert_eq!(report.total_cases, 1);
5886 assert_eq!(report.passed_cases, 1);
5887 assert!(report.cases[0].report.within_tolerance);
5888 }
5889
5890 #[test]
5891 fn timing_corpus_rejects_empty_or_duplicate_case_ids() {
5892 assert!(matches!(
5893 evaluate_playback_timing_corpus(&[]),
5894 Err(crate::Error::InvalidPlaybackTimingCorpus)
5895 ));
5896 let score = Score::new("corpus", 120, 4, 4, 0, 1);
5897 let case = PlaybackTimingCase {
5898 id: "duplicate".into(),
5899 expected_events: to_playback_events(&score, &PlaybackOptions::default()),
5900 score,
5901 options: PlaybackOptions::default(),
5902 tolerance: PlaybackTimingTolerance::default(),
5903 };
5904 assert!(matches!(
5905 evaluate_playback_timing_corpus(&[case.clone(), case]),
5906 Err(crate::Error::InvalidPlaybackTimingCorpus)
5907 ));
5908 }
5909
5910 fn comparison_event(time_secs: f64, duration_secs: f64) -> PlaybackEvent {
5911 PlaybackEvent {
5912 address: Some("0:0:0:0:0".into()),
5913 source: Some(NoteAddr {
5914 part: 0,
5915 staff: 0,
5916 measure: 0,
5917 voice: 0,
5918 note: 0,
5919 }),
5920 source_voice_number: None,
5921 time_beats: 0.0,
5922 time_secs,
5923 pitch_midi: 60,
5924 pitch_midi_cents: 6000,
5925 pitch_bend_curve: Vec::new(),
5926 post_note_pause_beats: 0.0,
5927 articulations: Vec::new(),
5928 chord_symbol: None,
5929 guitar_technique: None,
5930 velocity: 64,
5931 duration_beats: 1.0,
5932 duration_secs,
5933 pedal: false,
5934 part_index: 0,
5935 channel: 0,
5936 program: 0,
5937 instrument_id: None,
5938 is_metronome: false,
5939 }
5940 }
5941
5942 #[test]
5943 fn legacy_playback_event_json_defaults_bend_curve() {
5944 let event: PlaybackEvent = serde_json::from_str(
5945 r#"{"address":null,"source":null,"source_voice_number":null,"time_beats":0.0,"time_secs":0.0,"pitch_midi":60,"pitch_midi_cents":6000,"velocity":64,"duration_beats":1.0,"duration_secs":0.5,"pedal":false,"part_index":0,"channel":0,"is_metronome":false}"#,
5946 )
5947 .expect("legacy playback event remains readable");
5948 assert!(event.pitch_bend_curve.is_empty());
5949 assert_eq!(event.guitar_technique, None);
5950 }
5951
5952 #[test]
5953 fn tempo_ramp_uses_integrated_event_timing_and_carries_target_forward() {
5954 let mut score = Score::new("ramp", 120, 4, 4, 0, 2);
5955 for measure in &mut score.parts[0].staves[0].measures {
5956 measure.voices[0] = vec![
5957 crate::Note::new(crate::Pitch::new(crate::Step::C, 4), crate::Duration::Half),
5958 crate::Note::new(crate::Pitch::new(crate::Step::D, 4), crate::Duration::Half),
5959 ];
5960 }
5961 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
5962 let events = to_playback_events(&score, &PlaybackOptions::default());
5963 assert_eq!(events.len(), 4);
5964 let expected_measure_secs = 4.0 * 60.0 / (60.0 - 120.0) * (60.0f64 / 120.0).ln();
5965 assert!((events[2].time_secs - expected_measure_secs).abs() < 1e-9);
5966 assert!((events[0].duration_secs - 1.1507282898).abs() < 1e-6);
5967 assert!((events[2].duration_secs - 2.0).abs() < 1e-9);
5968 }
5969
5970 #[test]
5971 fn breath_and_caesura_expose_deterministic_post_note_pause() {
5972 let mut score = Score::new("pause", 120, 4, 4, 0, 1);
5973 let mut breath = crate::Note::new(
5974 crate::Pitch::new(crate::Step::C, 4),
5975 crate::Duration::Quarter,
5976 );
5977 breath.articulations.push(Articulation::BreathMark);
5978 let mut caesura = crate::Note::new(
5979 crate::Pitch::new(crate::Step::D, 4),
5980 crate::Duration::Quarter,
5981 );
5982 caesura
5983 .articulations
5984 .extend([Articulation::BreathMark, Articulation::Caesura]);
5985 score.parts[0].staves[0].measures[0].voices[0] = vec![breath, caesura];
5986
5987 let events = to_playback_events(&score, &PlaybackOptions::default());
5988 assert_eq!(events.len(), 2);
5989 assert_eq!(events[0].post_note_pause_beats, 0.25);
5990 assert_eq!(events[1].post_note_pause_beats, 0.5);
5991 assert_eq!(
5992 events[1].articulations,
5993 vec![Articulation::BreathMark, Articulation::Caesura]
5994 );
5995 }
5996
5997 #[test]
5998 fn measure_instrument_change_resolves_on_playback_events() {
5999 let mut score = Score::new("instrument", 120, 4, 4, 0, 2);
6000 let mut base = crate::InstrumentDefinition::new("piano", "Piano");
6001 base.midi_channel = 1;
6002 base.midi_program = 0;
6003 score.parts[0].instrument = Some(base);
6004 let mut change = crate::InstrumentDefinition::new("flute", "Flute");
6005 change.midi_channel = 2;
6006 change.midi_program = 73;
6007 change.transpose_semitones = -2;
6008 score.parts[0].staves[0].measures[1].instrument_change = Some(change);
6009 for measure in &mut score.parts[0].staves[0].measures {
6010 measure.voices[0] = vec![crate::Note::new(
6011 crate::Pitch::new(crate::Step::C, 4),
6012 crate::Duration::Whole,
6013 )];
6014 }
6015
6016 let events = to_playback_events(&score, &PlaybackOptions::default());
6017 assert_eq!(events.len(), 2);
6018 assert_eq!(
6019 (
6020 events[0].channel,
6021 events[0].program,
6022 events[0].instrument_id.as_deref()
6023 ),
6024 (1, 0, Some("piano"))
6025 );
6026 assert_eq!(
6027 (
6028 events[1].channel,
6029 events[1].program,
6030 events[1].instrument_id.as_deref()
6031 ),
6032 (2, 73, Some("flute"))
6033 );
6034 assert_eq!(events[0].pitch_midi, 60);
6035 assert_eq!(events[1].pitch_midi, 58);
6036 }
6037
6038 #[test]
6039 fn authored_midi_program_change_selects_the_next_note_at_its_source_tick() {
6040 let mut score = Score::new("program", 120, 4, 4, 0, 1);
6041 score.parts[0].staves[0].measures[0].voices[0] = vec![
6042 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
6043 Note::new(Pitch::new(Step::E, 4), Duration::Quarter),
6044 ];
6045 score.parts[0].midi_program_changes = vec![
6046 MidiProgramChange {
6047 tick: 480,
6048 channel: 1,
6049 program: 73,
6050 },
6051 MidiProgramChange {
6052 tick: 480,
6053 channel: 0,
6054 program: 40,
6055 },
6056 ];
6057 let events = to_playback_events(&score, &PlaybackOptions::default());
6058 assert_eq!(events.len(), 2);
6059 assert_eq!((events[0].program, events[1].program), (0, 40));
6060 assert_eq!((events[0].time_secs, events[1].time_secs), (0.0, 0.5));
6061 }
6062
6063 #[test]
6064 fn generic_playback_event_preserves_authored_guitar_technique() {
6065 let mut score = Score::new("technique", 120, 4, 4, 0, 1);
6066 let mut note = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
6067 note.guitar_technique = Some(GuitarTechnique::HammerOn);
6068 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
6069
6070 let event = to_playback_events(&score, &PlaybackOptions::default())
6071 .into_iter()
6072 .next()
6073 .expect("one note event");
6074 assert_eq!(event.guitar_technique, Some(GuitarTechnique::HammerOn));
6075 }
6076
6077 #[test]
6078 fn playback_timing_comparison_reports_tolerance_and_identity() {
6079 let expected = [comparison_event(1.0, 0.5)];
6080 let actual = [comparison_event(1.004, 0.503)];
6081 let report = compare_playback_timing(
6082 &expected,
6083 &actual,
6084 &PlaybackTimingTolerance {
6085 start_secs: 0.005,
6086 duration_secs: 0.005,
6087 },
6088 )
6089 .expect("comparison");
6090 assert!(report.within_tolerance);
6091 assert_eq!(report.matched_events, 1);
6092 assert_eq!(
6093 report.contract_version,
6094 PLAYBACK_COMPARISON_CONTRACT_VERSION
6095 );
6096
6097 let mut typed_only = comparison_event(1.004, 0.503);
6098 typed_only.address = None;
6099 let report = compare_playback_timing(
6100 &expected,
6101 &[typed_only],
6102 &PlaybackTimingTolerance::default(),
6103 )
6104 .expect("typed source comparison");
6105 assert_eq!(report.matched_events, 1);
6106
6107 let actual = [comparison_event(1.02, 0.6)];
6108 let report =
6109 compare_playback_timing(&expected, &actual, &PlaybackTimingTolerance::default())
6110 .expect("comparison");
6111 assert!(!report.within_tolerance);
6112 assert!(
6113 report
6114 .mismatches
6115 .iter()
6116 .any(|m| matches!(m, PlaybackTimingMismatch::StartTime { index: 0, .. }))
6117 );
6118 assert!(
6119 report
6120 .mismatches
6121 .iter()
6122 .any(|m| matches!(m, PlaybackTimingMismatch::Duration { index: 0, .. }))
6123 );
6124 }
6125
6126 #[test]
6127 fn playback_timing_comparison_rejects_instrument_route_mismatch() {
6128 let expected = comparison_event(0.0, 0.5);
6129 let mut actual = expected.clone();
6130 actual.program = 73;
6131 assert!(
6132 compare_playback_timing(&[expected], &[actual], &PlaybackTimingTolerance::default(),)
6133 .expect("comparison")
6134 .mismatches
6135 .iter()
6136 .any(|mismatch| matches!(
6137 mismatch,
6138 PlaybackTimingMismatch::EventIdentity { index: 0 }
6139 ))
6140 );
6141 }
6142
6143 #[test]
6144 fn playback_timing_comparison_rejects_invalid_tolerance() {
6145 let error = compare_playback_timing(
6146 &[],
6147 &[],
6148 &PlaybackTimingTolerance {
6149 start_secs: -0.1,
6150 duration_secs: 0.0,
6151 },
6152 );
6153 assert!(matches!(
6154 error,
6155 Err(crate::Error::InvalidPlaybackComparison)
6156 ));
6157 }
6158
6159 #[test]
6160 fn tablature_performance_projection_keeps_position_and_reports_pitch_error() {
6161 let mut score = Score::new("Tab", 120, 4, 4, 0, 1);
6162 score.parts[0].staves[0].tablature = Some(super::super::notation::TablatureConfig {
6163 lines: 6,
6164 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6165 capo: 0,
6166 });
6167 let notes = &mut score.parts[0].staves[0].measures[0].voices[0];
6168 *notes = vec![crate::Note::new(
6169 crate::Pitch::new(crate::Step::C, 4),
6170 crate::Duration::Quarter,
6171 )];
6172 notes[0].tab_position = Some(super::super::notation::TabPosition {
6173 string: 1,
6174 fret: 20,
6175 });
6176 notes[0].guitar_technique = Some(super::super::notation::GuitarTechnique::Bend);
6177 notes[0].guitar_bend_alter_cents = Some(150);
6178 notes[0].guitar_bend_curve = vec![
6179 super::super::score::GuitarBendPoint {
6180 position_per_mille: 0,
6181 alter_cents: 0,
6182 },
6183 super::super::score::GuitarBendPoint {
6184 position_per_mille: 500,
6185 alter_cents: 150,
6186 },
6187 super::super::score::GuitarBendPoint {
6188 position_per_mille: 1_000,
6189 alter_cents: 0,
6190 },
6191 ];
6192 let report = project_tablature_performance(&score, &PlaybackOptions::default())
6193 .expect("tablature projection");
6194 assert_eq!(report.events.len(), 1);
6195 assert_eq!(report.events[0].string, 1);
6196 assert_eq!(report.events[0].fret, 20);
6197 assert_eq!(
6198 report.events[0].technique,
6199 Some(super::super::notation::GuitarTechnique::Bend)
6200 );
6201 assert_eq!(report.events[0].bend_alter_cents, Some(150));
6202 assert_eq!(report.events[0].bend_curve.len(), 3);
6203 assert_eq!(report.events[0].bend_curve[1].position_per_mille, 500);
6204 assert_eq!(report.events[0].bend_curve[1].alter_cents, 150);
6205 assert_eq!(
6206 report.events[0].playback.pitch_bend_curve,
6207 report.events[0].bend_curve
6208 );
6209 assert!(report.diagnostics.is_empty());
6210
6211 score.parts[0].staves[0].measures[0].voices[0][0].tab_position =
6212 Some(super::super::notation::TabPosition {
6213 string: 1,
6214 fret: 19,
6215 });
6216 let report = project_tablature_performance(&score, &PlaybackOptions::default())
6217 .expect("tablature projection");
6218 assert_eq!(report.events[0].pitch_error_cents, 100);
6219 assert!(matches!(
6220 report.diagnostics[0],
6221 TablaturePerformanceDiagnostic::PitchMismatch {
6222 error_cents: 100,
6223 ..
6224 }
6225 ));
6226 }
6227
6228 #[test]
6229 fn tablature_performance_projection_does_not_invent_missing_positions() {
6230 let mut score = Score::new("Tab", 120, 4, 4, 0, 1);
6231 score.parts[0].staves[0].tablature = Some(super::super::notation::TablatureConfig {
6232 lines: 6,
6233 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6234 capo: 0,
6235 });
6236 score.parts[0].staves[0].measures[0].voices[0].push(crate::Note::new(
6237 crate::Pitch::new(crate::Step::C, 4),
6238 crate::Duration::Quarter,
6239 ));
6240 let report = project_tablature_performance(&score, &PlaybackOptions::default())
6241 .expect("tablature projection");
6242 assert!(report.events.is_empty());
6243 assert!(matches!(
6244 report.diagnostics[0],
6245 TablaturePerformanceDiagnostic::MissingPosition { .. }
6246 ));
6247 }
6248
6249 #[test]
6250 fn tablature_performance_event_accepts_legacy_json_without_technique() {
6251 let mut score = Score::new("Tab", 120, 4, 4, 0, 1);
6252 score.parts[0].staves[0].tablature = Some(super::super::notation::TablatureConfig {
6253 lines: 6,
6254 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6255 capo: 0,
6256 });
6257 let mut note = crate::Note::new(
6258 crate::Pitch::new(crate::Step::C, 4),
6259 crate::Duration::Quarter,
6260 );
6261 note.tab_position = Some(super::super::notation::TabPosition {
6262 string: 1,
6263 fret: 20,
6264 });
6265 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
6266 let report = project_tablature_performance(&score, &PlaybackOptions::default())
6267 .expect("tablature projection");
6268 let mut legacy = serde_json::to_value(&report.events[0]).expect("event JSON");
6269 legacy
6270 .as_object_mut()
6271 .expect("event object")
6272 .remove("technique");
6273 let restored: TablaturePerformanceEvent =
6274 serde_json::from_value(legacy).expect("legacy event JSON");
6275 assert_eq!(restored.technique, None);
6276 }
6277
6278 #[test]
6279 fn tablature_round_trip_report_preserves_authored_positions() {
6280 let mut score = Score::new("Tab", 120, 4, 4, 0, 1);
6281 score.parts[0].staves[0].tablature = Some(super::super::notation::TablatureConfig {
6282 lines: 6,
6283 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6284 capo: 2,
6285 });
6286 let mut note = crate::Note::new(
6287 crate::Pitch::new(crate::Step::C, 4),
6288 crate::Duration::Quarter,
6289 );
6290 note.tab_positions = vec![crate::TabPosition { string: 5, fret: 1 }];
6291 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
6292 let report = tablature_round_trip_report(&score).expect("tab round-trip report");
6293 assert!(report.equivalent);
6294 assert_eq!(report.checked_notes, 1);
6295 assert_eq!(report.positioned_notes, 1);
6296 assert!(report.diagnostics.is_empty());
6297 }
6298
6299 #[test]
6300 fn tablature_performance_uses_measure_local_tuning_change() {
6301 let mut score = Score::new("Local tuning", 120, 4, 4, 0, 2);
6302 let staff = &mut score.parts[0].staves[0];
6303 staff.tablature = Some(super::super::notation::TablatureConfig {
6304 lines: 6,
6305 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6306 capo: 0,
6307 });
6308 staff.measures[1].tablature_change = Some(super::super::notation::TablatureConfig {
6309 lines: 6,
6310 tuning_midi: vec![38, 45, 50, 55, 59, 64],
6311 capo: 0,
6312 });
6313 let mut note = Note::new(Pitch::new(Step::C, 3), Duration::Quarter);
6314 note.tab_position = Some(TabPosition {
6315 string: 1,
6316 fret: 10,
6317 });
6318 staff.measures[1].voices[0] = vec![note];
6319
6320 let report = project_tablature_performance(&score, &PlaybackOptions::default())
6321 .expect("tablature projection");
6322 assert_eq!(report.events.len(), 1);
6323 assert_eq!(report.events[0].expected_pitch_midi_cents, 4_800);
6324 assert!(!report.diagnostics.iter().any(|diagnostic| matches!(
6325 diagnostic,
6326 TablaturePerformanceDiagnostic::PitchMismatch { .. }
6327 )));
6328 }
6329}