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