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 measure: usize,
3138 note: usize,
3139 notated_end_beats: f64,
3140 staccato: bool,
3141 }
3142
3143 let mut merged = Vec::with_capacity(events.len());
3144 let mut pending: HashMap<(usize, i32), Vec<PendingTie>> = HashMap::new();
3145 let mut clock = None;
3146 for event in events {
3147 let Some(source) = event.source.as_ref() else {
3148 merged.push(event);
3149 continue;
3150 };
3151 let tied_staff = score
3152 .parts
3153 .get(source.part)
3154 .and_then(|part| part.staves.get(source.staff));
3155 let tied_note = tied_staff
3156 .and_then(|staff| staff.measures.get(source.measure))
3157 .and_then(|measure| measure.voices.get(source.voice))
3158 .and_then(|voice| voice.get(source.note));
3159 let (Some(staff), Some(note)) = (tied_staff, tied_note) else {
3160 merged.push(event);
3161 continue;
3162 };
3163 let transpose_cents = i32::from(staff.transpose_semitones) * 100;
3165 let pitch_index = note
3166 .pitches
3167 .iter()
3168 .position(|pitch| pitch.to_midi_cents() + transpose_cents == event.pitch_midi_cents);
3169 let (tie_start, tie_end, partial_end) = match pitch_index {
3170 Some(index) => (
3171 note.pitch_tie_start(index),
3172 note.pitch_tie_end(index),
3173 note.pitch_incoming_partial_ties.get(index) == Some(&true),
3174 ),
3175 None => (note.tie_start, note.tie_end, false),
3176 };
3177 let mut articulation_scale = 1.0;
3178 let mut staccato = false;
3179 for articulation in ¬e.articulations {
3180 match articulation {
3181 Articulation::Staccato | Articulation::Staccatissimo => {
3182 articulation_scale *= 0.5;
3183 staccato = true;
3184 }
3185 Articulation::Fermata
3186 if options.fermata_multiplier.is_finite()
3187 && options.fermata_multiplier < 1.0 =>
3188 {
3189 articulation_scale *= options.fermata_multiplier.max(0.0)
3190 }
3191 _ => {}
3192 }
3193 }
3194 let notated_duration = if articulation_scale.is_finite()
3195 && articulation_scale > 0.0
3196 && event.duration_beats.is_finite()
3197 {
3198 event.duration_beats / articulation_scale
3199 } else {
3200 note.beats()
3201 };
3202 let explicit_source = pitch_index
3203 .and_then(|index| note.pitch_tie_sources.get(index))
3204 .and_then(Option::as_ref);
3205 let written_onset = |address: &NoteAddr| -> Option<f64> {
3206 let notes = &score
3207 .parts
3208 .get(address.part)?
3209 .staves
3210 .get(address.staff)?
3211 .measures
3212 .get(address.measure)?
3213 .voices
3214 .get(address.voice)?;
3215 Some(
3216 notes
3217 .iter()
3218 .take(address.note)
3219 .filter(|note| !note.is_grace && !note.is_cue)
3220 .map(Note::beats)
3221 .sum(),
3222 )
3223 };
3224 let target_onset = written_onset(source);
3225 let source_staff = source.staff;
3226 let source_voice = source.voice;
3227 let source_measure = source.measure;
3228 let source_note = source.note;
3229 let key = (source.part, event.pitch_midi_cents);
3230 if let Some(candidates) = pending.get_mut(&key) {
3231 candidates.retain(|candidate| candidate.notated_end_beats >= event.time_beats - 1e-9);
3232 if tie_end || partial_end {
3233 let matching = candidates
3234 .iter()
3235 .enumerate()
3236 .filter(|(_, candidate)| {
3237 let jumped = source.measure != candidate.measure.saturating_add(1)
3238 && (source.measure != candidate.measure
3239 || source.note <= candidate.note);
3240 let previous_address = NoteAddr {
3241 part: source.part,
3242 staff: candidate.staff,
3243 measure: candidate.measure,
3244 voice: candidate.voice,
3245 note: candidate.note,
3246 };
3247 let source_matches = explicit_source.is_none_or(|location| {
3248 written_onset(&previous_address)
3249 .zip(target_onset)
3250 .is_some_and(|(previous, target)| {
3251 location.matches(source, &previous_address, previous - target)
3252 })
3253 });
3254 let partial_matches = partial_end
3255 && jumped
3256 && candidate.staff == source.staff
3257 && candidate.voice == source.voice;
3258 source_matches
3259 && (tie_end || partial_matches)
3260 && (candidate.notated_end_beats - event.time_beats).abs() < 1e-9
3261 })
3262 .max_by_key(|(_, candidate)| {
3263 (
3264 candidate.staff == source.staff,
3265 candidate.voice == source.voice,
3266 )
3267 })
3268 .map(|(position, _)| position);
3269 if let Some(position) = matching {
3270 let mut candidate = candidates.remove(position);
3271 let previous = &mut merged[candidate.index];
3272 for articulation in event.articulations.iter().filter(|articulation| {
3273 matches!(
3274 articulation,
3275 Articulation::Staccato | Articulation::Staccatissimo
3276 )
3277 }) {
3278 if !previous.articulations.contains(articulation) {
3279 previous.articulations.push(articulation.clone());
3280 }
3281 }
3282 candidate.notated_end_beats = event.time_beats + notated_duration;
3283 candidate.staccato |= staccato;
3284 previous.duration_beats += event.duration_beats;
3285 previous.duration_secs += event.duration_secs;
3286 if candidate.staccato {
3287 previous.duration_beats =
3288 (candidate.notated_end_beats - previous.time_beats) * 0.5;
3289 let timeline =
3290 clock.get_or_insert_with(|| playback_measure_clock(score, options));
3291 if let Some(end_secs) = playback_secs_at_beat(
3292 timeline,
3293 previous.time_beats + previous.duration_beats,
3294 ) {
3295 previous.duration_secs = end_secs - previous.time_secs;
3296 }
3297 }
3298 if tie_start {
3299 candidate.staff = source.staff;
3300 candidate.voice = source.voice;
3301 candidate.measure = source.measure;
3302 candidate.note = source.note;
3303 candidates.push(candidate);
3304 }
3305 continue;
3306 }
3307 }
3308 }
3309
3310 let index = merged.len();
3311 merged.push(event);
3312 if tie_start {
3313 pending.entry(key).or_default().push(PendingTie {
3314 index,
3315 staff: source_staff,
3316 voice: source_voice,
3317 measure: source_measure,
3318 note: source_note,
3319 notated_end_beats: merged[index].time_beats + notated_duration,
3320 staccato,
3321 });
3322 }
3323 }
3324 merged
3325}
3326
3327#[derive(Clone, Copy)]
3328struct PlaybackClockMeasure {
3329 start_beats: f64,
3330 start_secs: f64,
3331 bpm: f64,
3332 ramp_to_bpm: Option<f64>,
3333 beats: f64,
3334}
3335
3336fn playback_measure_clock(score: &Score, options: &PlaybackOptions) -> Vec<PlaybackClockMeasure> {
3337 let marks = score_tempo_marks(score);
3338 let mut bpm = initial_tempo_bpm(score, options);
3339 let mut start_beats = 0.0;
3340 let mut start_secs = 0.0;
3341 let mut clock = Vec::new();
3342 for index in measure_sequence(score).into_iter().filter(|index| {
3343 options
3344 .loop_region
3345 .is_none_or(|(start, end)| *index >= start && *index <= end)
3346 }) {
3347 let mark = marks.get(index).copied().unwrap_or_default();
3348 if let Some(next) = mark.bpm.and_then(|tempo| written_tempo_bpm(options, tempo)) {
3349 bpm = next;
3350 }
3351 let beats = score.playback_measure_beats(index);
3352 let ramp_to_bpm = written_tempo_ramp_bpm(options, mark.ramp_to_bpm);
3353 clock.push(PlaybackClockMeasure {
3354 start_beats,
3355 start_secs,
3356 bpm,
3357 ramp_to_bpm,
3358 beats,
3359 });
3360 start_beats += beats;
3361 start_secs += tempo_ramp_seconds(bpm, ramp_to_bpm, beats, beats);
3362 if let Some(next) = ramp_to_bpm {
3363 bpm = next;
3364 }
3365 }
3366 clock
3367}
3368
3369#[derive(Clone, Copy, Debug)]
3371struct FermataHold {
3372 at_beat: f64,
3373 at_secs: f64,
3374 extra_beats: f64,
3375 extra_secs: f64,
3376 cumulative_beats: f64,
3377 cumulative_secs: f64,
3378}
3379
3380fn score_fermata_holds(score: &Score, options: &PlaybackOptions) -> Vec<FermataHold> {
3381 let multiplier = options.fermata_multiplier;
3382 if !multiplier.is_finite()
3383 || multiplier <= 1.0
3384 || !score
3385 .parts
3386 .iter()
3387 .flat_map(|part| &part.staves)
3388 .flat_map(|staff| &staff.measures)
3389 .flat_map(|measure| &measure.voices)
3390 .flatten()
3391 .any(|note| note.articulations.contains(&Articulation::Fermata))
3392 {
3393 return Vec::new();
3394 }
3395 let sequence = measure_sequence(score).into_iter().filter(|index| {
3396 options
3397 .loop_region
3398 .is_none_or(|(start, end)| *index >= start && *index <= end)
3399 });
3400 let clock = playback_measure_clock(score, options);
3401 let swing = options
3402 .swing
3403 .filter(|ratio| ratio.is_finite() && *ratio > 0.5 && *ratio < 1.0);
3404 let mut holds = Vec::new();
3405 for (index, measure_clock) in sequence.zip(clock.iter()) {
3406 for staff in score.parts.iter().flat_map(|part| &part.staves) {
3407 let Some(measure) = staff.measures.get(index) else {
3408 continue;
3409 };
3410 let grid_offset = if index == 0 {
3411 let meter = staff
3412 .meter_at(index, &score.settings.time_signature)
3413 .total_beats();
3414 (meter - measure_clock.beats).max(0.0)
3415 } else {
3416 0.0
3417 };
3418 for voice in &measure.voices {
3419 let (mut local, mut written) = (0.0, 0.0);
3420 let mut swing_second = false;
3421 for (note_index, note) in voice.iter().enumerate() {
3422 if note.is_grace || note.is_cue {
3423 continue;
3424 }
3425 let duration = if let Some(ratio) = swing {
3426 if swing_second {
3427 swing_second = false;
3428 (1.0 - ratio) * note.beats() * 2.0
3429 } else if starts_swing_pair(
3430 voice,
3431 note_index,
3432 written,
3433 grid_offset,
3434 measure_clock.beats,
3435 &options.swing_unit,
3436 ) {
3437 swing_second = true;
3438 ratio * note.beats() * 2.0
3439 } else {
3440 note.beats()
3441 }
3442 } else {
3443 note.beats()
3444 };
3445 written += note.beats();
3446 local += duration;
3447 if note.articulations.contains(&Articulation::Fermata) && duration > 0.0 {
3448 let at_beat = measure_clock.start_beats + local;
3449 let Some(at_secs) = playback_secs_at_beat(&clock, at_beat) else {
3450 continue;
3451 };
3452 let bpm = measure_clock.ramp_to_bpm.map_or(measure_clock.bpm, |end| {
3453 measure_clock.bpm
3454 + (end - measure_clock.bpm)
3455 * (local / measure_clock.beats.max(f64::EPSILON))
3456 .clamp(0.0, 1.0)
3457 });
3458 let extra_beats = duration * (multiplier - 1.0);
3459 holds.push(FermataHold {
3460 at_beat,
3461 at_secs,
3462 extra_beats,
3463 extra_secs: extra_beats * 60.0 / bpm,
3464 cumulative_beats: 0.0,
3465 cumulative_secs: 0.0,
3466 });
3467 }
3468 }
3469 }
3470 }
3471 }
3472 holds.sort_by(|a, b| a.at_beat.total_cmp(&b.at_beat));
3473 let mut unique: Vec<FermataHold> = Vec::new();
3474 for hold in holds {
3475 if let Some(last) = unique.last_mut()
3476 && (last.at_beat - hold.at_beat).abs() < 1e-9
3477 {
3478 if hold.extra_beats > last.extra_beats {
3479 *last = hold;
3480 }
3481 } else {
3482 unique.push(hold);
3483 }
3484 }
3485 let (mut beats, mut seconds) = (0.0, 0.0);
3486 for hold in &mut unique {
3487 beats += hold.extra_beats;
3488 seconds += hold.extra_secs;
3489 hold.cumulative_beats = beats;
3490 hold.cumulative_secs = seconds;
3491 }
3492 unique
3493}
3494
3495pub(super) fn fermata_total_secs(score: &Score, region: Option<(usize, usize)>) -> f64 {
3496 let options = PlaybackOptions {
3497 loop_region: region,
3498 ..Default::default()
3499 };
3500 score_fermata_holds(score, &options)
3501 .last()
3502 .map_or(0.0, |hold| hold.cumulative_secs)
3503}
3504
3505pub fn playback_fermata_pauses(score: &Score, options: &PlaybackOptions) -> Vec<(f64, f64)> {
3508 score_fermata_holds(score, options)
3509 .into_iter()
3510 .map(|hold| (hold.at_beat, hold.extra_beats))
3511 .collect()
3512}
3513
3514fn apply_fermata_holds(events: &mut [PlaybackEvent], holds: &[FermataHold]) {
3515 let before = |index: usize| index.checked_sub(1).and_then(|i| holds.get(i));
3516 for event in events {
3517 let start = event.time_beats;
3518 let end = start + event.duration_beats;
3519 let first = holds.partition_point(|hold| hold.at_beat <= start + 1e-9);
3520 let last = holds.partition_point(|hold| hold.at_beat <= end + 1e-9);
3521 let start_beats = before(first).map_or(0.0, |hold| hold.cumulative_beats);
3522 let start_secs = before(first).map_or(0.0, |hold| hold.cumulative_secs);
3523 let end_beats = before(last).map_or(0.0, |hold| hold.cumulative_beats);
3524 let end_secs = before(last).map_or(0.0, |hold| hold.cumulative_secs);
3525 event.time_beats += start_beats;
3526 event.time_secs += start_secs;
3527 event.duration_beats += end_beats - start_beats;
3528 event.duration_secs += end_secs - start_secs;
3529 }
3530}
3531
3532fn fermata_warp_secs(seconds: f64, holds: &[FermataHold]) -> f64 {
3533 let count = holds.partition_point(|hold| hold.at_secs <= seconds + 1e-9);
3534 seconds
3535 + count
3536 .checked_sub(1)
3537 .map_or(0.0, |index| holds[index].cumulative_secs)
3538}
3539
3540fn fermata_unwarp_secs(seconds: f64, holds: &[FermataHold]) -> f64 {
3541 let count = holds
3542 .partition_point(|hold| hold.at_secs + hold.cumulative_secs - hold.extra_secs <= seconds);
3543 if count == 0 {
3544 return seconds;
3545 }
3546 let hold = holds[count - 1];
3547 let start = hold.at_secs + hold.cumulative_secs - hold.extra_secs;
3548 if seconds < start + hold.extra_secs {
3549 hold.at_secs
3550 } else {
3551 seconds - hold.cumulative_secs
3552 }
3553}
3554
3555fn playback_secs_at_beat(clock: &[PlaybackClockMeasure], beat: f64) -> Option<f64> {
3556 let index = clock.partition_point(|measure| measure.start_beats <= beat);
3557 let measure = clock.get(index.saturating_sub(1))?;
3558 let offset = beat - measure.start_beats;
3559 if offset > measure.beats + 1e-9 {
3560 return None;
3561 }
3562 Some(
3563 measure.start_secs
3564 + tempo_ramp_seconds(
3565 measure.bpm,
3566 measure.ramp_to_bpm,
3567 measure.beats,
3568 offset.clamp(0.0, measure.beats),
3569 ),
3570 )
3571}
3572
3573pub fn to_playback_events_bounded(
3575 score: &Score,
3576 options: &PlaybackOptions,
3577) -> Result<Vec<PlaybackEvent>, crate::Error> {
3578 let events = to_playback_events(score, options);
3579 if events.len() > MAX_PLAYBACK_COMPARISON_EVENTS {
3580 return Err(crate::Error::PlaybackComparisonTooLarge(events.len()));
3581 }
3582 Ok(events)
3583}
3584
3585#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3587pub struct SharedUnisonPlaybackEvent {
3588 pub event: PlaybackEvent,
3589 pub sources: Vec<NoteAddr>,
3590}
3591
3592pub fn coalesce_shared_unisons(events: Vec<PlaybackEvent>) -> Vec<SharedUnisonPlaybackEvent> {
3599 use std::collections::HashMap;
3600 let mut groups: Vec<SharedUnisonPlaybackEvent> = Vec::with_capacity(events.len());
3601 let mut buckets: HashMap<(usize, u64, i32, u8, u8), Vec<usize>> = HashMap::new();
3602 for event in events {
3603 let Some(source) = event.source.clone().filter(|_| !event.is_metronome) else {
3604 groups.push(SharedUnisonPlaybackEvent {
3605 event,
3606 sources: Vec::new(),
3607 });
3608 continue;
3609 };
3610 let key = (
3611 event.part_index,
3612 event.time_beats.to_bits(),
3613 event.pitch_midi_cents,
3614 event.channel,
3615 event.program,
3616 );
3617 let mut normalized = event.clone();
3618 normalized.address = None;
3619 normalized.source = None;
3620 normalized.source_voice_number = None;
3621 let candidates = buckets.entry(key).or_default();
3622 let matching = candidates.iter().take(256).copied().find(|&index| {
3624 let group = &groups[index];
3625 if group.sources.len() >= 256
3626 || group
3627 .sources
3628 .iter()
3629 .any(|other| other.staff == source.staff && other.voice == source.voice)
3630 {
3631 return false;
3632 }
3633 let mut other = group.event.clone();
3634 other.address = None;
3635 other.source = None;
3636 other.source_voice_number = None;
3637 normalized == other
3638 });
3639 if let Some(index) = matching {
3640 groups[index].sources.push(source);
3641 } else {
3642 if candidates.len() < 256 {
3643 candidates.push(groups.len());
3644 }
3645 groups.push(SharedUnisonPlaybackEvent {
3646 event,
3647 sources: vec![source],
3648 });
3649 }
3650 }
3651 groups
3652}
3653
3654#[derive(Debug, Clone, Serialize, Deserialize)]
3658pub struct PlaybackPosition {
3659 pub measure_index: usize,
3661 pub beat: f64,
3663}
3664
3665struct MeasureSegment {
3666 measure_idx: usize,
3667 start_secs: f64,
3668 duration_secs: f64,
3669 beats: f64,
3670 bpm: f64,
3671 ramp_to_bpm: Option<f64>,
3672}
3673
3674fn build_measure_segments(score: &Score, options: &PlaybackOptions) -> Vec<MeasureSegment> {
3675 let init_bpm = initial_tempo_bpm(score, options);
3676 let tempo_marks = score_tempo_marks(score);
3677 let full_seq = measure_sequence(score);
3678 let seq: Vec<usize> = if let Some((lo, hi)) = options.loop_region {
3679 full_seq
3680 .into_iter()
3681 .filter(|&i| i >= lo && i <= hi)
3682 .collect()
3683 } else {
3684 full_seq
3685 };
3686
3687 let mut segments = Vec::with_capacity(seq.len());
3688 let mut cursor_secs = 0.0f64;
3689 let mut current_bpm = init_bpm;
3690
3691 for idx in seq {
3692 let mark = tempo_marks.get(idx).copied().unwrap_or_default();
3693 if let Some(t) = mark.bpm.and_then(|tempo| written_tempo_bpm(options, tempo)) {
3694 current_bpm = t;
3695 }
3696 let beats = score.playback_measure_beats(idx);
3697 let ramp_to_bpm = written_tempo_ramp_bpm(options, mark.ramp_to_bpm);
3698 let duration_secs = tempo_ramp_seconds(current_bpm, ramp_to_bpm, beats, beats);
3699
3700 segments.push(MeasureSegment {
3701 measure_idx: idx,
3702 start_secs: cursor_secs,
3703 duration_secs,
3704 beats,
3705 bpm: current_bpm,
3706 ramp_to_bpm,
3707 });
3708 cursor_secs += duration_secs;
3709 if let Some(end_bpm) = ramp_to_bpm {
3710 current_bpm = end_bpm;
3711 }
3712 }
3713 segments
3714}
3715
3716pub fn compute_playback_position(
3722 score: &Score,
3723 options: &PlaybackOptions,
3724 elapsed_secs: f64,
3725) -> Option<PlaybackPosition> {
3726 if elapsed_secs < 0.0 {
3727 return None;
3728 }
3729 let holds = score_fermata_holds(score, options);
3730 let elapsed_secs = fermata_unwarp_secs(elapsed_secs, &holds);
3731 let segments = build_measure_segments(score, options);
3732 for seg in &segments {
3733 if elapsed_secs < seg.start_secs + seg.duration_secs + 1e-9 {
3734 let beat = tempo_ramp_beats_at_seconds(
3735 seg.bpm,
3736 seg.ramp_to_bpm,
3737 seg.beats,
3738 elapsed_secs - seg.start_secs,
3739 )
3740 .clamp(0.0, seg.beats);
3741 return Some(PlaybackPosition {
3742 measure_index: seg.measure_idx,
3743 beat,
3744 });
3745 }
3746 }
3747 None
3748}
3749
3750#[cfg(test)]
3751mod tests {
3752 use super::*;
3753 use crate::model::{
3754 duration::Duration,
3755 pitch::{Pitch, Step},
3756 score::{MidiAftertouch, MidiControlChange, MidiPitchBend, MidiProgramChange, Note, Score},
3757 };
3758
3759 fn opts(bpm: Option<u16>) -> PlaybackOptions {
3760 PlaybackOptions {
3761 bpm_override: bpm,
3762 ..Default::default()
3763 }
3764 }
3765
3766 #[test]
3767 fn shared_unison_policy_never_merges_different_routing_or_same_voice() {
3768 let mut score = Score::new("unison", 120, 4, 4, 0, 1);
3769 let note = Note::new(Pitch::new(Step::A, 3), Duration::Whole);
3770 score.parts[0].staves[0].measures[0].voices[0] = vec![note.clone()];
3771 score.parts[0].staves[0].measures[0].voices[1] = vec![note];
3772 let events = to_playback_events(&score, &PlaybackOptions::default());
3773 assert_eq!(coalesce_shared_unisons(events.clone()).len(), 1);
3774 let mut different_program = events.clone();
3775 different_program[1].program += 1;
3776 assert_eq!(coalesce_shared_unisons(different_program).len(), 2);
3777 let mut different_expression = events.clone();
3778 different_expression[1].velocity += 1;
3779 assert_eq!(coalesce_shared_unisons(different_expression).len(), 2);
3780 let mut same_voice = events;
3781 let voice = same_voice[0].source.as_ref().unwrap().voice;
3782 same_voice[1].source.as_mut().unwrap().voice = voice;
3783 assert_eq!(coalesce_shared_unisons(same_voice).len(), 2);
3784 }
3785
3786 #[test]
3787 fn rest_pedal_controls_follow_swing_and_muted_parts() {
3788 let mut score = Score::new("rest pedal", 120, 2, 4, 0, 1);
3789 let first = Note::rest(Duration::Eighth);
3790 let mut second = Note::rest(Duration::Eighth);
3791 second.pedal_start = true;
3792 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3793 note.pedal_end = true;
3794 score.parts[0].staves[0].measures[0].voices[0] = vec![first, second, note];
3795 let mut options = PlaybackOptions {
3796 swing: Some(0.67),
3797 ..Default::default()
3798 };
3799 let manifest =
3800 build_offline_render_manifest(&score, &options, &OfflineRenderRequest::default())
3801 .unwrap();
3802 let controls: Vec<_> = manifest
3803 .semantic_events
3804 .iter()
3805 .filter_map(|event| match event.kind {
3806 OfflineRenderSemanticEventKind::Pedal { down } => Some((event.frame, down)),
3807 _ => None,
3808 })
3809 .collect();
3810 assert_eq!(controls, vec![(16080, true), (24000, false)]);
3811 options.muted_parts.push(0);
3812 let muted =
3813 build_offline_render_manifest(&score, &options, &OfflineRenderRequest::default())
3814 .unwrap();
3815 assert!(
3816 !muted
3817 .semantic_events
3818 .iter()
3819 .any(|event| matches!(event.kind, OfflineRenderSemanticEventKind::Pedal { .. }))
3820 );
3821 }
3822
3823 #[test]
3824 fn playback_options_accepts_an_empty_json_object_as_defaults() {
3825 let parsed: PlaybackOptions = serde_json::from_str("{}").expect("default playback options");
3826 assert_eq!(parsed, PlaybackOptions::default());
3827 }
3828
3829 #[test]
3830 fn bounded_playback_events_match_unbounded_schedule_within_limit() {
3831 let score = Score::new("bounded", 120, 4, 4, 0, 1);
3832 let options = opts(Some(120));
3833 let unbounded = to_playback_events(&score, &options);
3834 let bounded = to_playback_events_bounded(&score, &options).expect("bounded schedule");
3835 assert_eq!(bounded, unbounded);
3836 }
3837
3838 #[test]
3839 fn bars_after_a_meter_change_keep_the_new_meter() {
3840 use crate::model::duration::Duration;
3841 use crate::{Note, Pitch, Step, TimeSignature};
3842 let mut score = Score::new("meter", 120, 4, 4, 0, 3);
3843 let staff = &mut score.parts[0].staves[0];
3844 staff.measures[1].time_sig = Some(TimeSignature {
3845 numerator: 3,
3846 denominator: 4,
3847 });
3848 for measure in &mut staff.measures {
3849 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3850 }
3851 let onsets: Vec<f64> = to_playback_events(&score, &opts(Some(120)))
3852 .iter()
3853 .filter(|event| event.address.is_some())
3854 .map(|event| event.time_beats)
3855 .collect();
3856 assert_eq!(onsets, vec![0.0, 4.0, 7.0]);
3858 }
3859
3860 #[test]
3861 fn tempo_written_on_either_part_governs_the_whole_score() {
3862 let mut score = Score::new("shared tempo", 120, 4, 4, 0, 2);
3863 let mut second = score.parts[0].clone();
3864 second.id = "second".into();
3865 score.parts.push(second);
3866 for part in &mut score.parts {
3867 for measure in &mut part.staves[0].measures {
3868 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3869 }
3870 }
3871 score.parts[0].staves[0].measures[0].tempo = Some(92);
3872 score.parts[1].staves[0].measures[1].tempo = Some(60);
3873 let options = PlaybackOptions {
3874 metronome: Some(MetronomeConfig::default()),
3875 ..Default::default()
3876 };
3877 let events = to_playback_events(&score, &options);
3878 for part_index in 0..2 {
3879 let first = events
3880 .iter()
3881 .find(|event| {
3882 event.part_index == part_index
3883 && event
3884 .source
3885 .as_ref()
3886 .is_some_and(|source| source.measure == 0)
3887 })
3888 .unwrap();
3889 let second = events
3890 .iter()
3891 .find(|event| {
3892 event.part_index == part_index
3893 && event
3894 .source
3895 .as_ref()
3896 .is_some_and(|source| source.measure == 1)
3897 })
3898 .unwrap();
3899 assert!((first.duration_secs - 60.0 / 92.0).abs() < 1e-9);
3900 assert!((second.time_secs - 4.0 * 60.0 / 92.0).abs() < 1e-9);
3901 assert!((second.duration_secs - 1.0).abs() < 1e-9);
3902 }
3903 let clicks: Vec<_> = events.iter().filter(|event| event.is_metronome).collect();
3904 assert!((clicks[1].time_secs - 60.0 / 92.0).abs() < 1e-9);
3905 assert!((clicks[4].time_secs - 4.0 * 60.0 / 92.0).abs() < 1e-9);
3906 let position = compute_playback_position(&score, &options, 1.5 * 60.0 / 92.0).unwrap();
3907 assert_eq!(position.measure_index, 0);
3908 assert!((position.beat - 1.5).abs() < 1e-9);
3909 let timeline_1 = offline_measure_times(&score, &options, 1, 0, &score_tempo_marks(&score));
3910 assert!((timeline_1[0].bpm - 92.0).abs() < 1e-9);
3911 assert!((timeline_1[1].bpm - 60.0).abs() < 1e-9);
3912 }
3913
3914 #[test]
3915 fn empty_score_no_events() {
3916 let score = Score::new("T", 120, 4, 4, 0, 1);
3917 assert!(to_playback_events(&score, &opts(None)).is_empty());
3918 }
3919
3920 #[test]
3921 fn single_note_at_beat_zero() {
3922 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3923 score.parts[0].staves[0].measures[0].voices[0] =
3924 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3925 let events = to_playback_events(&score, &opts(None));
3926 assert_eq!(events.len(), 1);
3927 assert_eq!(events[0].address.as_deref(), Some("0:0:0:0:0"));
3928 assert_eq!(
3929 events[0].source,
3930 Some(NoteAddr {
3931 part: 0,
3932 staff: 0,
3933 measure: 0,
3934 voice: 0,
3935 note: 0,
3936 })
3937 );
3938 assert!((events[0].time_beats).abs() < 1e-9);
3939 assert_eq!(events[0].pitch_midi, 60);
3940 assert_eq!(events[0].velocity, 64);
3941 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
3942 assert_eq!(events[0].part_index, 0);
3943 }
3944
3945 #[test]
3946 fn tied_notes_are_one_continuous_playback_event() {
3947 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3948 let mut first = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3949 first.tie_start = true;
3950 let mut second = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3951 second.tie_end = true;
3952 score.parts[0].staves[0].measures[0].voices[0] = vec![first, second];
3953
3954 let events = to_playback_events(&score, &opts(None));
3955 assert_eq!(events.len(), 1);
3956 assert_eq!(
3957 events[0].source.as_ref().map(|address| address.note),
3958 Some(0)
3959 );
3960 assert!((events[0].time_beats).abs() < 1e-9);
3961 assert!((events[0].duration_beats - 2.0).abs() < 1e-9);
3962 assert!((events[0].duration_secs - 1.0).abs() < 1e-9);
3963 }
3964
3965 #[test]
3966 fn staccato_tied_across_barline_has_one_half_length_attack() {
3967 for marks in [[true, false], [false, true], [true, true]] {
3968 let mut score = Score::new("tied staccato", 120, 4, 4, 0, 2);
3969 let mut first = Note::new(Pitch::new(Step::D, 5), Duration::Eighth);
3970 first.tie_start = true;
3971 let mut second = Note::new(Pitch::new(Step::D, 5), Duration::Eighth);
3972 second.tie_end = true;
3973 if marks[0] {
3974 first.articulations.push(Articulation::Staccato);
3975 }
3976 if marks[1] {
3977 second.articulations.push(Articulation::Staccato);
3978 }
3979 score.parts[0].staves[0].measures[0].voices[0] = vec![
3980 Note::rest(Duration::Half),
3981 Note::rest(Duration::Quarter),
3982 Note::rest(Duration::Eighth),
3983 first,
3984 ];
3985 score.parts[0].staves[0].measures[1].voices[0] = vec![second];
3986 let events = to_playback_events(&score, &opts(None));
3987 assert_eq!(events.len(), 1, "articulations: {marks:?}");
3988 assert_eq!(events[0].pitch_midi, 74);
3989 assert_eq!(events[0].time_beats, 3.5);
3990 assert!(events[0].articulations.contains(&Articulation::Staccato));
3991 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
3992 assert!((events[0].duration_secs - 0.25).abs() < 1e-9);
3993 }
3994 }
3995
3996 #[test]
3997 fn tied_staccato_uses_tempo_at_sounding_end() {
3998 let mut score = Score::new("tied tempo", 120, 4, 4, 0, 2);
3999 let mut first = Note::new(Pitch::new(Step::D, 5), Duration::Eighth);
4000 first.tie_start = true;
4001 first.articulations.push(Articulation::Staccato);
4002 let mut second = Note::new(Pitch::new(Step::D, 5), Duration::Quarter);
4003 second.dot_count = 1;
4004 second.tie_end = true;
4005 score.parts[0].staves[0].measures[0].voices[0] = vec![
4006 Note::rest(Duration::Half),
4007 Note::rest(Duration::Quarter),
4008 Note::rest(Duration::Eighth),
4009 first,
4010 ];
4011 score.parts[0].staves[0].measures[1].tempo = Some(60);
4012 score.parts[0].staves[0].measures[1].voices[0] = vec![second];
4013 let events = to_playback_events(&score, &opts(None));
4014 assert_eq!(events.len(), 1);
4015 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
4016 assert!((events[0].duration_secs - 0.75).abs() < 1e-9);
4017 }
4018
4019 #[test]
4020 fn a_tie_on_one_chord_pitch_holds_only_that_pitch() {
4021 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4022 let chord = || {
4024 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4025 note.pitches.push(Pitch::new(Step::G, 4));
4026 note
4027 };
4028 let mut first = chord();
4029 first.set_pitch_ties(&[true, false], &[false, false]);
4030 let mut second = chord();
4031 second.set_pitch_ties(&[false, false], &[true, false]);
4032 assert!(first.tie_start && second.tie_end);
4033 score.parts[0].staves[0].measures[0].voices[0] = vec![first, second];
4034
4035 let events = to_playback_events(&score, &opts(None));
4036 let durations = |midi: u8| {
4037 events
4038 .iter()
4039 .filter(|event| event.pitch_midi == midi)
4040 .map(|event| event.duration_beats)
4041 .collect::<Vec<_>>()
4042 };
4043 assert_eq!(durations(60), vec![2.0]);
4044 assert_eq!(durations(67), vec![1.0, 1.0]);
4045 }
4046
4047 #[test]
4048 fn ties_cross_measure_boundaries_and_tempo_changes_without_retriggering() {
4049 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4050 let first_measure = &mut score.parts[0].staves[0].measures[0];
4051 let mut first = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
4052 first.tie_start = true;
4053 first_measure.voices[0] = vec![first];
4054
4055 let second_measure = &mut score.parts[0].staves[0].measures[1];
4056 second_measure.tempo = Some(60);
4057 let mut second = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
4058 second.tie_end = true;
4059 second_measure.voices[0] = vec![second];
4060
4061 let events = to_playback_events(&score, &opts(None));
4062 assert_eq!(events.len(), 1);
4063 assert_eq!(
4064 events[0].source.as_ref().map(|address| address.measure),
4065 Some(0)
4066 );
4067 assert!((events[0].time_beats).abs() < 1e-9);
4068 assert!((events[0].duration_beats - 8.0).abs() < 1e-9);
4069 assert!((events[0].duration_secs - 6.0).abs() < 1e-9);
4070 }
4071
4072 #[test]
4073 fn ties_continue_across_voice_and_staff_changes() {
4074 let whole_c = || Note::new(Pitch::new(Step::C, 4), Duration::Whole);
4075
4076 let mut voice_score = Score::new("voice change", 120, 4, 4, 0, 2);
4077 let mut start = whole_c();
4078 start.tie_start = true;
4079 let mut end = whole_c();
4080 end.tie_end = true;
4081 voice_score.parts[0].staves[0].measures[0].voices[0] = vec![start.clone()];
4082 voice_score.parts[0].staves[0].measures[1].voices[1] = vec![end.clone()];
4083 let events = to_playback_events(&voice_score, &opts(None));
4084 assert_eq!(events.len(), 1);
4085 assert_eq!(events[0].time_beats, 0.0);
4086 assert_eq!(events[0].duration_beats, 8.0);
4087
4088 let mut staff_score = Score::new("staff change", 120, 4, 4, 0, 2);
4089 let second_staff = staff_score.parts[0].staves[0].clone();
4090 staff_score.parts[0].staves.push(second_staff);
4091 staff_score.parts[0].staves[0].measures[0].voices[0] = vec![start];
4092 staff_score.parts[0].staves[1].measures[1].voices[0] = vec![end];
4093 let events = to_playback_events(&staff_score, &opts(None));
4094 assert_eq!(events.len(), 1);
4095 assert_eq!(events[0].time_beats, 0.0);
4096 assert_eq!(events[0].duration_beats, 8.0);
4097 }
4098
4099 #[test]
4100 fn cross_voice_tie_keeps_independent_unison_attacks() {
4101 let mut score = Score::new("unison", 120, 4, 4, 0, 2);
4102 let mut start = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
4103 start.tie_start = true;
4104 let mut end = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
4105 end.tie_end = true;
4106 score.parts[0].staves[0].measures[0].voices[0] = vec![start];
4107 score.parts[0].staves[0].measures[0].voices[1] =
4108 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4109 score.parts[0].staves[0].measures[1].voices[1] = vec![end];
4110 score.parts[0].staves[0].measures[1].voices[2] =
4111 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4112
4113 let mut durations: Vec<_> = to_playback_events(&score, &opts(None))
4114 .into_iter()
4115 .map(|event| event.duration_beats)
4116 .collect();
4117 durations.sort_by(f64::total_cmp);
4118 assert_eq!(durations, vec![4.0, 4.0, 8.0]);
4119 }
4120
4121 #[test]
4122 fn malformed_tie_end_does_not_drop_playback_event() {
4123 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4124 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4125 note.tie_end = true;
4126 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4127 let events = to_playback_events(&score, &opts(None));
4128 assert_eq!(events.len(), 1);
4129 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
4130 }
4131
4132 #[test]
4133 fn microtonal_playback_event_keeps_exact_midi_cents() {
4134 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4135 score.parts[0].staves[0].measures[0].voices[0] = vec![Note::new(
4136 Pitch::with_microtone(Step::C, 4, 0, 50),
4137 Duration::Quarter,
4138 )];
4139 let events = to_playback_events(&score, &opts(None));
4140 assert_eq!(events[0].pitch_midi, 61);
4141 assert_eq!(events[0].pitch_midi_cents, 6050);
4142 }
4143
4144 #[test]
4145 fn chord_expands_to_multiple_events() {
4146 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4147 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4148 note.pitches.push(Pitch::new(Step::E, 4));
4149 note.pitches.push(Pitch::new(Step::G, 4));
4150 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4151 let events = to_playback_events(&score, &opts(None));
4152 assert_eq!(events.len(), 3);
4153 assert!(
4154 events
4155 .iter()
4156 .all(|event| event.address.as_deref() == Some("0:0:0:0:0"))
4157 );
4158 assert!(events.iter().all(|e| e.time_beats.abs() < 1e-9));
4159 }
4160
4161 #[test]
4162 fn playback_events_preserve_authored_chord_symbol_without_inventing_notes() {
4163 let mut score = Score::new("Harmony", 120, 4, 4, 0, 1);
4164 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4165 note.chord_symbol = Some(ChordSymbol {
4166 root: "C".into(),
4167 kind: "major-seventh".into(),
4168 bass: Some("E".into()),
4169 placement: None,
4170 extender: false,
4171 harmonic_degree: None,
4172 harmony_function: None,
4173 harmony_type: None,
4174 chord_ref: None,
4175 range_end: None,
4176 degrees: Vec::new(),
4177 });
4178 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4179
4180 let events = to_playback_events(&score, &PlaybackOptions::default());
4181 assert_eq!(events.len(), 1);
4182 assert_eq!(events[0].pitch_midi, 60);
4183 assert_eq!(
4184 events[0]
4185 .chord_symbol
4186 .as_ref()
4187 .map(ChordSymbol::display_text),
4188 Some("Cmaj7/E".into())
4189 );
4190 }
4191
4192 #[test]
4193 fn authored_realization_default_keeps_ornament_as_one_semantic_event() {
4194 let mut score = Score::new("authored ornament", 120, 4, 4, 0, 1);
4195 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4196 note.articulations.push(Articulation::Trill);
4197 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4198
4199 let events = to_playback_events(&score, &PlaybackOptions::default());
4200 assert_eq!(events.len(), 1);
4201 assert_eq!(events[0].pitch_midi, 60);
4202 assert_eq!(events[0].articulations, vec![Articulation::Trill]);
4203 }
4204
4205 #[test]
4206 fn ornament_arpeggio_profile_realizes_trill_with_pinned_timing() {
4207 let mut score = Score::new("realized ornament", 120, 4, 4, 0, 1);
4208 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4209 note.articulations.push(Articulation::Trill);
4210 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4211
4212 let events = to_playback_events(
4213 &score,
4214 &PlaybackOptions {
4215 realization_profile: PlaybackRealizationProfile::OrnamentArpeggioV1,
4216 ..Default::default()
4217 },
4218 );
4219 assert_eq!(events.len(), 4);
4220 assert_eq!(
4221 events
4222 .iter()
4223 .map(|event| event.pitch_midi)
4224 .collect::<Vec<_>>(),
4225 vec![60, 61, 60, 61]
4226 );
4227 for (index, event) in events.iter().enumerate() {
4228 assert!((event.time_beats - index as f64 * 0.25).abs() < 1e-9);
4229 assert!((event.duration_beats - 0.25).abs() < 1e-9);
4230 assert!((event.time_secs - index as f64 * 0.125).abs() < 1e-9);
4231 assert!((event.duration_secs - 0.125).abs() < 1e-9);
4232 assert!(!event.articulations.contains(&Articulation::Trill));
4233 }
4234 }
4235
4236 #[test]
4237 fn ornament_arpeggio_profile_staggers_chord_in_authored_direction() {
4238 let mut score = Score::new("realized arpeggio", 120, 4, 4, 0, 1);
4239 let mut chord = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4240 chord.pitches.push(Pitch::new(Step::E, 4));
4241 chord.pitches.push(Pitch::new(Step::G, 4));
4242 chord.arpeggiate = Some(false);
4243 score.parts[0].staves[0].measures[0].voices[0] = vec![chord];
4244
4245 let events = to_playback_events(
4246 &score,
4247 &PlaybackOptions {
4248 realization_profile: PlaybackRealizationProfile::OrnamentArpeggioV1,
4249 ..Default::default()
4250 },
4251 );
4252 assert_eq!(events.len(), 3);
4253 let by_pitch: BTreeMap<_, _> = events
4254 .iter()
4255 .map(|event| (event.pitch_midi, event.time_beats))
4256 .collect();
4257 assert!((by_pitch[&67] - 0.0).abs() < 1e-9);
4258 assert!((by_pitch[&64] - 0.09).abs() < 1e-9);
4259 assert!((by_pitch[&60] - 0.18).abs() < 1e-9);
4260 }
4261
4262 #[test]
4263 fn written_grace_steals_time_from_preceding_note() {
4264 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4265 let previous = Note::new(Pitch::new(Step::F, 4), Duration::Half);
4266 let mut grace = Note::new(Pitch::new(Step::B, 4), Duration::Eighth);
4267 grace.is_grace = true;
4268 let regular = Note::new(Pitch::new(Step::A, 4), Duration::Quarter);
4269 score.parts[0].staves[0].measures[0].voices[0] = vec![previous, grace, regular];
4270 let events = to_playback_events(&score, &opts(None));
4271 assert_eq!(events.len(), 3);
4272 assert_eq!(
4273 events
4274 .iter()
4275 .map(|event| event.pitch_midi)
4276 .collect::<Vec<_>>(),
4277 vec![65, 71, 69]
4278 );
4279 assert!((events[0].duration_beats - 1.875).abs() < 1e-9);
4280 assert!((events[1].time_beats - 1.875).abs() < 1e-9);
4281 assert!((events[1].duration_beats - 0.125).abs() < 1e-9);
4282 assert!((events[2].time_beats - 2.0).abs() < 1e-9);
4283 assert!((events[1].time_secs - 0.9375).abs() < 1e-9);
4284 }
4285
4286 #[test]
4287 fn opening_grace_borrows_from_main_note_without_moving_later_notes() {
4288 let mut score = Score::new("opening grace", 120, 4, 4, 0, 1);
4289 let mut grace = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
4290 grace.is_grace = true;
4291 let main = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4292 let next = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
4293 score.parts[0].staves[0].measures[0].voices[0] = vec![grace, main, next];
4294 let events = to_playback_events(&score, &opts(None));
4295 assert_eq!(events.len(), 3);
4296 assert_eq!(
4297 events
4298 .iter()
4299 .map(|event| event.pitch_midi)
4300 .collect::<Vec<_>>(),
4301 vec![62, 60, 64]
4302 );
4303 assert!((events[0].time_beats).abs() < 1e-9);
4304 assert!((events[0].duration_beats - 0.125).abs() < 1e-9);
4305 assert!((events[1].time_beats - 0.125).abs() < 1e-9);
4306 assert!((events[1].duration_beats - 0.875).abs() < 1e-9);
4307 assert!((events[2].time_beats - 1.0).abs() < 1e-9);
4308 }
4309
4310 #[test]
4311 fn consecutive_graces_are_ordered_and_cue_notes_remain_silent() {
4312 let mut score = Score::new("two graces", 120, 4, 4, 0, 1);
4313 let mut first_grace = Note::new(Pitch::new(Step::D, 4), Duration::Sixteenth);
4314 first_grace.is_grace = true;
4315 let mut second_grace = Note::new(Pitch::new(Step::E, 4), Duration::Sixteenth);
4316 second_grace.is_grace = true;
4317 let mut cue = Note::new(Pitch::new(Step::B, 4), Duration::Eighth);
4318 cue.is_cue = true;
4319 score.parts[0].staves[0].measures[0].voices[0] = vec![
4320 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
4321 first_grace,
4322 second_grace,
4323 Note::new(Pitch::new(Step::F, 4), Duration::Quarter),
4324 cue,
4325 ];
4326 let events = to_playback_events(&score, &opts(None));
4327 assert_eq!(
4328 events
4329 .iter()
4330 .map(|event| event.pitch_midi)
4331 .collect::<Vec<_>>(),
4332 vec![60, 62, 64, 65]
4333 );
4334 assert_eq!(
4335 events
4336 .iter()
4337 .map(|event| event.time_beats)
4338 .collect::<Vec<_>>(),
4339 vec![0.0, 0.875, 0.9375, 1.0]
4340 );
4341 assert!((events[0].duration_beats - 0.875).abs() < 1e-9);
4342 }
4343
4344 #[test]
4345 fn metronome_events_have_no_source_address() {
4346 let score = Score::new("T", 120, 4, 4, 0, 1);
4347 let options = PlaybackOptions {
4348 metronome: Some(MetronomeConfig::default()),
4349 ..Default::default()
4350 };
4351 let events = to_playback_events(&score, &options);
4352 assert!(!events.is_empty());
4353 assert!(events.iter().all(|event| event.address.is_none()));
4354 }
4355
4356 #[test]
4357 fn second_note_has_correct_time() {
4358 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4359 score.parts[0].staves[0].measures[0].voices[0] = vec![
4360 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
4361 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
4362 ];
4363 let events = to_playback_events(&score, &opts(None));
4364 assert_eq!(events.len(), 2);
4365 assert!((events[0].time_beats).abs() < 1e-9);
4366 assert!((events[1].time_beats - 1.0).abs() < 1e-9);
4367 }
4368
4369 #[test]
4370 fn sparse_voice_keeps_measure_boundaries() {
4371 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4372 let note = || Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4373 score.parts[0].staves[0].measures[0].voices[1] = vec![note()];
4374 score.parts[0].staves[0].measures[1].voices[1] = vec![note()];
4375
4376 let events = to_playback_events(&score, &opts(None));
4377 assert_eq!(events.len(), 2);
4378 assert!((events[0].time_beats).abs() < 1e-9);
4379 assert!((events[1].time_beats - 4.0).abs() < 1e-9);
4380 assert!((events[1].time_secs - 2.0).abs() < 1e-9);
4381 }
4382
4383 #[test]
4384 fn time_secs_120_bpm_quarter_note_is_half_second() {
4385 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4386 score.parts[0].staves[0].measures[0].voices[0] =
4387 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4388 let events = to_playback_events(&score, &opts(None));
4389 assert!((events[0].time_secs).abs() < 1e-9);
4390 assert!((events[0].duration_secs - 0.5).abs() < 1e-9);
4391 }
4392
4393 #[test]
4394 fn bpm_override_changes_time_secs() {
4395 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4396 score.parts[0].staves[0].measures[0].voices[0] = vec![
4397 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
4398 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
4399 ];
4400 let events = to_playback_events(&score, &opts(Some(60)));
4401 assert!((events[0].time_secs).abs() < 1e-9);
4402 assert!((events[1].time_secs - 1.0).abs() < 1e-9);
4403 }
4404
4405 #[test]
4406 fn tempo_multiplier_preserves_written_tempo_changes_and_ramps() {
4407 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4408 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
4409 score.parts[0].staves[0].measures[1].tempo = Some(90);
4410 score.parts[0].staves[0].measures[0].voices[0] =
4411 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4412 score.parts[0].staves[0].measures[1].voices[0] =
4413 vec![Note::new(Pitch::new(Step::D, 4), Duration::Quarter)];
4414 let options = PlaybackOptions {
4415 tempo_multiplier: 0.5,
4416 ..Default::default()
4417 };
4418
4419 let events = to_playback_events(&score, &options);
4420 let second_measure_start = tempo_ramp_seconds(60.0, Some(30.0), 4.0, 4.0);
4421 assert!((events[1].time_secs - second_measure_start).abs() < 1e-9);
4422
4423 let one_beat_into_ramp = tempo_ramp_seconds(60.0, Some(30.0), 4.0, 1.0);
4424 let position = compute_playback_position(&score, &options, one_beat_into_ramp).unwrap();
4425 assert_eq!(position.measure_index, 0);
4426 assert!((position.beat - 1.0).abs() < 1e-9);
4427
4428 let second_measure_position =
4429 compute_playback_position(&score, &options, second_measure_start + 60.0 / 45.0)
4430 .unwrap();
4431 assert_eq!(second_measure_position.measure_index, 1);
4432 assert!((second_measure_position.beat - 1.0).abs() < 1e-9);
4433 }
4434
4435 #[test]
4436 fn bpm_override_suppresses_written_tempo_changes_and_ramps() {
4437 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4438 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
4439 score.parts[0].staves[0].measures[1].tempo = Some(90);
4440 score.parts[0].staves[0].measures[0].voices[0] =
4441 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4442 score.parts[0].staves[0].measures[1].voices[0] =
4443 vec![Note::new(Pitch::new(Step::D, 4), Duration::Quarter)];
4444 let options = PlaybackOptions {
4445 bpm_override: Some(120),
4446 ..Default::default()
4447 };
4448
4449 let events = to_playback_events(&score, &options);
4450 assert!((events[1].time_secs - 2.0).abs() < 1e-9);
4451 let position = compute_playback_position(&score, &options, 2.5).unwrap();
4452 assert_eq!(position.measure_index, 1);
4453 assert!((position.beat - 1.0).abs() < 1e-9);
4454 }
4455
4456 #[test]
4457 fn playback_position_tracks_repeats_rests_and_meter_changes_with_a_tempo_multiplier() {
4458 use crate::model::notation::{Barline, TimeSignature};
4459
4460 let mut score = Score::new("transport", 120, 4, 4, 0, 3);
4461 let staff = &mut score.parts[0].staves[0];
4462 staff.measures[0].barline_left = Barline::RepeatStart;
4463 staff.measures[1].time_sig = Some(TimeSignature {
4464 numerator: 3,
4465 denominator: 4,
4466 });
4467 staff.measures[2].barline_right = Barline::RepeatEnd;
4468 staff.measures[0].voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4469 staff.measures[1].voices[0] = vec![Note::rest(Duration::Quarter)];
4470 staff.measures[2].voices[0] = vec![Note::new(Pitch::new(Step::D, 4), Duration::Quarter)];
4471 let options = PlaybackOptions {
4472 tempo_multiplier: 0.5,
4473 ..Default::default()
4474 };
4475
4476 let events = to_playback_events(&score, &options);
4477 let note_starts: Vec<f64> = events
4478 .iter()
4479 .filter(|event| event.address.is_some())
4480 .map(|event| event.time_secs)
4481 .collect();
4482 assert_eq!(note_starts, vec![0.0, 7.0, 10.0, 17.0]);
4484
4485 let middle = compute_playback_position(&score, &options, 5.0).unwrap();
4486 assert_eq!(middle.measure_index, 1);
4487 assert!((middle.beat - 1.0).abs() < 1e-9);
4488
4489 let repeated = compute_playback_position(&score, &options, 10.5).unwrap();
4490 assert_eq!(repeated.measure_index, 0);
4491 assert!((repeated.beat - 0.5).abs() < 1e-9);
4492 }
4493
4494 #[test]
4495 fn transpose_semitones_shifts_midi_output() {
4496 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4497 score.parts[0].staves[0].transpose_semitones = -2;
4498 score.parts[0].staves[0].measures[0].voices[0] =
4499 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4500 let events = to_playback_events(&score, &opts(None));
4501 assert_eq!(events.len(), 1);
4502 assert_eq!(events[0].pitch_midi, 58);
4503 }
4504
4505 #[test]
4506 fn percussion_channel_9_ignores_transpose_semitones() {
4507 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4508 score.parts[0].midi_channel = 9;
4509 score.parts[0].staves[0].transpose_semitones = -2;
4510 score.parts[0].staves[0].measures[0].voices[0] =
4511 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4512 let events = to_playback_events(&score, &opts(None));
4513 assert_eq!(events.len(), 1);
4514 assert_eq!(events[0].pitch_midi, 60);
4515 }
4516
4517 #[test]
4518 fn staccato_halves_duration_beats() {
4519 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4520 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4521 note.articulations
4522 .push(crate::model::notation::Articulation::Staccato);
4523 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4524 let events = to_playback_events(&score, &opts(None));
4525 assert_eq!(events.len(), 1);
4526 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
4527 }
4528
4529 #[test]
4530 fn staccatissimo_also_halves_duration() {
4531 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4532 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4533 note.articulations
4534 .push(crate::model::notation::Articulation::Staccatissimo);
4535 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4536 let events = to_playback_events(&score, &opts(None));
4537 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
4538 }
4539
4540 #[test]
4541 fn staccato_does_not_shift_next_note_time() {
4542 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4543 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4544 n1.articulations
4545 .push(crate::model::notation::Articulation::Staccato);
4546 let n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
4547 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
4548 let events = to_playback_events(&score, &opts(None));
4549 assert_eq!(events.len(), 2);
4550 assert!((events[1].time_beats - 1.0).abs() < 1e-9);
4551 }
4552
4553 #[test]
4554 fn dynamics_hold_until_the_next_marking_across_voices_and_bars() {
4555 use crate::model::notation::Dynamic;
4556 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4557 let quarters = |dynamics: [Option<Dynamic>; 4]| -> Vec<Note> {
4558 dynamics
4559 .into_iter()
4560 .map(|dynamic| {
4561 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4562 note.dynamic = dynamic;
4563 note
4564 })
4565 .collect()
4566 };
4567 score.parts[0].staves[0].measures[0].voices[0] =
4570 quarters([Some(Dynamic::P), None, Some(Dynamic::Sf), None]);
4571 score.parts[0].staves[0].measures[0].voices[1] = vec![
4572 Note::new(Pitch::new(Step::C, 3), Duration::Half),
4573 Note::new(Pitch::new(Step::C, 3), Duration::Half),
4574 ];
4575 score.parts[0].staves[0].measures[1].voices[0] =
4576 quarters([Some(Dynamic::Fp), None, Some(Dynamic::Pf), None]);
4577 let events = to_playback_events(&score, &opts(None));
4578 let velocities = |pitch: u8| -> Vec<u8> {
4579 events
4580 .iter()
4581 .filter(|event| event.pitch_midi == pitch)
4582 .map(|event| event.velocity)
4583 .collect()
4584 };
4585 let (p, f, sf) = (
4586 Dynamic::P.to_velocity(),
4587 Dynamic::F.to_velocity(),
4588 Dynamic::Sf.to_velocity(),
4589 );
4590 assert_eq!(velocities(60), vec![p, p, sf, p, f, p, p, f]);
4591 assert_eq!(velocities(48), vec![p, sf]);
4592 assert_eq!(
4594 DynamicTimeline::default().velocity(0, 0.0, None),
4595 DynamicTimeline::DEFAULT_VELOCITY
4596 );
4597 }
4598
4599 #[test]
4600 fn hairpins_ramp_towards_the_following_marking_or_two_steps() {
4601 use crate::model::notation::{Dynamic, HairpinKind};
4602 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4603 let mut bar = |index: usize, dynamics: [Option<Dynamic>; 4], end: Option<Dynamic>| {
4604 let mut notes: Vec<Note> = dynamics
4605 .into_iter()
4606 .map(|dynamic| {
4607 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4608 note.dynamic = dynamic;
4609 note
4610 })
4611 .collect();
4612 notes[0].hairpin_start = Some(HairpinKind::Crescendo);
4613 notes[2].hairpin_end = true;
4614 notes[3].dynamic = end;
4615 score.parts[0].staves[0].measures[index].voices[0] = notes;
4616 };
4617 bar(0, [Some(Dynamic::P), None, None, None], Some(Dynamic::F));
4619 bar(1, [None, None, None, None], None);
4620 let events = to_playback_events(&score, &opts(None));
4621 let velocities: Vec<u8> = events.iter().map(|event| event.velocity).collect();
4622 assert_eq!(&velocities[..4], &[48, 60, 72, 84]);
4623 assert_eq!(&velocities[4..], &[84, 92, 100, 108]);
4624 }
4625
4626 #[test]
4627 fn explicit_dynamic_at_hairpin_end_overrides_terminal_level_in_all_voices() {
4628 use crate::{Dynamic, HairpinKind};
4629 for (hairpin, dynamic, attack, sustain) in [
4630 (HairpinKind::Crescendo, Dynamic::P, 48, 48),
4631 (HairpinKind::Decrescendo, Dynamic::F, 84, 84),
4632 (
4633 HairpinKind::Decrescendo,
4634 Dynamic::Sfz,
4635 Dynamic::Sfz.to_velocity(),
4636 40,
4637 ),
4638 (
4639 HairpinKind::Crescendo,
4640 Dynamic::Pf,
4641 Dynamic::Pf.to_velocity(),
4642 84,
4643 ),
4644 ] {
4645 let mut score = Score::new("endpoint", 120, 4, 4, 0, 2);
4646 let mut first = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
4647 first.hairpin_start = Some(hairpin);
4648 first.hairpin_end = true;
4649 score.parts[0].staves[0].measures[0].voices[0] = vec![first];
4650 let mut upper = Note::new(Pitch::new(Step::C, 5), Duration::Half);
4651 upper.dynamic = Some(dynamic);
4652 score.parts[0].staves[0].measures[1].voices[0] =
4653 vec![upper, Note::new(Pitch::new(Step::D, 5), Duration::Half)];
4654 score.parts[0].staves[0].measures[1].voices[1] = vec![
4655 Note::new(Pitch::new(Step::E, 4), Duration::Half),
4656 Note::new(Pitch::new(Step::F, 4), Duration::Half),
4657 ];
4658 let events = to_playback_events(&score, &opts(None));
4659 let at_start: Vec<_> = events
4660 .iter()
4661 .filter(|e| (e.time_beats - 4.0).abs() < 1e-9)
4662 .map(|e| e.velocity)
4663 .collect();
4664 let later: Vec<_> = events
4665 .iter()
4666 .filter(|e| (e.time_beats - 6.0).abs() < 1e-9)
4667 .map(|e| e.velocity)
4668 .collect();
4669 assert_eq!(at_start, vec![attack, attack]);
4670 assert_eq!(later, vec![sustain, sustain]);
4671 }
4672 }
4673
4674 #[test]
4675 fn dynamic_names_round_trip_and_compounds_continue_at_their_second_level() {
4676 use crate::model::notation::Dynamic;
4677 for dynamic in Dynamic::ALL {
4678 assert_eq!(
4679 Dynamic::from_musicxml_str(dynamic.to_musicxml_str()),
4680 Some(dynamic)
4681 );
4682 }
4683 assert_eq!(Dynamic::from_musicxml_str("fffff"), Some(Dynamic::Ffff));
4684 assert_eq!(Dynamic::Sfpp.sustained_level(), Some(Dynamic::Pp));
4685 assert_eq!(Dynamic::Sfz.sustained_level(), None);
4686 assert_eq!(Dynamic::from_musicxml_str("other"), None);
4687 }
4688
4689 #[test]
4690 fn accent_boosts_velocity_clamped() {
4691 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4692 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4693 note.articulations
4694 .push(crate::model::notation::Articulation::Accent);
4695 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4696 let events = to_playback_events(&score, &opts(None));
4697 assert_eq!(events[0].velocity, 84);
4698 }
4699
4700 #[test]
4701 fn accent_clamped_at_127() {
4702 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4703 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4704 note.dynamic = Some(crate::model::notation::Dynamic::Ffff);
4705 note.articulations
4706 .push(crate::model::notation::Articulation::Accent);
4707 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4708 let events = to_playback_events(&score, &opts(None));
4709 assert_eq!(events[0].velocity, 127);
4710 }
4711
4712 #[test]
4713 fn tenuto_keeps_full_duration() {
4714 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4715 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4716 note.articulations
4717 .push(crate::model::notation::Articulation::Tenuto);
4718 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4719 let events = to_playback_events(&score, &opts(None));
4720 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
4721 }
4722
4723 #[test]
4724 fn pedal_start_sets_pedal_field() {
4725 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4726 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4727 note.pedal_start = true;
4728 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4729 let events = to_playback_events(&score, &opts(None));
4730 assert!(events[0].pedal);
4731 }
4732
4733 #[test]
4734 fn no_pedal_start_pedal_is_false() {
4735 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4736 score.parts[0].staves[0].measures[0].voices[0] =
4737 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4738 let events = to_playback_events(&score, &opts(None));
4739 assert!(!events[0].pedal);
4740 }
4741
4742 #[test]
4743 fn set_tempo_at_measure_changes_time_secs() {
4744 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4745 score.parts[0].staves[0].measures[0].voices[0] =
4746 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4747 score.parts[0].staves[0].measures[1].tempo = Some(60);
4748 score.parts[0].staves[0].measures[1].voices[0] =
4749 vec![Note::new(Pitch::new(Step::D, 4), Duration::Whole)];
4750 let events = to_playback_events(&score, &opts(None));
4751 assert_eq!(events.len(), 2);
4752 assert!((events[0].time_secs).abs() < 1e-9);
4753 assert!((events[0].duration_secs - 2.0).abs() < 1e-9);
4754 assert!((events[1].time_secs - 2.0).abs() < 1e-9);
4755 assert!((events[1].duration_secs - 4.0).abs() < 1e-9);
4756 }
4757
4758 #[test]
4759 fn muted_part_produces_no_events() {
4760 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4761 score.parts[0].staves[0].measures[0].voices[0] =
4762 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4763 let options = PlaybackOptions {
4764 muted_parts: vec![0],
4765 ..Default::default()
4766 };
4767 assert!(to_playback_events(&score, &options).is_empty());
4768 }
4769
4770 #[test]
4771 fn part_index_field_set_correctly() {
4772 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4773 score.parts[0].staves[0].measures[0].voices[0] =
4774 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4775 let events = to_playback_events(&score, &opts(None));
4776 assert_eq!(events[0].part_index, 0);
4777 }
4778
4779 #[test]
4782 fn loop_region_filters_measures() {
4783 let mut score = Score::new("T", 120, 4, 4, 0, 3);
4785 for mi in 0..3 {
4786 score.parts[0].staves[0].measures[mi].voices[0] =
4787 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4788 }
4789 let options = PlaybackOptions {
4790 loop_region: Some((1, 2)),
4791 ..Default::default()
4792 };
4793 let events = to_playback_events(&score, &options);
4794 assert_eq!(events.len(), 2);
4795 assert!((events[0].time_beats).abs() < 1e-9);
4797 }
4798
4799 #[test]
4800 fn loop_region_none_plays_all_measures() {
4801 let mut score = Score::new("T", 120, 4, 4, 0, 3);
4802 for mi in 0..3 {
4803 score.parts[0].staves[0].measures[mi].voices[0] =
4804 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
4805 }
4806 let events = to_playback_events(&score, &opts(None));
4807 assert_eq!(events.len(), 3);
4808 }
4809
4810 #[test]
4813 fn fermata_holds_a_shared_clock_across_voices_and_repeat_traversal() {
4814 use crate::model::notation::Barline;
4815 let mut score = Score::new("Shared fermata", 120, 3, 4, 0, 1);
4816 let measure = &mut score.parts[0].staves[0].measures[0];
4817 measure.barline_left = Barline::RepeatStart;
4818 measure.barline_right = Barline::RepeatEnd;
4819 let mut held = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4820 held.articulations.push(Articulation::Fermata);
4821 measure.voices[0] = vec![
4822 held,
4823 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
4824 Note::new(Pitch::new(Step::E, 4), Duration::Quarter),
4825 ];
4826 measure.voices[1] = vec![
4827 Note::new(Pitch::new(Step::C, 3), Duration::Half),
4828 Note::new(Pitch::new(Step::G, 3), Duration::Quarter),
4829 ];
4830 let events = to_playback_events(&score, &PlaybackOptions::default());
4831 let onsets = |pitch| {
4832 events
4833 .iter()
4834 .filter(|event| event.pitch_midi == pitch)
4835 .map(|event| event.time_beats)
4836 .collect::<Vec<_>>()
4837 };
4838 assert_eq!(onsets(60), vec![0.0, 3.5]);
4839 assert_eq!(onsets(62), vec![1.5, 5.0]);
4840 assert_eq!(onsets(64), vec![2.5, 6.0]);
4841 assert_eq!(onsets(55), vec![2.5, 6.0]);
4842 let bass = events.iter().find(|event| event.pitch_midi == 48).unwrap();
4843 assert_eq!(bass.duration_beats, 2.5);
4844 assert_eq!(bass.duration_secs, 1.25);
4845 assert!((super::super::score::score_duration_secs(&score) - 3.5).abs() < 1e-9);
4846 assert!(
4847 (super::super::score::score_duration_secs_region(&score, (0, 0)) - 3.5).abs() < 1e-9
4848 );
4849 }
4850
4851 #[test]
4852 fn simultaneous_fermatas_hold_once_and_delay_rest_pedal_and_position() {
4853 let mut score = Score::new("Two staves", 120, 3, 4, 0, 1);
4854 let mut held = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4855 held.articulations.push(Articulation::Fermata);
4856 score.parts[0].staves[0].measures[0].voices[0] = vec![
4857 held.clone(),
4858 Note::new(Pitch::new(Step::D, 4), Duration::Half),
4859 ];
4860 let mut lower = score.parts[0].staves[0].clone();
4861 held.pitches[0] = Pitch::new(Step::C, 3);
4862 let mut rest = Note::rest(Duration::Quarter);
4863 rest.pedal_start = true;
4864 lower.measures[0].voices[0] = vec![
4865 held,
4866 rest,
4867 Note::new(Pitch::new(Step::E, 3), Duration::Quarter),
4868 ];
4869 lower.measures[0].voices[1] = vec![
4870 Note::new(Pitch::new(Step::C, 2), Duration::Half),
4871 Note::rest(Duration::Quarter),
4872 ];
4873 score.parts[0].staves.push(lower);
4874 let opts = PlaybackOptions::default();
4875 let events = to_playback_events(&score, &opts);
4876 assert_eq!(
4877 events
4878 .iter()
4879 .find(|event| event.pitch_midi == 62)
4880 .unwrap()
4881 .time_beats,
4882 1.5
4883 );
4884 assert_eq!(
4885 events
4886 .iter()
4887 .find(|event| event.pitch_midi == 36)
4888 .unwrap()
4889 .duration_beats,
4890 2.5
4891 );
4892 let manifest = build_offline_render_manifest(
4893 &score,
4894 &opts,
4895 &OfflineRenderRequest {
4896 sample_rate_hz: 48_000,
4897 ..Default::default()
4898 },
4899 )
4900 .unwrap();
4901 assert!(manifest.semantic_events.iter().any(|event| matches!(
4902 event.kind,
4903 OfflineRenderSemanticEventKind::Pedal { down: true }
4904 ) && event.frame == 36_000));
4905 let position = compute_playback_position(&score, &opts, 0.625).unwrap();
4906 assert!((position.beat - 1.0).abs() < 1e-9);
4907 let position = compute_playback_position(&score, &opts, 1.0).unwrap();
4908 assert!((position.beat - 1.5).abs() < 1e-9);
4909 }
4910
4911 #[test]
4912 fn fermata_multiplier_extends_duration() {
4913 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4914 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4915 note.articulations
4916 .push(crate::model::notation::Articulation::Fermata);
4917 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4918 let options = PlaybackOptions {
4919 fermata_multiplier: 2.0,
4920 ..Default::default()
4921 };
4922 let events = to_playback_events(&score, &options);
4923 assert_eq!(events.len(), 1);
4924 assert!((events[0].duration_beats - 2.0).abs() < 1e-9);
4925 }
4926
4927 #[test]
4928 fn fermata_default_multiplier_is_1_5() {
4929 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4930 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4931 note.articulations
4932 .push(crate::model::notation::Articulation::Fermata);
4933 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4934 let events = to_playback_events(&score, &PlaybackOptions::default());
4935 assert_eq!(events.len(), 1);
4936 assert!((events[0].duration_beats - 1.5).abs() < 1e-9);
4937 }
4938
4939 #[test]
4940 fn fermata_hold_is_an_explicit_post_note_request_separate_from_extension() {
4941 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4942 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4943 note.articulations
4944 .push(crate::model::notation::Articulation::Fermata);
4945 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4946 let options = PlaybackOptions {
4947 fermata_multiplier: 2.0,
4948 fermata_hold_beats: 0.75,
4949 ..Default::default()
4950 };
4951 let events = to_playback_events(&score, &options);
4952 assert!((events[0].duration_beats - 2.0).abs() < 1e-9);
4953 assert!((events[0].post_note_pause_beats - 0.75).abs() < 1e-9);
4954 }
4955
4956 #[test]
4957 fn invalid_fermata_hold_is_safely_ignored() {
4958 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4959 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4960 note.articulations
4961 .push(crate::model::notation::Articulation::Fermata);
4962 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
4963 let options = PlaybackOptions {
4964 fermata_hold_beats: f64::NAN,
4965 ..Default::default()
4966 };
4967 assert_eq!(
4968 to_playback_events(&score, &options)[0].post_note_pause_beats,
4969 0.0
4970 );
4971 }
4972
4973 #[test]
4974 fn non_fermata_note_unaffected_by_fermata_multiplier() {
4975 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4976 score.parts[0].staves[0].measures[0].voices[0] =
4977 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4978 let options = PlaybackOptions {
4979 fermata_multiplier: 3.0,
4980 ..Default::default()
4981 };
4982 let events = to_playback_events(&score, &options);
4983 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
4984 }
4985
4986 #[test]
4989 fn swing_triplet_first_eighth_is_long() {
4990 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4992 score.parts[0].staves[0].measures[0].voices[0] = vec![
4993 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
4994 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
4995 ];
4996 let options = PlaybackOptions {
4997 swing: Some(0.67),
4998 ..Default::default()
4999 };
5000 let events = to_playback_events(&score, &options);
5001 let e_c = events.iter().find(|e| e.pitch_midi == 60).unwrap();
5003 let e_d = events.iter().find(|e| e.pitch_midi == 62).unwrap();
5004 assert!(
5005 (e_c.duration_beats - 0.67).abs() < 1e-9,
5006 "first eighth should be 0.67"
5007 );
5008 assert!(
5009 (e_d.duration_beats - 0.33).abs() < 1e-9,
5010 "second eighth should be 0.33"
5011 );
5012 assert!(
5014 (e_d.time_beats - 0.67).abs() < 1e-9,
5015 "second note start should be at 0.67"
5016 );
5017 }
5018
5019 #[test]
5020 fn swing_does_not_stretch_an_unpaired_eighth() {
5021 let mut score = Score::new("unpaired", 120, 4, 4, 0, 1);
5022 score.parts[0].staves[0].measures[0].voices[0] = vec![
5023 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
5024 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
5025 ];
5026 let events = to_playback_events(
5027 &score,
5028 &PlaybackOptions {
5029 swing: Some(2.0 / 3.0),
5030 ..Default::default()
5031 },
5032 );
5033 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
5034 assert!((events[1].time_beats - 0.5).abs() < 1e-9);
5035 }
5036
5037 #[test]
5038 fn cue_note_does_not_sound_or_pair_with_a_written_eighth() {
5039 let mut score = Score::new("cue", 120, 4, 4, 0, 1);
5040 let mut cue = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
5041 cue.is_cue = true;
5042 score.parts[0].staves[0].measures[0].voices[0] = vec![
5043 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
5044 cue,
5045 Note::new(Pitch::new(Step::E, 4), Duration::Eighth),
5046 ];
5047 let events = to_playback_events(
5048 &score,
5049 &PlaybackOptions {
5050 swing: Some(2.0 / 3.0),
5051 ..Default::default()
5052 },
5053 );
5054 assert_eq!(events.len(), 2);
5055 assert_eq!(events[0].pitch_midi, 60);
5056 assert_eq!(events[0].duration_beats, 0.5);
5057 assert_eq!(events[1].pitch_midi, 64);
5058 assert_eq!(events[1].time_beats, 0.5);
5059 }
5060
5061 #[test]
5062 fn swing_requires_metric_alignment_and_a_matching_next_note() {
5063 let mut score = Score::new("metric boundary", 120, 4, 4, 0, 1);
5064 score.parts[0].staves[0].measures[0].voices[0] = vec![
5065 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
5066 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
5067 Note::new(Pitch::new(Step::E, 4), Duration::Eighth),
5068 Note::new(Pitch::new(Step::F, 4), Duration::Eighth),
5069 Note::new(Pitch::new(Step::G, 4), Duration::Eighth),
5070 Note::new(Pitch::new(Step::A, 4), Duration::Eighth),
5071 ];
5072 let events = to_playback_events(
5073 &score,
5074 &PlaybackOptions {
5075 swing: Some(2.0 / 3.0),
5076 ..Default::default()
5077 },
5078 );
5079 assert!((events[0].time_beats).abs() < 1e-9);
5080 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
5081 assert!((events[2].time_beats - 1.5).abs() < 1e-9);
5082 assert!((events[2].duration_beats - 0.5).abs() < 1e-9);
5083 assert!((events[3].time_beats - 2.0).abs() < 1e-9);
5084 assert!((events[3].duration_beats - 2.0 / 3.0).abs() < 1e-9);
5085 assert!((events[4].time_beats - 8.0 / 3.0).abs() < 1e-9);
5086 assert!((events[4].duration_beats - 1.0 / 3.0).abs() < 1e-9);
5087 assert!((events[5].time_beats - 3.0).abs() < 1e-9);
5088 assert!((events[5].duration_beats - 0.5).abs() < 1e-9);
5089 }
5090
5091 #[test]
5092 fn swing_keeps_the_offbeat_delay_after_an_eighth_rest() {
5093 let mut score = Score::new("rest swing", 120, 4, 4, 0, 1);
5094 score.parts[0].staves[0].measures[0].voices[0] = vec![
5095 Note::rest(Duration::Eighth),
5096 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
5097 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
5098 ];
5099 let events = to_playback_events(
5100 &score,
5101 &PlaybackOptions {
5102 swing: Some(2.0 / 3.0),
5103 ..Default::default()
5104 },
5105 );
5106 assert!((events[0].time_beats - 2.0 / 3.0).abs() < 1e-9);
5107 assert!((events[0].duration_beats - 1.0 / 3.0).abs() < 1e-9);
5108 assert!((events[1].time_beats - 1.0).abs() < 1e-9);
5109 }
5110
5111 #[test]
5112 fn invalid_swing_ratios_keep_straight_finite_timing() {
5113 let mut score = Score::new("invalid swing", 120, 4, 4, 0, 1);
5114 score.parts[0].staves[0].measures[0].voices[0] = vec![
5115 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
5116 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
5117 ];
5118 for ratio in [f64::NAN, f64::INFINITY, -1.0, 0.5, 1.0] {
5119 let events = to_playback_events(
5120 &score,
5121 &PlaybackOptions {
5122 swing: Some(ratio),
5123 ..Default::default()
5124 },
5125 );
5126 assert_eq!(events.len(), 2);
5127 assert_eq!(events[0].duration_beats, 0.5);
5128 assert_eq!(events[1].time_beats, 0.5);
5129 assert_eq!(events[1].duration_beats, 0.5);
5130 assert!(events.iter().all(|event| event.time_secs.is_finite()));
5131 }
5132 }
5133
5134 #[test]
5135 fn pickup_swing_uses_metric_position_in_the_full_bar() {
5136 let mut score = Score::new("pickup swing", 120, 4, 4, 0, 1);
5137 let measure = &mut score.parts[0].staves[0].measures[0];
5138 measure.actual_length = Some(crate::MeasureLength {
5139 numerator: 3,
5140 denominator: 8,
5141 });
5142 measure.voices[0] = vec![
5143 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
5144 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
5145 Note::new(Pitch::new(Step::E, 4), Duration::Eighth),
5146 ];
5147 let events = to_playback_events(
5148 &score,
5149 &PlaybackOptions {
5150 swing: Some(2.0 / 3.0),
5151 ..Default::default()
5152 },
5153 );
5154 assert_eq!(events[0].duration_beats, 0.5);
5155 assert!((events[1].time_beats - 0.5).abs() < 1e-9);
5156 assert!((events[1].duration_beats - 2.0 / 3.0).abs() < 1e-9);
5157 assert!((events[2].time_beats - 7.0 / 6.0).abs() < 1e-9);
5158 assert!((events[2].duration_beats - 1.0 / 3.0).abs() < 1e-9);
5159 }
5160
5161 #[test]
5162 fn swing_non_eighth_not_affected() {
5163 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5164 score.parts[0].staves[0].measures[0].voices[0] =
5165 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5166 let options = PlaybackOptions {
5167 swing: Some(0.67),
5168 ..Default::default()
5169 };
5170 let events = to_playback_events(&score, &options);
5171 assert!(
5172 (events[0].duration_beats - 1.0).abs() < 1e-9,
5173 "quarter note unaffected"
5174 );
5175 }
5176
5177 #[test]
5178 fn swing_none_is_straight() {
5179 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5180 score.parts[0].staves[0].measures[0].voices[0] =
5181 vec![Note::new(Pitch::new(Step::C, 4), Duration::Eighth)];
5182 let options = PlaybackOptions {
5183 swing: None,
5184 ..Default::default()
5185 };
5186 let events = to_playback_events(&score, &options);
5187 assert!(
5188 (events[0].duration_beats - 0.5).abs() < 1e-9,
5189 "no swing = straight eighth"
5190 );
5191 }
5192
5193 #[test]
5194 fn channel_matches_part_midi_channel() {
5195 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5196 score.parts[0].midi_channel = 3;
5197 score.parts[0].staves[0].measures[0].voices[0] =
5198 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5199 let events = to_playback_events(&score, &opts(None));
5200 assert_eq!(events[0].channel, 3);
5201 }
5202
5203 #[test]
5204 fn swing_unit_default_is_eighth() {
5205 assert_eq!(PlaybackOptions::default().swing_unit, Duration::Eighth);
5206 }
5207
5208 #[test]
5209 fn swing_unit_sixteenth() {
5210 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5211 score.parts[0].staves[0].measures[0].voices[0] = vec![
5212 Note::new(Pitch::new(Step::C, 4), Duration::Sixteenth),
5213 Note::new(Pitch::new(Step::D, 4), Duration::Sixteenth),
5214 ];
5215 let options = PlaybackOptions {
5216 swing: Some(0.67),
5217 swing_unit: Duration::Sixteenth,
5218 ..Default::default()
5219 };
5220 let events = to_playback_events(&score, &options);
5221 let e_c = events.iter().find(|e| e.pitch_midi == 60).unwrap();
5222 let e_d = events.iter().find(|e| e.pitch_midi == 62).unwrap();
5223 assert!(
5224 (e_c.duration_beats - 0.335).abs() < 1e-9,
5225 "first 16th should be 0.335"
5226 );
5227 assert!(
5228 (e_d.duration_beats - 0.165).abs() < 1e-9,
5229 "second 16th should be 0.165"
5230 );
5231 }
5232
5233 #[test]
5234 fn swing_resets_per_measure() {
5235 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5237 let pair = || {
5238 vec![
5239 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
5240 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
5241 ]
5242 };
5243 score.parts[0].staves[0].measures[0].voices[0] = pair();
5244 score.parts[0].staves[0].measures[1].voices[0] = pair();
5245 let options = PlaybackOptions {
5246 swing: Some(0.67),
5247 ..Default::default()
5248 };
5249 let events = to_playback_events(&score, &options);
5250 let durations: Vec<f64> = events.iter().map(|e| e.duration_beats).collect();
5252 assert!((durations[0] - 0.67).abs() < 1e-9, "m0 first note long");
5254 assert!((durations[1] - 0.33).abs() < 1e-9, "m0 second note short");
5256 assert!(
5258 (durations[2] - 0.67).abs() < 1e-9,
5259 "m1 first note long (reset)"
5260 );
5261 assert!((durations[3] - 0.33).abs() < 1e-9, "m1 second note short");
5263 }
5264
5265 #[test]
5266 fn multi_voice_events_preserve_source_voice_addresses() {
5267 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5268 score.parts[0].staves[0].measures[0].voices[0] =
5269 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5270 score.parts[0].staves[0].measures[0].voices[1] =
5271 vec![Note::new(Pitch::new(Step::E, 4), Duration::Quarter)];
5272
5273 let events = to_playback_events(&score, &PlaybackOptions::default());
5274 let addresses: Vec<&str> = events
5275 .iter()
5276 .filter_map(|event| event.address.as_deref())
5277 .collect();
5278 assert!(addresses.contains(&"0:0:0:0:0"));
5279 assert!(addresses.contains(&"0:0:0:1:0"));
5280 }
5281
5282 #[test]
5283 fn playback_exposes_original_musicxml_voice_number_alongside_slot_address() {
5284 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5285 let measure = &mut score.parts[0].staves[0].measures[0];
5286 measure.voices[1] = vec![Note::new(Pitch::new(Step::E, 4), Duration::Quarter)];
5287 measure.source_voice_numbers[1] = Some(5);
5288
5289 let events = to_playback_events(&score, &PlaybackOptions::default());
5290 let event = events
5291 .iter()
5292 .find(|event| event.address.as_deref() == Some("0:0:0:1:0"))
5293 .expect("event from slot 1");
5294 assert_eq!(event.source.as_ref().map(|source| source.voice), Some(1));
5295 assert_eq!(event.source_voice_number, Some(5));
5296 }
5297
5298 #[test]
5301 fn playback_position_at_zero_is_measure_0_beat_0() {
5302 let score = Score::new("T", 120, 4, 4, 0, 4);
5303 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 0.0).unwrap();
5304 assert_eq!(pos.measure_index, 0);
5305 assert!(pos.beat.abs() < 1e-9);
5306 }
5307
5308 #[test]
5309 fn playback_position_at_half_measure_is_beat_2() {
5310 let score = Score::new("T", 120, 4, 4, 0, 4);
5312 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 0.5).unwrap();
5313 assert_eq!(pos.measure_index, 0);
5314 assert!(
5315 (pos.beat - 1.0).abs() < 1e-9,
5316 "expected beat 1.0, got {}",
5317 pos.beat
5318 );
5319 }
5320
5321 #[test]
5322 fn playback_position_beyond_score_is_none() {
5323 let score = Score::new("T", 120, 4, 4, 0, 1);
5325 assert!(compute_playback_position(&score, &PlaybackOptions::default(), 10.0).is_none());
5326 }
5327
5328 #[test]
5329 fn playback_position_tempo_change_takes_effect() {
5330 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5333 score.parts[0].staves[0].measures[1].tempo = Some(60);
5334 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 2.5).unwrap();
5335 assert_eq!(pos.measure_index, 1);
5336 assert!(
5337 (pos.beat - 0.5).abs() < 1e-9,
5338 "expected beat 0.5, got {}",
5339 pos.beat
5340 );
5341 }
5342
5343 #[test]
5344 fn playback_position_loop_region_starts_at_zero() {
5345 let score = Score::new("T", 120, 4, 4, 0, 4);
5347 let options = PlaybackOptions {
5348 loop_region: Some((1, 2)),
5349 ..Default::default()
5350 };
5351 let pos = compute_playback_position(&score, &options, 0.0).unwrap();
5352 assert_eq!(pos.measure_index, 1);
5353 assert!(pos.beat.abs() < 1e-9);
5354 }
5355
5356 #[test]
5359 fn metronome_injects_beat_events() {
5360 let score = Score::new("T", 120, 4, 4, 0, 1);
5362 let options = PlaybackOptions {
5363 metronome: Some(MetronomeConfig::default()),
5364 ..Default::default()
5365 };
5366 let events = to_playback_events(&score, &options);
5367 let metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
5368 assert_eq!(metro_events.len(), 4, "expected 4 metronome clicks in 4/4");
5369 }
5370
5371 #[test]
5372 fn metronome_accent_is_first_beat() {
5373 let score = Score::new("T", 120, 4, 4, 0, 1);
5374 let metro = MetronomeConfig::default();
5375 let options = PlaybackOptions {
5376 metronome: Some(metro.clone()),
5377 ..Default::default()
5378 };
5379 let events = to_playback_events(&score, &options);
5380 let mut metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
5381 metro_events.sort_by(|a, b| a.time_beats.partial_cmp(&b.time_beats).unwrap());
5382 assert_eq!(metro_events[0].pitch_midi, metro.accent_pitch);
5383 assert_eq!(metro_events[0].velocity, metro.accent_velocity);
5384 }
5385
5386 #[test]
5387 fn metronome_regular_beat_pitch() {
5388 let score = Score::new("T", 120, 4, 4, 0, 1);
5389 let metro = MetronomeConfig::default();
5390 let options = PlaybackOptions {
5391 metronome: Some(metro.clone()),
5392 ..Default::default()
5393 };
5394 let events = to_playback_events(&score, &options);
5395 let mut metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
5396 metro_events.sort_by(|a, b| a.time_beats.partial_cmp(&b.time_beats).unwrap());
5397 for ev in &metro_events[1..] {
5398 assert_eq!(ev.pitch_midi, metro.beat_pitch);
5399 assert_eq!(ev.velocity, metro.beat_velocity);
5400 }
5401 }
5402
5403 #[test]
5404 fn metronome_integrates_measure_tempo_ramp_and_carries_ending_tempo() {
5405 let mut score = Score::new("T", 120, 4, 4, 0, 2);
5406 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
5407 let options = PlaybackOptions {
5408 metronome: Some(MetronomeConfig::default()),
5409 ..Default::default()
5410 };
5411
5412 let mut clicks: Vec<_> = to_playback_events(&score, &options)
5413 .into_iter()
5414 .filter(|event| event.is_metronome)
5415 .collect();
5416 clicks.sort_by(|left, right| left.time_beats.total_cmp(&right.time_beats));
5417
5418 let expected_second_beat = tempo_ramp_seconds(120.0, Some(60.0), 4.0, 1.0);
5419 let expected_second_measure = tempo_ramp_seconds(120.0, Some(60.0), 4.0, 4.0);
5420 assert!((clicks[1].time_secs - expected_second_beat).abs() < 1e-9);
5421 assert!((clicks[4].time_secs - expected_second_measure).abs() < 1e-9);
5422 assert!((clicks[5].time_secs - (expected_second_measure + 1.0)).abs() < 1e-9);
5423 }
5424
5425 #[test]
5426 fn metronome_events_are_marked() {
5427 let score = Score::new("T", 120, 4, 4, 0, 1);
5428 let options = PlaybackOptions {
5429 metronome: Some(MetronomeConfig::default()),
5430 ..Default::default()
5431 };
5432 let events = to_playback_events(&score, &options);
5433 assert!(events.iter().any(|e| e.is_metronome));
5434 }
5435
5436 #[test]
5437 fn metronome_none_produces_no_extra_events() {
5438 let score = Score::new("T", 120, 4, 4, 0, 1);
5439 let events = to_playback_events(&score, &PlaybackOptions::default());
5440 assert!(events.iter().all(|e| !e.is_metronome));
5441 }
5442
5443 #[test]
5444 fn offline_render_manifest_uses_event_end_as_exact_frame_length() {
5445 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5446 score.parts[0].staves[0].measures[0].voices[0][0] =
5447 Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5448 let request = OfflineRenderRequest {
5449 format: OfflineRenderFormat::Flac,
5450 sample_rate_hz: 48_000,
5451 channels: 2,
5452 provider_identity: Some("test-provider@1".into()),
5453 ..Default::default()
5454 };
5455
5456 let manifest = build_offline_render_manifest(&score, &PlaybackOptions::default(), &request)
5457 .expect("valid render manifest");
5458 assert_eq!(manifest.contract_version, OFFLINE_RENDER_CONTRACT_VERSION);
5459 assert!(manifest.validate().is_ok());
5460 assert_eq!(manifest.format, OfflineRenderFormat::Flac);
5461 assert_eq!(manifest.duration_frames, 24_000);
5462 assert_eq!(manifest.events.len(), 1);
5463 assert_eq!(manifest.frame_events.len(), 1);
5464 assert_eq!(manifest.frame_events[0].start_frame, 0);
5465 assert_eq!(manifest.frame_events[0].duration_frames, 24_000);
5466 assert_eq!(manifest.frame_events[0].end_frame, 24_000);
5467 assert_eq!(manifest.playback_options, PlaybackOptions::default());
5468 assert_eq!(
5469 manifest.provider_identity.as_deref(),
5470 Some("test-provider@1")
5471 );
5472 }
5473
5474 #[test]
5475 fn offline_render_manifest_rejects_out_of_range_audio_shape() {
5476 let score = Score::new("T", 120, 4, 4, 0, 1);
5477 let request = OfflineRenderRequest {
5478 sample_rate_hz: 0,
5479 ..Default::default()
5480 };
5481 assert!(matches!(
5482 build_offline_render_manifest(&score, &PlaybackOptions::default(), &request),
5483 Err(crate::Error::InvalidOfflineRenderRequest)
5484 ));
5485 }
5486
5487 #[test]
5488 fn offline_render_manifest_selection_keeps_source_address_and_release_tail() {
5489 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5490 score.parts[0].staves[0].measures[0].voices[0] = vec![
5491 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
5492 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
5493 ];
5494 let events = to_playback_events(&score, &PlaybackOptions::default());
5495 score.parts[0].midi_control_changes.push(MidiControlChange {
5496 tick: 480,
5497 channel: 0,
5498 controller: 7,
5499 value: 100,
5500 });
5501 let request = OfflineRenderRequest {
5502 sample_rate_hz: 8_000,
5503 release_tail_millis: 125,
5504 scope: OfflineRenderScope::Selection {
5505 addresses: vec![events[1].source.clone().expect("source address")],
5506 },
5507 ..Default::default()
5508 };
5509 let manifest = build_offline_render_manifest(&score, &PlaybackOptions::default(), &request)
5510 .expect("selection manifest");
5511 assert_eq!(manifest.events.len(), 1);
5512 assert_eq!(manifest.events[0].source, events[1].source);
5513 assert_eq!(manifest.frame_events[0].start_frame, 0);
5514 assert!(manifest.semantic_events.iter().any(|event| matches!(
5515 event.kind,
5516 OfflineRenderSemanticEventKind::Controller {
5517 channel: 0,
5518 controller: 7,
5519 value: 100
5520 }
5521 ) && event.frame == 0));
5522 assert_eq!(manifest.release_tail_frames, 1_000);
5523 assert_eq!(manifest.duration_frames, 5_000);
5524 }
5525
5526 #[test]
5527 fn offline_render_request_owned_options_override_legacy_argument() {
5528 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5529 score.parts[0].staves[0].measures[0].voices[0][0] =
5530 Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5531 let request_options = PlaybackOptions {
5532 bpm_override: Some(60),
5533 ..Default::default()
5534 };
5535 let manifest = build_offline_render_manifest(
5536 &score,
5537 &PlaybackOptions::default(),
5538 &OfflineRenderRequest {
5539 sample_rate_hz: 48_000,
5540 playback_options: Some(request_options.clone()),
5541 ..Default::default()
5542 },
5543 )
5544 .expect("request-owned playback options");
5545 assert_eq!(manifest.playback_options, request_options);
5546 assert_eq!(manifest.frame_events[0].duration_frames, 48_000);
5547 }
5548
5549 #[test]
5550 fn offline_manifest_projects_typed_midi_events_to_exact_frames() {
5551 let mut score = Score::new("MIDI semantics", 120, 4, 4, 0, 1);
5552 score.parts[0].midi_pitch_bends.push(MidiPitchBend {
5553 tick: 480,
5554 channel: 2,
5555 value: -512,
5556 });
5557 score.parts[0].midi_program_changes.push(MidiProgramChange {
5558 tick: 480,
5559 channel: 2,
5560 program: 41,
5561 });
5562 score.parts[0].midi_aftertouch.push(MidiAftertouch {
5563 tick: 480,
5564 channel: 2,
5565 key: Some(64),
5566 value: 88,
5567 });
5568 let manifest = build_offline_render_manifest(
5569 &score,
5570 &PlaybackOptions::default(),
5571 &OfflineRenderRequest {
5572 sample_rate_hz: 48_000,
5573 ..Default::default()
5574 },
5575 )
5576 .expect("manifest builds");
5577 for kind in [
5578 OfflineRenderSemanticEventKind::PitchBend {
5579 channel: 2,
5580 value: -512,
5581 },
5582 OfflineRenderSemanticEventKind::ProgramChange {
5583 channel: 2,
5584 program: 41,
5585 },
5586 OfflineRenderSemanticEventKind::Aftertouch {
5587 channel: 2,
5588 key: Some(64),
5589 value: 88,
5590 },
5591 ] {
5592 assert!(manifest.semantic_events.iter().any(|event| {
5593 event.kind == kind
5594 && event.frame == 24_000
5595 && event
5596 .measure
5597 .as_ref()
5598 .is_some_and(|address| address.part == 0 && address.measure == 0)
5599 }));
5600 }
5601 }
5602
5603 #[test]
5604 fn offline_manifest_frames_cover_repeat_ramp_fermata_pedal_bend_and_instrument_change() {
5605 let mut score = Score::new("fixture", 120, 4, 4, 0, 2);
5606 let mut first = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
5607 first.pedal_start = true;
5608 first.articulations = vec![Articulation::Fermata, Articulation::BreathMark];
5609 first.guitar_bend_curve = vec![GuitarBendPoint {
5610 position_per_mille: 500,
5611 alter_cents: 100,
5612 }];
5613 score.parts[0].staves[0].measures[0].voices[0] = vec![first];
5614 score.parts[0].staves[0].measures[0].barline_left =
5615 super::super::notation::Barline::RepeatStart;
5616 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
5617 score.parts[0].staves[0].measures[0].navigation = Some("Segno".to_string());
5618 score.parts[0].midi_control_changes.push(MidiControlChange {
5619 tick: 480,
5620 channel: 0,
5621 controller: 11,
5622 value: 96,
5623 });
5624
5625 let mut second = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
5626 second.pedal_end = true;
5627 score.parts[0].staves[0].measures[1].voices[0] = vec![second];
5628 score.parts[0].staves[0].measures[1].barline_right =
5629 super::super::notation::Barline::RepeatEnd;
5630 let mut flute = crate::InstrumentDefinition::new("flute", "Flute");
5631 flute.midi_channel = 2;
5632 flute.midi_program = 73;
5633 score.parts[0].staves[0].measures[1].instrument_change = Some(flute);
5634
5635 let request = OfflineRenderRequest {
5636 sample_rate_hz: 48_000,
5637 playback_options: Some(PlaybackOptions {
5638 fermata_multiplier: 1.5,
5639 fermata_hold_beats: 0.25,
5640 ..Default::default()
5641 }),
5642 ..Default::default()
5643 };
5644 let manifest = build_offline_render_manifest(&score, &PlaybackOptions::default(), &request)
5645 .expect("fixture manifest");
5646 assert_eq!(manifest.frame_events.len(), 4);
5647 let measures: Vec<usize> = manifest
5648 .frame_events
5649 .iter()
5650 .map(|event| event.event.source.as_ref().unwrap().measure)
5651 .collect();
5652 assert_eq!(measures, vec![0, 1, 0, 1]);
5653 assert!(manifest.frame_events[0].event.pedal);
5654 assert_eq!(manifest.frame_events[0].event.post_note_pause_beats, 0.25);
5655 assert_eq!(manifest.frame_events[0].event.pitch_bend_curve.len(), 1);
5656 assert_eq!(
5657 manifest.frame_events[1].event.instrument_id.as_deref(),
5658 Some("flute")
5659 );
5660 assert_eq!(manifest.frame_events[3].event.program, 73);
5661 assert!(manifest.semantic_events.iter().any(|event| {
5662 matches!(
5663 &event.kind,
5664 OfflineRenderSemanticEventKind::InstrumentChange {
5665 instrument_id,
5666 channel: 2,
5667 program: 73,
5668 transpose_semitones: 0,
5669 } if instrument_id == "flute"
5670 ) && event
5671 .measure
5672 .as_ref()
5673 .is_some_and(|address| address.measure == 1)
5674 && event.frame == manifest.frame_events[1].start_frame
5675 }));
5676 assert!(manifest.semantic_events.iter().any(|event| matches!(
5677 event.kind,
5678 OfflineRenderSemanticEventKind::Pedal { down: true }
5679 ) && event.frame
5680 == manifest.frame_events[0].start_frame));
5681 assert!(manifest.semantic_events.iter().any(|event| matches!(
5682 event.kind,
5683 OfflineRenderSemanticEventKind::Pedal { down: false }
5684 ) && event.frame
5685 == manifest.frame_events[1].start_frame));
5686 assert!(manifest.semantic_events.iter().any(|event| matches!(
5687 event.kind,
5688 OfflineRenderSemanticEventKind::Articulation {
5689 articulation: Articulation::Fermata
5690 }
5691 )));
5692 assert!(manifest.semantic_events.iter().any(|event| matches!(
5693 event.kind,
5694 OfflineRenderSemanticEventKind::GuitarBend { .. }
5695 )));
5696 let tempo_events = manifest
5697 .semantic_events
5698 .iter()
5699 .filter(|event| matches!(event.kind, OfflineRenderSemanticEventKind::Tempo { .. }))
5700 .count();
5701 let navigation_events = manifest
5702 .semantic_events
5703 .iter()
5704 .filter(|event| {
5705 matches!(
5706 event.kind,
5707 OfflineRenderSemanticEventKind::Navigation { .. }
5708 )
5709 })
5710 .count();
5711 let control_frames: Vec<u64> = manifest
5712 .semantic_events
5713 .iter()
5714 .filter_map(|event| {
5715 matches!(
5716 event.kind,
5717 OfflineRenderSemanticEventKind::Controller {
5718 channel: 0,
5719 controller: 11,
5720 value: 96
5721 }
5722 )
5723 .then_some(event.frame)
5724 })
5725 .collect();
5726 assert_eq!(tempo_events, 2);
5727 assert_eq!(navigation_events, 2);
5728 assert_eq!(control_frames.len(), 2);
5729 let ramp_duration = tempo_ramp_seconds(120.0, Some(60.0), 4.0, 4.0);
5730 let first_hold = 0.5 * 60.0 / 105.0;
5733 let second_hold = 0.5;
5734 assert_eq!(
5735 control_frames[0],
5736 ((tempo_ramp_seconds(120.0, Some(60.0), 4.0, 1.0) + first_hold) * 48_000.0).round()
5737 as u64
5738 );
5739 assert_eq!(
5740 control_frames[1],
5741 ((ramp_duration + 5.0 + first_hold + second_hold) * 48_000.0).round() as u64
5742 );
5743
5744 let expected_starts = [
5745 0.0,
5746 ramp_duration + first_hold,
5747 ramp_duration + first_hold + 4.0,
5748 ramp_duration + first_hold + 8.0 + second_hold,
5749 ];
5750 for (frame_event, expected_secs) in manifest.frame_events.iter().zip(expected_starts) {
5751 assert_eq!(
5752 frame_event.start_frame,
5753 (expected_secs * 48_000.0).round() as u64
5754 );
5755 }
5756 }
5757
5758 #[test]
5759 fn offline_render_manifest_reports_unresolved_selection_address() {
5760 let score = Score::new("T", 120, 4, 4, 0, 1);
5761 let request = OfflineRenderRequest {
5762 scope: OfflineRenderScope::Selection {
5763 addresses: vec![NoteAddr {
5764 part: 0,
5765 staff: 0,
5766 measure: 0,
5767 voice: 0,
5768 note: 99,
5769 }],
5770 },
5771 ..Default::default()
5772 };
5773 let manifest = build_offline_render_manifest(&score, &PlaybackOptions::default(), &request)
5774 .expect("empty selection is representable");
5775 assert_eq!(manifest.events.len(), 0);
5776 assert_eq!(
5777 manifest.diagnostics,
5778 vec![
5779 OfflineRenderDiagnostic {
5780 kind: OfflineRenderDiagnosticKind::UnresolvedSelectionAddress,
5781 source: Some(NoteAddr {
5782 part: 0,
5783 staff: 0,
5784 measure: 0,
5785 voice: 0,
5786 note: 99,
5787 }),
5788 },
5789 OfflineRenderDiagnostic {
5790 kind: OfflineRenderDiagnosticKind::EmptySelection,
5791 source: None,
5792 },
5793 ]
5794 );
5795 }
5796
5797 #[test]
5798 fn offline_render_manifest_v1_json_migrates_with_v2_defaults() {
5799 let legacy = serde_json::json!({
5800 "contract_version": 1,
5801 "playback_contract_version": 2,
5802 "format": "Wav",
5803 "sample_rate_hz": 48_000,
5804 "channels": 2,
5805 "duration_frames": 0,
5806 "events": []
5807 });
5808 let manifest: OfflineRenderManifest =
5809 serde_json::from_value(legacy).expect("v1 manifest remains readable");
5810 assert_eq!(manifest.contract_version, 1);
5811 assert_eq!(manifest.scope, OfflineRenderScope::FullScore);
5812 assert_eq!(manifest.release_tail_frames, 0);
5813 assert_eq!(manifest.playback_options, PlaybackOptions::default());
5814 assert!(manifest.frame_events.is_empty());
5815 assert!(manifest.diagnostics.is_empty());
5816 assert!(manifest.validate().is_ok());
5817 }
5818
5819 #[test]
5820 fn offline_render_manifest_validation_rejects_tampered_frame_and_tail_boundaries() {
5821 let mut score = Score::new("T", 120, 4, 4, 0, 1);
5822 score.parts[0].staves[0].measures[0].voices[0] =
5823 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5824 let manifest = build_offline_render_manifest(
5825 &score,
5826 &PlaybackOptions::default(),
5827 &OfflineRenderRequest {
5828 sample_rate_hz: 48_000,
5829 release_tail_millis: 100,
5830 ..Default::default()
5831 },
5832 )
5833 .expect("manifest builds");
5834 assert!(manifest.validate().is_ok());
5835
5836 let mut invalid_tail = manifest.clone();
5837 invalid_tail.duration_frames = invalid_tail.duration_frames.saturating_sub(1);
5838 assert!(matches!(
5839 invalid_tail.validate(),
5840 Err(crate::Error::InvalidOfflineRenderRequest)
5841 ));
5842
5843 let mut invalid_frame = manifest;
5844 invalid_frame.frame_events[0].end_frame =
5845 invalid_frame.frame_events[0].end_frame.saturating_add(1);
5846 assert!(matches!(
5847 invalid_frame.validate(),
5848 Err(crate::Error::InvalidOfflineRenderRequest)
5849 ));
5850 }
5851
5852 #[test]
5853 fn routing_manifest_prefers_instrument_bus_and_isolates_metronome() {
5854 let mut score = Score::new("routing", 120, 4, 4, 0, 1);
5855 let mut instrument = crate::InstrumentDefinition::new("flute", "Flute");
5856 instrument.midi_channel = 2;
5857 instrument.midi_program = 73;
5858 score.parts[0].instrument = Some(instrument);
5859 score.parts[0].staves[0].measures[0].voices[0] =
5860 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5861 let options = PlaybackOptions {
5862 metronome: Some(MetronomeConfig::default()),
5863 ..Default::default()
5864 };
5865 let mut config = PlaybackRoutingConfig {
5866 provider_identity: Some("sf2-host@1".into()),
5867 ..Default::default()
5868 };
5869 config.part_buses.insert(0, "parts".into());
5870 config
5871 .instrument_buses
5872 .insert("flute".into(), "winds".into());
5873 config.aux_sends.push(PlaybackAuxSend {
5874 source_bus: "winds".into(),
5875 destination_bus: "reverb".into(),
5876 gain_db: -12.0,
5877 });
5878 config.effect_routes.push(PlaybackEffectRoute {
5879 bus_id: "reverb".into(),
5880 effect_id: "convolution".into(),
5881 enabled: true,
5882 });
5883
5884 let manifest = build_playback_routing_manifest(&score, &options, &config)
5885 .expect("valid routing manifest");
5886 assert_eq!(manifest.contract_version, PLAYBACK_ROUTING_CONTRACT_VERSION);
5887 assert_eq!(manifest.provider_identity.as_deref(), Some("sf2-host@1"));
5888 assert!(manifest.routes.iter().any(|route| {
5889 route.instrument_id.as_deref() == Some("flute") && route.bus_id == "winds"
5890 }));
5891 assert!(
5892 manifest
5893 .routes
5894 .iter()
5895 .any(|route| route.is_metronome && route.bus_id == "metronome")
5896 );
5897 assert_eq!(manifest.aux_sends.len(), 1);
5898 assert_eq!(manifest.effect_routes.len(), 1);
5899 }
5900
5901 #[test]
5902 fn routing_manifest_rejects_unsafe_bus_and_non_finite_send_gain() {
5903 let score = Score::new("routing", 120, 4, 4, 0, 1);
5904 let config = PlaybackRoutingConfig {
5905 master_bus: "not a bus".into(),
5906 ..Default::default()
5907 };
5908 assert!(matches!(
5909 build_playback_routing_manifest(&score, &PlaybackOptions::default(), &config),
5910 Err(crate::Error::InvalidPlaybackRouting)
5911 ));
5912
5913 let mut config = PlaybackRoutingConfig::default();
5914 config.aux_sends.push(PlaybackAuxSend {
5915 source_bus: "part".into(),
5916 destination_bus: "master".into(),
5917 gain_db: f64::NAN,
5918 });
5919 assert!(matches!(
5920 build_playback_routing_manifest(&score, &PlaybackOptions::default(), &config),
5921 Err(crate::Error::InvalidPlaybackRouting)
5922 ));
5923 }
5924
5925 #[test]
5926 fn timing_corpus_reports_score_schedule_results_without_audio_claims() {
5927 let mut score = Score::new("corpus", 120, 4, 4, 0, 1);
5928 score.parts[0].staves[0].measures[0].voices[0] =
5929 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
5930 let expected_events = to_playback_events(&score, &PlaybackOptions::default());
5931 let report = evaluate_playback_timing_corpus(&[PlaybackTimingCase {
5932 id: "quarter-note".into(),
5933 score,
5934 options: PlaybackOptions::default(),
5935 expected_events,
5936 tolerance: PlaybackTimingTolerance::default(),
5937 }])
5938 .expect("valid timing corpus");
5939 assert_eq!(
5940 report.contract_version,
5941 PLAYBACK_TIMING_CORPUS_CONTRACT_VERSION
5942 );
5943 assert_eq!(report.total_cases, 1);
5944 assert_eq!(report.passed_cases, 1);
5945 assert!(report.cases[0].report.within_tolerance);
5946 }
5947
5948 #[test]
5949 fn timing_corpus_rejects_empty_or_duplicate_case_ids() {
5950 assert!(matches!(
5951 evaluate_playback_timing_corpus(&[]),
5952 Err(crate::Error::InvalidPlaybackTimingCorpus)
5953 ));
5954 let score = Score::new("corpus", 120, 4, 4, 0, 1);
5955 let case = PlaybackTimingCase {
5956 id: "duplicate".into(),
5957 expected_events: to_playback_events(&score, &PlaybackOptions::default()),
5958 score,
5959 options: PlaybackOptions::default(),
5960 tolerance: PlaybackTimingTolerance::default(),
5961 };
5962 assert!(matches!(
5963 evaluate_playback_timing_corpus(&[case.clone(), case]),
5964 Err(crate::Error::InvalidPlaybackTimingCorpus)
5965 ));
5966 }
5967
5968 fn comparison_event(time_secs: f64, duration_secs: f64) -> PlaybackEvent {
5969 PlaybackEvent {
5970 address: Some("0:0:0:0:0".into()),
5971 source: Some(NoteAddr {
5972 part: 0,
5973 staff: 0,
5974 measure: 0,
5975 voice: 0,
5976 note: 0,
5977 }),
5978 source_voice_number: None,
5979 time_beats: 0.0,
5980 time_secs,
5981 pitch_midi: 60,
5982 pitch_midi_cents: 6000,
5983 pitch_bend_curve: Vec::new(),
5984 post_note_pause_beats: 0.0,
5985 articulations: Vec::new(),
5986 chord_symbol: None,
5987 guitar_technique: None,
5988 velocity: 64,
5989 duration_beats: 1.0,
5990 duration_secs,
5991 pedal: false,
5992 part_index: 0,
5993 channel: 0,
5994 program: 0,
5995 instrument_id: None,
5996 is_metronome: false,
5997 }
5998 }
5999
6000 #[test]
6001 fn legacy_playback_event_json_defaults_bend_curve() {
6002 let event: PlaybackEvent = serde_json::from_str(
6003 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}"#,
6004 )
6005 .expect("legacy playback event remains readable");
6006 assert!(event.pitch_bend_curve.is_empty());
6007 assert_eq!(event.guitar_technique, None);
6008 }
6009
6010 #[test]
6011 fn tempo_ramp_uses_integrated_event_timing_and_carries_target_forward() {
6012 let mut score = Score::new("ramp", 120, 4, 4, 0, 2);
6013 for measure in &mut score.parts[0].staves[0].measures {
6014 measure.voices[0] = vec![
6015 crate::Note::new(crate::Pitch::new(crate::Step::C, 4), crate::Duration::Half),
6016 crate::Note::new(crate::Pitch::new(crate::Step::D, 4), crate::Duration::Half),
6017 ];
6018 }
6019 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
6020 let events = to_playback_events(&score, &PlaybackOptions::default());
6021 assert_eq!(events.len(), 4);
6022 let expected_measure_secs = 4.0 * 60.0 / (60.0 - 120.0) * (60.0f64 / 120.0).ln();
6023 assert!((events[2].time_secs - expected_measure_secs).abs() < 1e-9);
6024 assert!((events[0].duration_secs - 1.1507282898).abs() < 1e-6);
6025 assert!((events[2].duration_secs - 2.0).abs() < 1e-9);
6026 }
6027
6028 #[test]
6029 fn breath_and_caesura_expose_deterministic_post_note_pause() {
6030 let mut score = Score::new("pause", 120, 4, 4, 0, 1);
6031 let mut breath = crate::Note::new(
6032 crate::Pitch::new(crate::Step::C, 4),
6033 crate::Duration::Quarter,
6034 );
6035 breath.articulations.push(Articulation::BreathMark);
6036 let mut caesura = crate::Note::new(
6037 crate::Pitch::new(crate::Step::D, 4),
6038 crate::Duration::Quarter,
6039 );
6040 caesura
6041 .articulations
6042 .extend([Articulation::BreathMark, Articulation::Caesura]);
6043 score.parts[0].staves[0].measures[0].voices[0] = vec![breath, caesura];
6044
6045 let events = to_playback_events(&score, &PlaybackOptions::default());
6046 assert_eq!(events.len(), 2);
6047 assert_eq!(events[0].post_note_pause_beats, 0.25);
6048 assert_eq!(events[1].post_note_pause_beats, 0.5);
6049 assert_eq!(
6050 events[1].articulations,
6051 vec![Articulation::BreathMark, Articulation::Caesura]
6052 );
6053 }
6054
6055 #[test]
6056 fn measure_instrument_change_resolves_on_playback_events() {
6057 let mut score = Score::new("instrument", 120, 4, 4, 0, 2);
6058 let mut base = crate::InstrumentDefinition::new("piano", "Piano");
6059 base.midi_channel = 1;
6060 base.midi_program = 0;
6061 score.parts[0].instrument = Some(base);
6062 let mut change = crate::InstrumentDefinition::new("flute", "Flute");
6063 change.midi_channel = 2;
6064 change.midi_program = 73;
6065 change.transpose_semitones = -2;
6066 score.parts[0].staves[0].measures[1].instrument_change = Some(change);
6067 for measure in &mut score.parts[0].staves[0].measures {
6068 measure.voices[0] = vec![crate::Note::new(
6069 crate::Pitch::new(crate::Step::C, 4),
6070 crate::Duration::Whole,
6071 )];
6072 }
6073
6074 let events = to_playback_events(&score, &PlaybackOptions::default());
6075 assert_eq!(events.len(), 2);
6076 assert_eq!(
6077 (
6078 events[0].channel,
6079 events[0].program,
6080 events[0].instrument_id.as_deref()
6081 ),
6082 (1, 0, Some("piano"))
6083 );
6084 assert_eq!(
6085 (
6086 events[1].channel,
6087 events[1].program,
6088 events[1].instrument_id.as_deref()
6089 ),
6090 (2, 73, Some("flute"))
6091 );
6092 assert_eq!(events[0].pitch_midi, 60);
6093 assert_eq!(events[1].pitch_midi, 58);
6094 }
6095
6096 #[test]
6097 fn authored_midi_program_change_selects_the_next_note_at_its_source_tick() {
6098 let mut score = Score::new("program", 120, 4, 4, 0, 1);
6099 score.parts[0].staves[0].measures[0].voices[0] = vec![
6100 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
6101 Note::new(Pitch::new(Step::E, 4), Duration::Quarter),
6102 ];
6103 score.parts[0].midi_program_changes = vec![
6104 MidiProgramChange {
6105 tick: 480,
6106 channel: 1,
6107 program: 73,
6108 },
6109 MidiProgramChange {
6110 tick: 480,
6111 channel: 0,
6112 program: 40,
6113 },
6114 ];
6115 let events = to_playback_events(&score, &PlaybackOptions::default());
6116 assert_eq!(events.len(), 2);
6117 assert_eq!((events[0].program, events[1].program), (0, 40));
6118 assert_eq!((events[0].time_secs, events[1].time_secs), (0.0, 0.5));
6119 }
6120
6121 #[test]
6122 fn generic_playback_event_preserves_authored_guitar_technique() {
6123 let mut score = Score::new("technique", 120, 4, 4, 0, 1);
6124 let mut note = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
6125 note.guitar_technique = Some(GuitarTechnique::HammerOn);
6126 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
6127
6128 let event = to_playback_events(&score, &PlaybackOptions::default())
6129 .into_iter()
6130 .next()
6131 .expect("one note event");
6132 assert_eq!(event.guitar_technique, Some(GuitarTechnique::HammerOn));
6133 }
6134
6135 #[test]
6136 fn playback_timing_comparison_reports_tolerance_and_identity() {
6137 let expected = [comparison_event(1.0, 0.5)];
6138 let actual = [comparison_event(1.004, 0.503)];
6139 let report = compare_playback_timing(
6140 &expected,
6141 &actual,
6142 &PlaybackTimingTolerance {
6143 start_secs: 0.005,
6144 duration_secs: 0.005,
6145 },
6146 )
6147 .expect("comparison");
6148 assert!(report.within_tolerance);
6149 assert_eq!(report.matched_events, 1);
6150 assert_eq!(
6151 report.contract_version,
6152 PLAYBACK_COMPARISON_CONTRACT_VERSION
6153 );
6154
6155 let mut typed_only = comparison_event(1.004, 0.503);
6156 typed_only.address = None;
6157 let report = compare_playback_timing(
6158 &expected,
6159 &[typed_only],
6160 &PlaybackTimingTolerance::default(),
6161 )
6162 .expect("typed source comparison");
6163 assert_eq!(report.matched_events, 1);
6164
6165 let actual = [comparison_event(1.02, 0.6)];
6166 let report =
6167 compare_playback_timing(&expected, &actual, &PlaybackTimingTolerance::default())
6168 .expect("comparison");
6169 assert!(!report.within_tolerance);
6170 assert!(
6171 report
6172 .mismatches
6173 .iter()
6174 .any(|m| matches!(m, PlaybackTimingMismatch::StartTime { index: 0, .. }))
6175 );
6176 assert!(
6177 report
6178 .mismatches
6179 .iter()
6180 .any(|m| matches!(m, PlaybackTimingMismatch::Duration { index: 0, .. }))
6181 );
6182 }
6183
6184 #[test]
6185 fn playback_timing_comparison_rejects_instrument_route_mismatch() {
6186 let expected = comparison_event(0.0, 0.5);
6187 let mut actual = expected.clone();
6188 actual.program = 73;
6189 assert!(
6190 compare_playback_timing(&[expected], &[actual], &PlaybackTimingTolerance::default(),)
6191 .expect("comparison")
6192 .mismatches
6193 .iter()
6194 .any(|mismatch| matches!(
6195 mismatch,
6196 PlaybackTimingMismatch::EventIdentity { index: 0 }
6197 ))
6198 );
6199 }
6200
6201 #[test]
6202 fn playback_timing_comparison_rejects_invalid_tolerance() {
6203 let error = compare_playback_timing(
6204 &[],
6205 &[],
6206 &PlaybackTimingTolerance {
6207 start_secs: -0.1,
6208 duration_secs: 0.0,
6209 },
6210 );
6211 assert!(matches!(
6212 error,
6213 Err(crate::Error::InvalidPlaybackComparison)
6214 ));
6215 }
6216
6217 #[test]
6218 fn tablature_performance_projection_keeps_position_and_reports_pitch_error() {
6219 let mut score = Score::new("Tab", 120, 4, 4, 0, 1);
6220 score.parts[0].staves[0].tablature = Some(super::super::notation::TablatureConfig {
6221 lines: 6,
6222 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6223 capo: 0,
6224 });
6225 let notes = &mut score.parts[0].staves[0].measures[0].voices[0];
6226 *notes = vec![crate::Note::new(
6227 crate::Pitch::new(crate::Step::C, 4),
6228 crate::Duration::Quarter,
6229 )];
6230 notes[0].tab_position = Some(super::super::notation::TabPosition {
6231 string: 1,
6232 fret: 20,
6233 });
6234 notes[0].guitar_technique = Some(super::super::notation::GuitarTechnique::Bend);
6235 notes[0].guitar_bend_alter_cents = Some(150);
6236 notes[0].guitar_bend_curve = vec![
6237 super::super::score::GuitarBendPoint {
6238 position_per_mille: 0,
6239 alter_cents: 0,
6240 },
6241 super::super::score::GuitarBendPoint {
6242 position_per_mille: 500,
6243 alter_cents: 150,
6244 },
6245 super::super::score::GuitarBendPoint {
6246 position_per_mille: 1_000,
6247 alter_cents: 0,
6248 },
6249 ];
6250 let report = project_tablature_performance(&score, &PlaybackOptions::default())
6251 .expect("tablature projection");
6252 assert_eq!(report.events.len(), 1);
6253 assert_eq!(report.events[0].string, 1);
6254 assert_eq!(report.events[0].fret, 20);
6255 assert_eq!(
6256 report.events[0].technique,
6257 Some(super::super::notation::GuitarTechnique::Bend)
6258 );
6259 assert_eq!(report.events[0].bend_alter_cents, Some(150));
6260 assert_eq!(report.events[0].bend_curve.len(), 3);
6261 assert_eq!(report.events[0].bend_curve[1].position_per_mille, 500);
6262 assert_eq!(report.events[0].bend_curve[1].alter_cents, 150);
6263 assert_eq!(
6264 report.events[0].playback.pitch_bend_curve,
6265 report.events[0].bend_curve
6266 );
6267 assert!(report.diagnostics.is_empty());
6268
6269 score.parts[0].staves[0].measures[0].voices[0][0].tab_position =
6270 Some(super::super::notation::TabPosition {
6271 string: 1,
6272 fret: 19,
6273 });
6274 let report = project_tablature_performance(&score, &PlaybackOptions::default())
6275 .expect("tablature projection");
6276 assert_eq!(report.events[0].pitch_error_cents, 100);
6277 assert!(matches!(
6278 report.diagnostics[0],
6279 TablaturePerformanceDiagnostic::PitchMismatch {
6280 error_cents: 100,
6281 ..
6282 }
6283 ));
6284 }
6285
6286 #[test]
6287 fn tablature_performance_projection_does_not_invent_missing_positions() {
6288 let mut score = Score::new("Tab", 120, 4, 4, 0, 1);
6289 score.parts[0].staves[0].tablature = Some(super::super::notation::TablatureConfig {
6290 lines: 6,
6291 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6292 capo: 0,
6293 });
6294 score.parts[0].staves[0].measures[0].voices[0].push(crate::Note::new(
6295 crate::Pitch::new(crate::Step::C, 4),
6296 crate::Duration::Quarter,
6297 ));
6298 let report = project_tablature_performance(&score, &PlaybackOptions::default())
6299 .expect("tablature projection");
6300 assert!(report.events.is_empty());
6301 assert!(matches!(
6302 report.diagnostics[0],
6303 TablaturePerformanceDiagnostic::MissingPosition { .. }
6304 ));
6305 }
6306
6307 #[test]
6308 fn tablature_performance_event_accepts_legacy_json_without_technique() {
6309 let mut score = Score::new("Tab", 120, 4, 4, 0, 1);
6310 score.parts[0].staves[0].tablature = Some(super::super::notation::TablatureConfig {
6311 lines: 6,
6312 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6313 capo: 0,
6314 });
6315 let mut note = crate::Note::new(
6316 crate::Pitch::new(crate::Step::C, 4),
6317 crate::Duration::Quarter,
6318 );
6319 note.tab_position = Some(super::super::notation::TabPosition {
6320 string: 1,
6321 fret: 20,
6322 });
6323 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
6324 let report = project_tablature_performance(&score, &PlaybackOptions::default())
6325 .expect("tablature projection");
6326 let mut legacy = serde_json::to_value(&report.events[0]).expect("event JSON");
6327 legacy
6328 .as_object_mut()
6329 .expect("event object")
6330 .remove("technique");
6331 let restored: TablaturePerformanceEvent =
6332 serde_json::from_value(legacy).expect("legacy event JSON");
6333 assert_eq!(restored.technique, None);
6334 }
6335
6336 #[test]
6337 fn tablature_round_trip_report_preserves_authored_positions() {
6338 let mut score = Score::new("Tab", 120, 4, 4, 0, 1);
6339 score.parts[0].staves[0].tablature = Some(super::super::notation::TablatureConfig {
6340 lines: 6,
6341 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6342 capo: 2,
6343 });
6344 let mut note = crate::Note::new(
6345 crate::Pitch::new(crate::Step::C, 4),
6346 crate::Duration::Quarter,
6347 );
6348 note.tab_positions = vec![crate::TabPosition { string: 5, fret: 1 }];
6349 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
6350 let report = tablature_round_trip_report(&score).expect("tab round-trip report");
6351 assert!(report.equivalent);
6352 assert_eq!(report.checked_notes, 1);
6353 assert_eq!(report.positioned_notes, 1);
6354 assert!(report.diagnostics.is_empty());
6355 }
6356
6357 #[test]
6358 fn tablature_performance_uses_measure_local_tuning_change() {
6359 let mut score = Score::new("Local tuning", 120, 4, 4, 0, 2);
6360 let staff = &mut score.parts[0].staves[0];
6361 staff.tablature = Some(super::super::notation::TablatureConfig {
6362 lines: 6,
6363 tuning_midi: vec![40, 45, 50, 55, 59, 64],
6364 capo: 0,
6365 });
6366 staff.measures[1].tablature_change = Some(super::super::notation::TablatureConfig {
6367 lines: 6,
6368 tuning_midi: vec![38, 45, 50, 55, 59, 64],
6369 capo: 0,
6370 });
6371 let mut note = Note::new(Pitch::new(Step::C, 3), Duration::Quarter);
6372 note.tab_position = Some(TabPosition {
6373 string: 1,
6374 fret: 10,
6375 });
6376 staff.measures[1].voices[0] = vec![note];
6377
6378 let report = project_tablature_performance(&score, &PlaybackOptions::default())
6379 .expect("tablature projection");
6380 assert_eq!(report.events.len(), 1);
6381 assert_eq!(report.events[0].expected_pitch_midi_cents, 4_800);
6382 assert!(!report.diagnostics.iter().any(|diagnostic| matches!(
6383 diagnostic,
6384 TablaturePerformanceDiagnostic::PitchMismatch { .. }
6385 )));
6386 }
6387}