1use super::duration::Duration;
2use super::notation::{Articulation, ChordSymbol, GuitarTechnique, TabPosition};
3use super::repeat::measure_sequence;
4use super::score::{GuitarBendPoint, Note, NoteAddr, Score};
5use serde::{Deserialize, Serialize};
6use std::collections::{BTreeMap, BTreeSet};
7
8fn default_fermata_multiplier() -> f64 {
9 1.5
10}
11fn default_fermata_hold_beats() -> f64 {
12 0.0
13}
14fn default_swing_unit() -> Duration {
15 Duration::Eighth
16}
17fn default_tempo_multiplier() -> f64 {
18 1.0
19}
20fn default_metronome_channel() -> u8 {
21 9
22}
23fn default_accent_pitch() -> u8 {
24 76
25}
26fn default_beat_pitch() -> u8 {
27 77
28}
29fn default_accent_velocity() -> u8 {
30 100
31}
32fn default_beat_velocity() -> u8 {
33 70
34}
35
36#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
42#[serde(rename_all = "kebab-case")]
43pub enum PlaybackRealizationProfile {
44 #[default]
46 Authored,
47 OrnamentArpeggioV1,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
61pub struct MetronomeConfig {
62 #[serde(default = "default_metronome_channel")]
64 pub channel: u8,
65 #[serde(default = "default_accent_pitch")]
67 pub accent_pitch: u8,
68 #[serde(default = "default_beat_pitch")]
70 pub beat_pitch: u8,
71 #[serde(default = "default_accent_velocity")]
73 pub accent_velocity: u8,
74 #[serde(default = "default_beat_velocity")]
76 pub beat_velocity: u8,
77}
78
79impl Default for MetronomeConfig {
80 fn default() -> Self {
81 Self {
82 channel: 9,
83 accent_pitch: 76,
84 beat_pitch: 77,
85 accent_velocity: 100,
86 beat_velocity: 70,
87 }
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93#[serde(default)]
94pub struct PlaybackOptions {
95 pub bpm_override: Option<u16>,
100 #[serde(default = "default_tempo_multiplier")]
105 pub tempo_multiplier: f64,
106 pub muted_parts: Vec<usize>,
108 #[serde(default)]
112 pub loop_region: Option<(usize, usize)>,
113 #[serde(default = "default_fermata_multiplier")]
115 pub fermata_multiplier: f64,
116 #[serde(default = "default_fermata_hold_beats")]
119 pub fermata_hold_beats: f64,
120 #[serde(default)]
125 pub swing: Option<f64>,
126 #[serde(default = "default_swing_unit")]
129 pub swing_unit: Duration,
130 #[serde(default)]
133 pub metronome: Option<MetronomeConfig>,
134 #[serde(default)]
137 pub realization_profile: PlaybackRealizationProfile,
138}
139
140impl Default for PlaybackOptions {
141 fn default() -> Self {
142 Self {
143 bpm_override: None,
144 tempo_multiplier: 1.0,
145 muted_parts: Vec::new(),
146 loop_region: None,
147 fermata_multiplier: 1.5,
148 fermata_hold_beats: 0.0,
149 swing: None,
150 swing_unit: Duration::Eighth,
151 metronome: None,
152 realization_profile: PlaybackRealizationProfile::Authored,
153 }
154 }
155}
156
157fn normalized_tempo_multiplier(options: &PlaybackOptions) -> f64 {
158 let multiplier = options.tempo_multiplier;
159 if multiplier.is_finite() && multiplier > 0.0 {
160 multiplier
161 } else {
162 1.0
163 }
164}
165
166fn swingable_rhythm_slot(note: &Note, unit: &Duration) -> bool {
167 !note.is_grace
168 && !note.is_cue
169 && note.tuplet.is_none()
170 && note.dot_count == 0
171 && note.duration == *unit
172}
173
174fn starts_swing_pair(
175 notes: &[Note],
176 note_index: usize,
177 written_beats: f64,
178 grid_offset_beats: f64,
179 measure_beats: f64,
180 unit: &Duration,
181) -> bool {
182 let Some(first) = notes.get(note_index) else {
183 return false;
184 };
185 let Some(second) = notes.get(note_index + 1) else {
186 return false;
187 };
188 if !swingable_rhythm_slot(first, unit) || !swingable_rhythm_slot(second, unit) {
189 return false;
190 }
191 let pair_beats = first.beats() * 2.0;
192 if written_beats + pair_beats > measure_beats + 1e-9 {
193 return false;
194 }
195 let slot = (written_beats + grid_offset_beats) / pair_beats;
196 (slot - slot.round()).abs() < 1e-9
197}
198
199fn initial_tempo_bpm(score: &Score, options: &PlaybackOptions) -> f64 {
200 f64::from(
201 options
202 .bpm_override
203 .unwrap_or(score.settings.tempo_bpm)
204 .max(1),
205 ) * normalized_tempo_multiplier(options)
206}
207
208fn written_tempo_bpm(options: &PlaybackOptions, tempo: u16) -> Option<f64> {
209 options
210 .bpm_override
211 .is_none()
212 .then(|| f64::from(tempo.max(1)) * normalized_tempo_multiplier(options))
213}
214
215fn written_tempo_ramp_bpm(options: &PlaybackOptions, tempo: Option<u16>) -> Option<f64> {
216 options.bpm_override.is_none().then_some(())?;
217 tempo.map(|tempo| f64::from(tempo.max(1)) * normalized_tempo_multiplier(options))
218}
219
220#[derive(Clone, Copy, Default)]
221struct ScoreTempoMark {
222 bpm: Option<u16>,
223 ramp_to_bpm: Option<u16>,
224}
225
226fn score_tempo_marks(score: &Score) -> Vec<ScoreTempoMark> {
229 let count = score
230 .parts
231 .iter()
232 .flat_map(|part| &part.staves)
233 .map(|staff| staff.measures.len())
234 .max()
235 .unwrap_or(0);
236 let mut marks = vec![ScoreTempoMark::default(); count];
237 for (index, mark) in marks.iter_mut().enumerate() {
238 for part in &score.parts {
239 for staff in &part.staves {
240 if let Some(measure) = staff.measures.get(index) {
241 mark.bpm = mark.bpm.or(measure.tempo);
242 mark.ramp_to_bpm = mark.ramp_to_bpm.or(measure.tempo_ramp_to);
243 }
244 }
245 }
246 }
247 marks
248}
249
250fn semantic_tempo_bpm(bpm: f64) -> u16 {
251 bpm.round().clamp(1.0, f64::from(u16::MAX)) as u16
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
258pub struct PlaybackEvent {
259 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub address: Option<String>,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub source: Option<NoteAddr>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub source_voice_number: Option<u32>,
270 pub time_beats: f64,
272 pub time_secs: f64,
274 pub pitch_midi: u8,
276 #[serde(default)]
281 pub pitch_midi_cents: i32,
282 #[serde(default)]
286 pub pitch_bend_curve: Vec<GuitarBendPoint>,
287 #[serde(default)]
290 pub post_note_pause_beats: f64,
291 #[serde(default)]
294 pub articulations: Vec<Articulation>,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub chord_symbol: Option<ChordSymbol>,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub guitar_technique: Option<GuitarTechnique>,
303 pub velocity: u8,
307 pub duration_beats: f64,
309 pub duration_secs: f64,
311 pub pedal: bool,
313 pub part_index: usize,
315 pub channel: u8,
317 #[serde(default)]
319 pub program: u8,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub instrument_id: Option<String>,
323 #[serde(default)]
325 pub is_metronome: bool,
326}
327
328pub const PLAYBACK_COMPARISON_CONTRACT_VERSION: u16 = 2;
330pub const MAX_PLAYBACK_COMPARISON_EVENTS: usize = 1_000_000;
332const MAX_PLAYBACK_MISMATCHES: usize = 256;
333pub const MAX_TAB_PERFORMANCE_EVENTS: usize = 1_000_000;
335const MAX_TAB_PERFORMANCE_DIAGNOSTICS: usize = 1_024;
336
337pub const OFFLINE_RENDER_CONTRACT_VERSION: u16 = 3;
339
340#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
343#[serde(rename_all = "kebab-case")]
344pub enum OfflineRenderScope {
345 #[default]
346 FullScore,
347 MeasureRange {
348 start: usize,
349 end: usize,
350 },
351 Selection {
352 addresses: Vec<NoteAddr>,
353 },
354}
355
356#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
358#[serde(rename_all = "kebab-case")]
359pub enum OfflineRenderNavigationPolicy {
360 #[default]
362 Authored,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
367pub struct OfflineRenderFrameEvent {
368 pub event: PlaybackEvent,
369 pub start_frame: u64,
370 pub duration_frames: u64,
371 pub end_frame: u64,
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
378#[serde(rename_all = "kebab-case")]
379pub enum OfflineRenderSemanticEventKind {
380 InstrumentChange {
383 instrument_id: String,
384 channel: u8,
385 program: u8,
386 transpose_semitones: i8,
387 },
388 Controller {
389 channel: u8,
390 controller: u8,
391 value: u8,
392 },
393 PitchBend {
395 channel: u8,
396 value: i16,
397 },
398 ProgramChange {
400 channel: u8,
401 program: u8,
402 },
403 Aftertouch {
405 channel: u8,
406 #[serde(default, skip_serializing_if = "Option::is_none")]
407 key: Option<u8>,
408 value: u8,
409 },
410 Tempo {
411 bpm: u16,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
413 ramp_to_bpm: Option<u16>,
414 },
415 Navigation {
416 marker: String,
417 },
418 Pedal {
419 down: bool,
420 },
421 Articulation {
422 articulation: Articulation,
423 },
424 GuitarBend {
425 points: Vec<GuitarBendPoint>,
426 },
427 ReleaseTail,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
434pub struct OfflineRenderSemanticFrameEvent {
435 pub kind: OfflineRenderSemanticEventKind,
436 pub frame: u64,
437 #[serde(default, skip_serializing_if = "Option::is_none")]
438 pub source: Option<NoteAddr>,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub measure: Option<OfflineRenderMeasureAddress>,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
445pub struct OfflineRenderMeasureAddress {
446 pub part: usize,
447 pub staff: usize,
448 pub measure: usize,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
453#[serde(rename_all = "kebab-case")]
454pub enum OfflineRenderDiagnosticKind {
455 UnresolvedSelectionAddress,
457 EmptySelection,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
463pub struct OfflineRenderDiagnostic {
464 pub kind: OfflineRenderDiagnosticKind,
465 #[serde(default, skip_serializing_if = "Option::is_none")]
466 pub source: Option<NoteAddr>,
467}
468
469#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
474pub enum OfflineRenderFormat {
475 #[default]
476 Wav,
477 Flac,
478 Mp3,
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
483pub struct OfflineRenderRequest {
484 #[serde(default)]
486 pub format: OfflineRenderFormat,
487 #[serde(default = "default_offline_render_sample_rate")]
489 pub sample_rate_hz: u32,
490 #[serde(default = "default_offline_render_channels")]
492 pub channels: u8,
493 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub provider_identity: Option<String>,
496 #[serde(default, skip_serializing_if = "Option::is_none")]
498 pub asset_identity: Option<String>,
499 #[serde(default, skip_serializing_if = "Option::is_none")]
501 pub view_id: Option<String>,
502 #[serde(default)]
503 pub scope: OfflineRenderScope,
504 #[serde(default)]
505 pub navigation_policy: OfflineRenderNavigationPolicy,
506 #[serde(default)]
508 pub release_tail_millis: u32,
509 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub playback_options: Option<PlaybackOptions>,
513}
514
515fn default_offline_render_sample_rate() -> u32 {
516 48_000
517}
518
519fn default_offline_render_channels() -> u8 {
520 2
521}
522
523impl Default for OfflineRenderRequest {
524 fn default() -> Self {
525 Self {
526 format: OfflineRenderFormat::Wav,
527 sample_rate_hz: default_offline_render_sample_rate(),
528 channels: default_offline_render_channels(),
529 provider_identity: None,
530 asset_identity: None,
531 view_id: None,
532 scope: OfflineRenderScope::FullScore,
533 navigation_policy: OfflineRenderNavigationPolicy::Authored,
534 release_tail_millis: 0,
535 playback_options: None,
536 }
537 }
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
545pub struct OfflineRenderManifest {
546 pub contract_version: u16,
547 pub playback_contract_version: u16,
548 pub format: OfflineRenderFormat,
549 pub sample_rate_hz: u32,
550 pub channels: u8,
551 #[serde(default, skip_serializing_if = "Option::is_none")]
552 pub provider_identity: Option<String>,
553 #[serde(default, skip_serializing_if = "Option::is_none")]
554 pub asset_identity: Option<String>,
555 #[serde(default, skip_serializing_if = "Option::is_none")]
556 pub view_id: Option<String>,
557 #[serde(default)]
558 pub scope: OfflineRenderScope,
559 #[serde(default)]
560 pub navigation_policy: OfflineRenderNavigationPolicy,
561 #[serde(default)]
562 pub release_tail_frames: u64,
563 #[serde(default)]
565 pub playback_options: PlaybackOptions,
566 pub duration_frames: u64,
567 pub events: Vec<PlaybackEvent>,
568 #[serde(default)]
571 pub frame_events: Vec<OfflineRenderFrameEvent>,
572 #[serde(default)]
575 pub semantic_events: Vec<OfflineRenderSemanticFrameEvent>,
576 #[serde(default)]
577 pub diagnostics: Vec<OfflineRenderDiagnostic>,
578}
579
580impl OfflineRenderManifest {
581 pub fn validate(&self) -> Result<(), crate::Error> {
585 if !(1..=OFFLINE_RENDER_CONTRACT_VERSION).contains(&self.contract_version)
586 || !(8_000..=384_000).contains(&self.sample_rate_hz)
587 || !(1..=8).contains(&self.channels)
588 || self.release_tail_frames > self.duration_frames
589 {
590 return Err(crate::Error::InvalidOfflineRenderRequest);
591 }
592
593 if self.contract_version >= 2 && self.frame_events.len() != self.events.len() {
594 return Err(crate::Error::InvalidOfflineRenderRequest);
595 }
596 let mut latest_event_end = 0_u64;
597 for (index, frame_event) in self.frame_events.iter().enumerate() {
598 let Some(expected_event) = self.events.get(index) else {
599 return Err(crate::Error::InvalidOfflineRenderRequest);
600 };
601 if frame_event.event != *expected_event
602 || frame_event
603 .start_frame
604 .checked_add(frame_event.duration_frames)
605 != Some(frame_event.end_frame)
606 || frame_event.end_frame > self.duration_frames
607 {
608 return Err(crate::Error::InvalidOfflineRenderRequest);
609 }
610 latest_event_end = latest_event_end.max(frame_event.end_frame);
611 }
612 let latest_semantic_frame = self
613 .semantic_events
614 .iter()
615 .map(|event| event.frame)
616 .max()
617 .unwrap_or(0);
618 if latest_semantic_frame > self.duration_frames {
619 return Err(crate::Error::InvalidOfflineRenderRequest);
620 }
621 if self.contract_version >= 2
622 && latest_event_end
623 .max(latest_semantic_frame)
624 .checked_add(self.release_tail_frames)
625 != Some(self.duration_frames)
626 {
627 return Err(crate::Error::InvalidOfflineRenderRequest);
628 }
629 Ok(())
630 }
631}
632
633#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
637pub struct OfflineRenderResult {
638 pub contract_version: u16,
639 pub format: OfflineRenderFormat,
640 pub sample_rate_hz: u32,
641 pub channels: u8,
642 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub provider_identity: Option<String>,
644 pub duration_frames: u64,
645 pub output_bytes: u64,
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
650pub struct PlaybackAuxSend {
651 pub source_bus: String,
652 pub destination_bus: String,
653 pub gain_db: f64,
655}
656
657#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
659pub struct PlaybackEffectRoute {
660 pub bus_id: String,
661 pub effect_id: String,
662 #[serde(default = "default_effect_enabled")]
663 pub enabled: bool,
664}
665
666fn default_effect_enabled() -> bool {
667 true
668}
669
670#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
672pub struct PlaybackRoutingConfig {
673 #[serde(default, skip_serializing_if = "Option::is_none")]
674 pub provider_identity: Option<String>,
675 #[serde(default = "default_master_bus")]
676 pub master_bus: String,
677 #[serde(default = "default_metronome_bus")]
678 pub metronome_bus: String,
679 #[serde(default)]
681 pub part_buses: BTreeMap<usize, String>,
682 #[serde(default)]
683 pub instrument_buses: BTreeMap<String, String>,
684 #[serde(default)]
685 pub aux_sends: Vec<PlaybackAuxSend>,
686 #[serde(default)]
687 pub effect_routes: Vec<PlaybackEffectRoute>,
688}
689
690fn default_master_bus() -> String {
691 "master".into()
692}
693
694fn default_metronome_bus() -> String {
695 "metronome".into()
696}
697
698impl Default for PlaybackRoutingConfig {
699 fn default() -> Self {
700 Self {
701 provider_identity: None,
702 master_bus: default_master_bus(),
703 metronome_bus: default_metronome_bus(),
704 part_buses: BTreeMap::new(),
705 instrument_buses: BTreeMap::new(),
706 aux_sends: Vec::new(),
707 effect_routes: Vec::new(),
708 }
709 }
710}
711
712#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
714pub struct PlaybackBusRoute {
715 pub part_index: usize,
716 pub channel: u8,
717 pub program: u8,
718 #[serde(default, skip_serializing_if = "Option::is_none")]
719 pub instrument_id: Option<String>,
720 pub is_metronome: bool,
721 pub bus_id: String,
722}
723
724#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
726pub struct PlaybackRoutingManifest {
727 pub contract_version: u16,
728 #[serde(default, skip_serializing_if = "Option::is_none")]
729 pub provider_identity: Option<String>,
730 pub master_bus: String,
731 pub metronome_bus: String,
732 pub routes: Vec<PlaybackBusRoute>,
733 pub aux_sends: Vec<PlaybackAuxSend>,
734 pub effect_routes: Vec<PlaybackEffectRoute>,
735}
736
737pub const PLAYBACK_ROUTING_CONTRACT_VERSION: u16 = 1;
739
740fn valid_routing_id(value: &str) -> bool {
741 !value.is_empty()
742 && value.len() <= 64
743 && value
744 .bytes()
745 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
746}
747
748fn validate_playback_routing(config: &PlaybackRoutingConfig) -> Result<(), crate::Error> {
749 if !valid_routing_id(&config.master_bus)
750 || !valid_routing_id(&config.metronome_bus)
751 || config.part_buses.values().any(|bus| !valid_routing_id(bus))
752 || config
753 .instrument_buses
754 .values()
755 .any(|bus| !valid_routing_id(bus))
756 || config.aux_sends.len() > 64
757 || config.effect_routes.len() > 128
758 {
759 return Err(crate::Error::InvalidPlaybackRouting);
760 }
761 for send in &config.aux_sends {
762 if !valid_routing_id(&send.source_bus)
763 || !valid_routing_id(&send.destination_bus)
764 || !send.gain_db.is_finite()
765 || !(-120.0..=24.0).contains(&send.gain_db)
766 {
767 return Err(crate::Error::InvalidPlaybackRouting);
768 }
769 }
770 if config
771 .effect_routes
772 .iter()
773 .any(|effect| !valid_routing_id(&effect.bus_id) || !valid_routing_id(&effect.effect_id))
774 {
775 return Err(crate::Error::InvalidPlaybackRouting);
776 }
777 Ok(())
778}
779
780pub fn build_playback_routing_manifest(
782 score: &Score,
783 playback_options: &PlaybackOptions,
784 config: &PlaybackRoutingConfig,
785) -> Result<PlaybackRoutingManifest, crate::Error> {
786 validate_playback_routing(config)?;
787 let events = to_playback_events_bounded(score, playback_options)?;
788 let mut routes = BTreeMap::new();
789 for event in events {
790 let bus_id = if event.is_metronome {
791 config.metronome_bus.clone()
792 } else if let Some(bus) = event
793 .instrument_id
794 .as_ref()
795 .and_then(|instrument_id| config.instrument_buses.get(instrument_id))
796 {
797 bus.clone()
798 } else if let Some(bus) = config.part_buses.get(&event.part_index) {
799 bus.clone()
800 } else {
801 config.master_bus.clone()
802 };
803 let route = PlaybackBusRoute {
804 part_index: event.part_index,
805 channel: event.channel,
806 program: event.program,
807 instrument_id: event.instrument_id,
808 is_metronome: event.is_metronome,
809 bus_id,
810 };
811 routes.insert(route.clone(), route);
812 }
813 Ok(PlaybackRoutingManifest {
814 contract_version: PLAYBACK_ROUTING_CONTRACT_VERSION,
815 provider_identity: config.provider_identity.clone(),
816 master_bus: config.master_bus.clone(),
817 metronome_bus: config.metronome_bus.clone(),
818 routes: routes.into_values().collect(),
819 aux_sends: config.aux_sends.clone(),
820 effect_routes: config.effect_routes.clone(),
821 })
822}
823
824pub fn build_offline_render_manifest(
826 score: &Score,
827 playback_options: &PlaybackOptions,
828 request: &OfflineRenderRequest,
829) -> Result<OfflineRenderManifest, crate::Error> {
830 if !(8_000..=384_000).contains(&request.sample_rate_hz) || !(1..=8).contains(&request.channels)
831 {
832 return Err(crate::Error::InvalidOfflineRenderRequest);
833 }
834 let resolved_score = match request.view_id.as_deref() {
835 Some(view_id) => score.resolve_view(view_id)?,
836 None => score.clone(),
837 };
838 let mut effective_options = request
839 .playback_options
840 .clone()
841 .unwrap_or_else(|| playback_options.clone());
842 if let OfflineRenderScope::MeasureRange { start, end } = request.scope {
843 if start > end {
844 return Err(crate::Error::InvalidOfflineRenderRequest);
845 }
846 effective_options.loop_region = Some((start, end));
847 }
848 let mut events = to_playback_events_bounded(&resolved_score, &effective_options)?;
849 let mut diagnostics = Vec::new();
850 let mut selection_origin_secs = 0.0;
851 if let OfflineRenderScope::Selection { ref addresses } = request.scope {
852 if addresses.is_empty() {
853 return Err(crate::Error::InvalidOfflineRenderRequest);
854 }
855 for address in addresses {
856 if !events
857 .iter()
858 .any(|event| event.source.as_ref() == Some(address))
859 {
860 diagnostics.push(OfflineRenderDiagnostic {
861 kind: OfflineRenderDiagnosticKind::UnresolvedSelectionAddress,
862 source: Some(address.clone()),
863 });
864 }
865 }
866 events.retain(|event| {
867 event
868 .source
869 .as_ref()
870 .is_some_and(|source| addresses.contains(source))
871 });
872 let origin_secs = events
873 .iter()
874 .map(|event| event.time_secs)
875 .fold(f64::INFINITY, f64::min);
876 if origin_secs.is_finite() {
877 selection_origin_secs = origin_secs;
878 for event in &mut events {
879 event.time_secs -= origin_secs;
880 }
881 }
882 if events.is_empty() {
883 diagnostics.push(OfflineRenderDiagnostic {
884 kind: OfflineRenderDiagnosticKind::EmptySelection,
885 source: None,
886 });
887 }
888 }
889 let duration_secs = events
890 .iter()
891 .map(|event| event.time_secs + event.duration_secs)
892 .fold(0.0_f64, f64::max);
893 if !duration_secs.is_finite() || duration_secs < 0.0 {
894 return Err(crate::Error::InvalidOfflineRenderRequest);
895 }
896 let release_tail_frames =
897 (f64::from(request.release_tail_millis) * f64::from(request.sample_rate_hz) / 1_000.0)
898 .ceil();
899 let duration_frames = (duration_secs * f64::from(request.sample_rate_hz)).ceil();
900 if !duration_frames.is_finite() || duration_frames > u64::MAX as f64 {
901 return Err(crate::Error::InvalidOfflineRenderRequest);
902 }
903 if !release_tail_frames.is_finite()
904 || release_tail_frames > u64::MAX as f64
905 || duration_frames > (u64::MAX as f64 - release_tail_frames)
906 {
907 return Err(crate::Error::InvalidOfflineRenderRequest);
908 }
909 let frame_events = events
910 .iter()
911 .cloned()
912 .map(|event| {
913 let start_frame = (event.time_secs * f64::from(request.sample_rate_hz)).round() as u64;
914 let end_frame = ((event.time_secs + event.duration_secs)
915 * f64::from(request.sample_rate_hz))
916 .ceil() as u64;
917 OfflineRenderFrameEvent {
918 duration_frames: end_frame.saturating_sub(start_frame),
919 event,
920 start_frame,
921 end_frame,
922 }
923 })
924 .collect::<Vec<_>>();
925 let mut semantic_events = Vec::new();
926 for event in &frame_events {
927 let source = event.event.source.clone();
928 if event.event.pedal {
929 semantic_events.push(OfflineRenderSemanticFrameEvent {
930 kind: OfflineRenderSemanticEventKind::Pedal { down: true },
931 frame: event.start_frame,
932 source: source.clone(),
933 measure: None,
934 });
935 }
936 if source.as_ref().is_some_and(|address| {
937 resolved_score
938 .parts
939 .get(address.part)
940 .and_then(|part| part.staves.get(address.staff))
941 .and_then(|staff| staff.measures.get(address.measure))
942 .and_then(|measure| measure.voices.get(address.voice))
943 .and_then(|voice| voice.get(address.note))
944 .is_some_and(|note| note.pedal_end)
945 }) {
946 semantic_events.push(OfflineRenderSemanticFrameEvent {
947 kind: OfflineRenderSemanticEventKind::Pedal { down: false },
948 frame: event.start_frame,
949 source: source.clone(),
950 measure: None,
951 });
952 }
953 for articulation in &event.event.articulations {
954 semantic_events.push(OfflineRenderSemanticFrameEvent {
955 kind: OfflineRenderSemanticEventKind::Articulation {
956 articulation: articulation.clone(),
957 },
958 frame: event.start_frame,
959 source: source.clone(),
960 measure: None,
961 });
962 }
963 if !event.event.pitch_bend_curve.is_empty() {
964 semantic_events.push(OfflineRenderSemanticFrameEvent {
965 kind: OfflineRenderSemanticEventKind::GuitarBend {
966 points: event.event.pitch_bend_curve.clone(),
967 },
968 frame: event.start_frame,
969 source,
970 measure: None,
971 });
972 }
973 }
974 append_measure_semantic_events(
975 &resolved_score,
976 &effective_options,
977 request.sample_rate_hz,
978 &request.scope,
979 selection_origin_secs,
980 &mut semantic_events,
981 );
982 let semantic_end_frame = semantic_events
986 .iter()
987 .map(|event| event.frame)
988 .max()
989 .unwrap_or(0);
990 let content_duration_frames = (duration_frames as u64).max(semantic_end_frame);
991 if release_tail_frames > 0.0 {
992 semantic_events.push(OfflineRenderSemanticFrameEvent {
993 kind: OfflineRenderSemanticEventKind::ReleaseTail,
994 frame: content_duration_frames,
995 source: None,
996 measure: None,
997 });
998 }
999 let manifest = OfflineRenderManifest {
1000 contract_version: OFFLINE_RENDER_CONTRACT_VERSION,
1001 playback_contract_version: PLAYBACK_COMPARISON_CONTRACT_VERSION,
1002 format: request.format,
1003 sample_rate_hz: request.sample_rate_hz,
1004 channels: request.channels,
1005 provider_identity: request.provider_identity.clone(),
1006 asset_identity: request.asset_identity.clone(),
1007 view_id: request.view_id.clone(),
1008 scope: request.scope.clone(),
1009 navigation_policy: request.navigation_policy,
1010 release_tail_frames: release_tail_frames as u64,
1011 playback_options: effective_options,
1012 duration_frames: content_duration_frames + release_tail_frames as u64,
1013 events,
1014 frame_events,
1015 semantic_events,
1016 diagnostics,
1017 };
1018 manifest.validate()?;
1019 Ok(manifest)
1020}
1021
1022#[derive(Clone, Copy)]
1023struct OfflineMeasureTime {
1024 measure: usize,
1025 start_secs: f64,
1026 bpm: f64,
1027 ramp_to_bpm: Option<f64>,
1028 beats: f64,
1029}
1030
1031fn offline_measure_times(
1032 score: &Score,
1033 options: &PlaybackOptions,
1034 part: usize,
1035 staff: usize,
1036 tempo_marks: &[ScoreTempoMark],
1037) -> Vec<OfflineMeasureTime> {
1038 let Some(staff_ref) = score
1039 .parts
1040 .get(part)
1041 .and_then(|part| part.staves.get(staff))
1042 else {
1043 return Vec::new();
1044 };
1045 let sequence = measure_sequence(score).into_iter().filter(|measure| {
1046 options
1047 .loop_region
1048 .is_none_or(|(start, end)| *measure >= start && *measure <= end)
1049 });
1050 let mut bpm = initial_tempo_bpm(score, options);
1051 let mut secs = 0.0;
1052 let mut result = Vec::new();
1053 for measure_index in sequence {
1054 if staff_ref.measures.get(measure_index).is_none() {
1055 continue;
1056 }
1057 let mark = tempo_marks.get(measure_index).copied().unwrap_or_default();
1058 if let Some(tempo) = mark.bpm.and_then(|tempo| written_tempo_bpm(options, tempo)) {
1059 bpm = tempo;
1060 }
1061 let beats = score.playback_measure_beats(measure_index);
1062 let ramp_to_bpm = written_tempo_ramp_bpm(options, mark.ramp_to_bpm);
1063 result.push(OfflineMeasureTime {
1064 measure: measure_index,
1065 start_secs: secs,
1066 bpm,
1067 ramp_to_bpm,
1068 beats,
1069 });
1070 secs += tempo_ramp_seconds(bpm, ramp_to_bpm, beats, beats);
1071 if let Some(end_bpm) = ramp_to_bpm {
1072 bpm = end_bpm;
1073 }
1074 }
1075 result
1076}
1077
1078fn append_measure_semantic_events(
1079 score: &Score,
1080 options: &PlaybackOptions,
1081 sample_rate_hz: u32,
1082 scope: &OfflineRenderScope,
1083 origin_secs: f64,
1084 events: &mut Vec<OfflineRenderSemanticFrameEvent>,
1085) {
1086 let tempo_marks = score_tempo_marks(score);
1087 for (part_index, part) in score.parts.iter().enumerate() {
1088 let timeline = offline_measure_times(score, options, part_index, 0, &tempo_marks);
1089 let Some(staff) = part.staves.first() else {
1090 continue;
1091 };
1092 let mut tick_starts = Vec::with_capacity(staff.measures.len());
1093 let mut tick = 0u64;
1094 for measure_index in 0..staff.measures.len() {
1095 tick_starts.push(tick);
1096 let beats = score.playback_measure_beats(measure_index);
1097 tick = tick.saturating_add((beats * 480.0).round() as u64);
1098 }
1099 for time in timeline {
1100 if let OfflineRenderScope::Selection { addresses } = scope
1101 && !addresses
1102 .iter()
1103 .any(|address| address.part == part_index && address.measure == time.measure)
1104 {
1105 continue;
1106 }
1107 let address = OfflineRenderMeasureAddress {
1108 part: part_index,
1109 staff: 0,
1110 measure: time.measure,
1111 };
1112 let frame = ((time.start_secs - origin_secs).max(0.0) * f64::from(sample_rate_hz))
1113 .round() as u64;
1114 let measure = &staff.measures[time.measure];
1115 let mark = tempo_marks.get(time.measure).copied().unwrap_or_default();
1116 if part_index == 0
1117 && options.bpm_override.is_none()
1118 && (mark.bpm.is_some() || mark.ramp_to_bpm.is_some())
1119 {
1120 events.push(OfflineRenderSemanticFrameEvent {
1121 kind: OfflineRenderSemanticEventKind::Tempo {
1122 bpm: semantic_tempo_bpm(time.bpm),
1123 ramp_to_bpm: time.ramp_to_bpm.map(semantic_tempo_bpm),
1124 },
1125 frame,
1126 source: None,
1127 measure: Some(address.clone()),
1128 });
1129 }
1130 if part_index == 0
1131 && let Some(marker) = &measure.navigation
1132 {
1133 events.push(OfflineRenderSemanticFrameEvent {
1134 kind: OfflineRenderSemanticEventKind::Navigation {
1135 marker: marker.clone(),
1136 },
1137 frame,
1138 source: None,
1139 measure: Some(address.clone()),
1140 });
1141 }
1142 if let Some(instrument) = &measure.instrument_change {
1143 events.push(OfflineRenderSemanticFrameEvent {
1144 kind: OfflineRenderSemanticEventKind::InstrumentChange {
1145 instrument_id: instrument.id.clone(),
1146 channel: instrument.midi_channel,
1147 program: instrument.midi_program,
1148 transpose_semitones: instrument.transpose_semitones,
1149 },
1150 frame,
1151 source: None,
1152 measure: Some(address.clone()),
1153 });
1154 }
1155 let start_tick = tick_starts[time.measure];
1156 let end_tick = start_tick.saturating_add((time.beats * 480.0).round() as u64);
1157 for control in &part.midi_control_changes {
1158 if control.tick < start_tick || control.tick >= end_tick {
1159 continue;
1160 }
1161 let local_beats = (control.tick - start_tick) as f64 / 480.0;
1162 let secs = time.start_secs
1163 + tempo_ramp_seconds(time.bpm, time.ramp_to_bpm, time.beats, local_beats);
1164 events.push(OfflineRenderSemanticFrameEvent {
1165 kind: OfflineRenderSemanticEventKind::Controller {
1166 channel: control.channel,
1167 controller: control.controller,
1168 value: control.value,
1169 },
1170 frame: ((secs - origin_secs).max(0.0) * f64::from(sample_rate_hz)).round()
1171 as u64,
1172 source: None,
1173 measure: Some(address.clone()),
1174 });
1175 }
1176 for bend in &part.midi_pitch_bends {
1177 if bend.tick < start_tick || bend.tick >= end_tick {
1178 continue;
1179 }
1180 let local_beats = (bend.tick - start_tick) as f64 / 480.0;
1181 let secs = time.start_secs
1182 + tempo_ramp_seconds(time.bpm, time.ramp_to_bpm, time.beats, local_beats);
1183 events.push(OfflineRenderSemanticFrameEvent {
1184 kind: OfflineRenderSemanticEventKind::PitchBend {
1185 channel: bend.channel,
1186 value: bend.value,
1187 },
1188 frame: ((secs - origin_secs).max(0.0) * f64::from(sample_rate_hz)).round()
1189 as u64,
1190 source: None,
1191 measure: Some(address.clone()),
1192 });
1193 }
1194 for program in &part.midi_program_changes {
1195 if program.tick < start_tick || program.tick >= end_tick {
1196 continue;
1197 }
1198 let local_beats = (program.tick - start_tick) as f64 / 480.0;
1199 let secs = time.start_secs
1200 + tempo_ramp_seconds(time.bpm, time.ramp_to_bpm, time.beats, local_beats);
1201 events.push(OfflineRenderSemanticFrameEvent {
1202 kind: OfflineRenderSemanticEventKind::ProgramChange {
1203 channel: program.channel,
1204 program: program.program,
1205 },
1206 frame: ((secs - origin_secs).max(0.0) * f64::from(sample_rate_hz)).round()
1207 as u64,
1208 source: None,
1209 measure: Some(address.clone()),
1210 });
1211 }
1212 for aftertouch in &part.midi_aftertouch {
1213 if aftertouch.tick < start_tick || aftertouch.tick >= end_tick {
1214 continue;
1215 }
1216 let local_beats = (aftertouch.tick - start_tick) as f64 / 480.0;
1217 let secs = time.start_secs
1218 + tempo_ramp_seconds(time.bpm, time.ramp_to_bpm, time.beats, local_beats);
1219 events.push(OfflineRenderSemanticFrameEvent {
1220 kind: OfflineRenderSemanticEventKind::Aftertouch {
1221 channel: aftertouch.channel,
1222 key: aftertouch.key,
1223 value: aftertouch.value,
1224 },
1225 frame: ((secs - origin_secs).max(0.0) * f64::from(sample_rate_hz)).round()
1226 as u64,
1227 source: None,
1228 measure: Some(address.clone()),
1229 });
1230 }
1231 }
1232 }
1233 events.sort_by_key(|event| event.frame);
1234}
1235
1236#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1238pub struct PlaybackTimingTolerance {
1239 pub start_secs: f64,
1240 pub duration_secs: f64,
1241}
1242
1243impl Default for PlaybackTimingTolerance {
1244 fn default() -> Self {
1245 Self {
1246 start_secs: 0.005,
1247 duration_secs: 0.005,
1248 }
1249 }
1250}
1251
1252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1254pub enum PlaybackTimingMismatch {
1255 EventCount { expected: usize, actual: usize },
1256 EventIdentity { index: usize },
1257 StartTime { index: usize, error_secs: f64 },
1258 Duration { index: usize, error_secs: f64 },
1259}
1260
1261#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1263pub struct PlaybackTimingReport {
1264 pub contract_version: u16,
1265 pub expected_events: usize,
1266 pub actual_events: usize,
1267 pub matched_events: usize,
1268 pub max_start_error_secs: f64,
1269 pub max_duration_error_secs: f64,
1270 pub within_tolerance: bool,
1271 pub mismatches: Vec<PlaybackTimingMismatch>,
1272}
1273
1274pub fn compare_playback_timing(
1279 expected: &[PlaybackEvent],
1280 actual: &[PlaybackEvent],
1281 tolerance: &PlaybackTimingTolerance,
1282) -> Result<PlaybackTimingReport, crate::Error> {
1283 if !tolerance.start_secs.is_finite()
1284 || tolerance.start_secs < 0.0
1285 || !tolerance.duration_secs.is_finite()
1286 || tolerance.duration_secs < 0.0
1287 {
1288 return Err(crate::Error::InvalidPlaybackComparison);
1289 }
1290 if expected.len() > MAX_PLAYBACK_COMPARISON_EVENTS {
1291 return Err(crate::Error::PlaybackComparisonTooLarge(expected.len()));
1292 }
1293 if actual.len() > MAX_PLAYBACK_COMPARISON_EVENTS {
1294 return Err(crate::Error::PlaybackComparisonTooLarge(actual.len()));
1295 }
1296 let mut mismatches = Vec::new();
1297 if expected.len() != actual.len() {
1298 mismatches.push(PlaybackTimingMismatch::EventCount {
1299 expected: expected.len(),
1300 actual: actual.len(),
1301 });
1302 }
1303 let mut matched_events = 0;
1304 let mut max_start_error_secs: f64 = 0.0;
1305 let mut max_duration_error_secs: f64 = 0.0;
1306 for (index, (expected_event, actual_event)) in expected.iter().zip(actual).enumerate() {
1307 let source_identity_matches = match (&expected_event.source, &actual_event.source) {
1308 (Some(expected), Some(actual)) => expected == actual,
1309 _ => expected_event.address == actual_event.address,
1310 };
1311 let identity_matches = source_identity_matches
1312 && expected_event.pitch_midi_cents == actual_event.pitch_midi_cents
1313 && expected_event.velocity == actual_event.velocity
1314 && expected_event.part_index == actual_event.part_index
1315 && expected_event.channel == actual_event.channel
1316 && expected_event.program == actual_event.program
1317 && expected_event.instrument_id == actual_event.instrument_id
1318 && expected_event.pitch_bend_curve == actual_event.pitch_bend_curve
1319 && expected_event.post_note_pause_beats == actual_event.post_note_pause_beats
1320 && expected_event.articulations == actual_event.articulations
1321 && expected_event.chord_symbol == actual_event.chord_symbol
1322 && expected_event.guitar_technique == actual_event.guitar_technique
1323 && expected_event.is_metronome == actual_event.is_metronome;
1324 let start_error_secs = (expected_event.time_secs - actual_event.time_secs).abs();
1325 let duration_error_secs = (expected_event.duration_secs - actual_event.duration_secs).abs();
1326 if start_error_secs.is_finite() {
1327 max_start_error_secs = max_start_error_secs.max(start_error_secs);
1328 }
1329 if duration_error_secs.is_finite() {
1330 max_duration_error_secs = max_duration_error_secs.max(duration_error_secs);
1331 }
1332 let start_matches =
1333 start_error_secs.is_finite() && start_error_secs <= tolerance.start_secs;
1334 let duration_matches =
1335 duration_error_secs.is_finite() && duration_error_secs <= tolerance.duration_secs;
1336 if identity_matches && start_matches && duration_matches {
1337 matched_events += 1;
1338 continue;
1339 }
1340 if mismatches.len() < MAX_PLAYBACK_MISMATCHES {
1341 if !identity_matches {
1342 mismatches.push(PlaybackTimingMismatch::EventIdentity { index });
1343 }
1344 if !start_matches && mismatches.len() < MAX_PLAYBACK_MISMATCHES {
1345 mismatches.push(PlaybackTimingMismatch::StartTime {
1346 index,
1347 error_secs: start_error_secs,
1348 });
1349 }
1350 if !duration_matches && mismatches.len() < MAX_PLAYBACK_MISMATCHES {
1351 mismatches.push(PlaybackTimingMismatch::Duration {
1352 index,
1353 error_secs: duration_error_secs,
1354 });
1355 }
1356 }
1357 }
1358 Ok(PlaybackTimingReport {
1359 contract_version: PLAYBACK_COMPARISON_CONTRACT_VERSION,
1360 expected_events: expected.len(),
1361 actual_events: actual.len(),
1362 matched_events,
1363 max_start_error_secs,
1364 max_duration_error_secs,
1365 within_tolerance: mismatches.is_empty(),
1366 mismatches,
1367 })
1368}
1369
1370pub const PLAYBACK_TIMING_CORPUS_CONTRACT_VERSION: u16 = 1;
1372const MAX_PLAYBACK_TIMING_CORPUS_CASES: usize = 256;
1373
1374#[derive(Debug, Clone, Serialize, Deserialize)]
1376pub struct PlaybackTimingCase {
1377 pub id: String,
1378 pub score: Score,
1379 #[serde(default)]
1380 pub options: PlaybackOptions,
1381 pub expected_events: Vec<PlaybackEvent>,
1382 #[serde(default)]
1383 pub tolerance: PlaybackTimingTolerance,
1384}
1385
1386#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1388pub struct PlaybackTimingCaseReport {
1389 pub id: String,
1390 pub report: PlaybackTimingReport,
1391}
1392
1393#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1395pub struct PlaybackTimingCorpusReport {
1396 pub contract_version: u16,
1397 pub total_cases: usize,
1398 pub passed_cases: usize,
1399 pub cases: Vec<PlaybackTimingCaseReport>,
1400}
1401
1402pub fn evaluate_playback_timing_corpus(
1407 cases: &[PlaybackTimingCase],
1408) -> Result<PlaybackTimingCorpusReport, crate::Error> {
1409 if cases.is_empty() || cases.len() > MAX_PLAYBACK_TIMING_CORPUS_CASES {
1410 return Err(crate::Error::InvalidPlaybackTimingCorpus);
1411 }
1412 let mut ids = BTreeSet::new();
1413 let mut reports = Vec::with_capacity(cases.len());
1414 let mut passed_cases = 0;
1415 for case in cases {
1416 if !valid_routing_id(&case.id) || !ids.insert(case.id.clone()) {
1417 return Err(crate::Error::InvalidPlaybackTimingCorpus);
1418 }
1419 let actual = to_playback_events_bounded(&case.score, &case.options)?;
1420 let report = compare_playback_timing(&case.expected_events, &actual, &case.tolerance)?;
1421 if report.within_tolerance {
1422 passed_cases += 1;
1423 }
1424 reports.push(PlaybackTimingCaseReport {
1425 id: case.id.clone(),
1426 report,
1427 });
1428 }
1429 Ok(PlaybackTimingCorpusReport {
1430 contract_version: PLAYBACK_TIMING_CORPUS_CONTRACT_VERSION,
1431 total_cases: cases.len(),
1432 passed_cases,
1433 cases: reports,
1434 })
1435}
1436
1437pub const TAB_PERFORMANCE_CONTRACT_VERSION: u16 = 4;
1439
1440#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1441pub struct TablaturePerformanceEvent {
1442 pub playback: PlaybackEvent,
1443 pub string: u8,
1444 pub fret: u8,
1445 #[serde(default)]
1447 pub technique: Option<GuitarTechnique>,
1448 #[serde(default)]
1450 pub bend_alter_cents: Option<i16>,
1451 #[serde(default)]
1454 pub bend_curve: Vec<GuitarBendPoint>,
1455 pub expected_pitch_midi_cents: i32,
1456 pub pitch_error_cents: i32,
1457}
1458
1459#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1460pub enum TablaturePerformanceDiagnostic {
1461 NoTablatureStaff {
1462 address: String,
1463 },
1464 MissingPosition {
1465 address: String,
1466 pitch_index: usize,
1467 },
1468 StringOutOfRange {
1469 address: String,
1470 string: u8,
1471 lines: u8,
1472 },
1473 TuningUnavailable {
1474 address: String,
1475 string: u8,
1476 },
1477 PitchMismatch {
1478 address: String,
1479 pitch_index: usize,
1480 error_cents: i32,
1481 },
1482}
1483
1484#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1485pub struct TablaturePerformanceReport {
1486 pub contract_version: u16,
1487 pub events: Vec<TablaturePerformanceEvent>,
1488 pub diagnostics: Vec<TablaturePerformanceDiagnostic>,
1489}
1490
1491pub const TAB_ROUND_TRIP_CONTRACT_VERSION: u16 = 1;
1493
1494#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1495pub struct TablatureRoundTripReport {
1496 pub contract_version: u16,
1497 pub checked_notes: usize,
1498 pub positioned_notes: usize,
1499 pub equivalent: bool,
1500 pub diagnostics: Vec<TablatureRoundTripDiagnostic>,
1501}
1502
1503#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1504pub enum TablatureRoundTripDiagnostic {
1505 StructureMismatch {
1506 address: String,
1507 },
1508 TablatureConfigMismatch {
1509 address: String,
1510 },
1511 PositionMismatch {
1512 address: String,
1513 pitch_index: usize,
1514 expected: Option<TabPosition>,
1515 actual: Option<TabPosition>,
1516 },
1517}
1518
1519pub fn tablature_round_trip_report(
1524 score: &Score,
1525) -> Result<TablatureRoundTripReport, crate::Error> {
1526 let encoded = serde_json::to_string(score)
1527 .map_err(|error| crate::Error::TabRoundTripSerialization(error.to_string()))?;
1528 let restored: Score = serde_json::from_str(&encoded)
1529 .map_err(|error| crate::Error::TabRoundTripSerialization(error.to_string()))?;
1530 let mut checked_notes = 0;
1531 let mut positioned_notes = 0;
1532 let mut diagnostics = Vec::new();
1533 for (part_index, part) in score.parts.iter().enumerate() {
1534 let Some(restored_part) = restored.parts.get(part_index) else {
1535 diagnostics.push(TablatureRoundTripDiagnostic::StructureMismatch {
1536 address: format!("{part_index}"),
1537 });
1538 continue;
1539 };
1540 for (staff_index, staff) in part.staves.iter().enumerate() {
1541 let address = format!("{part_index}:{staff_index}");
1542 let Some(restored_staff) = restored_part.staves.get(staff_index) else {
1543 diagnostics.push(TablatureRoundTripDiagnostic::StructureMismatch { address });
1544 continue;
1545 };
1546 if staff.tablature != restored_staff.tablature {
1547 diagnostics.push(TablatureRoundTripDiagnostic::TablatureConfigMismatch {
1548 address: address.clone(),
1549 });
1550 }
1551 for (measure_index, measure) in staff.measures.iter().enumerate() {
1552 let Some(restored_measure) = restored_staff.measures.get(measure_index) else {
1553 diagnostics.push(TablatureRoundTripDiagnostic::StructureMismatch {
1554 address: format!("{address}:{measure_index}"),
1555 });
1556 continue;
1557 };
1558 for (voice_index, voice) in measure.voices.iter().enumerate() {
1559 let Some(restored_voice) = restored_measure.voices.get(voice_index) else {
1560 diagnostics.push(TablatureRoundTripDiagnostic::StructureMismatch {
1561 address: format!("{address}:{measure_index}:{voice_index}"),
1562 });
1563 continue;
1564 };
1565 for (note_index, note) in voice.iter().enumerate() {
1566 checked_notes += 1;
1567 if !note.tab_positions.is_empty() || note.tab_position.is_some() {
1568 positioned_notes += 1;
1569 }
1570 let note_address =
1571 format!("{address}:{measure_index}:{voice_index}:{note_index}");
1572 let Some(restored_note) = restored_voice.get(note_index) else {
1573 diagnostics.push(TablatureRoundTripDiagnostic::StructureMismatch {
1574 address: note_address,
1575 });
1576 continue;
1577 };
1578 let pitch_count = note.pitches.len().max(note.tab_positions.len()).max(1);
1579 for pitch_index in 0..pitch_count {
1580 let expected = note
1581 .tab_positions
1582 .get(pitch_index)
1583 .cloned()
1584 .or_else(|| note.tab_position.clone());
1585 let actual = restored_note
1586 .tab_positions
1587 .get(pitch_index)
1588 .cloned()
1589 .or_else(|| restored_note.tab_position.clone());
1590 if expected != actual {
1591 diagnostics.push(TablatureRoundTripDiagnostic::PositionMismatch {
1592 address: note_address.clone(),
1593 pitch_index,
1594 expected,
1595 actual,
1596 });
1597 }
1598 }
1599 }
1600 }
1601 }
1602 }
1603 }
1604 Ok(TablatureRoundTripReport {
1605 contract_version: TAB_ROUND_TRIP_CONTRACT_VERSION,
1606 checked_notes,
1607 positioned_notes,
1608 equivalent: diagnostics.is_empty(),
1609 diagnostics,
1610 })
1611}
1612
1613pub fn project_tablature_performance(
1619 score: &Score,
1620 options: &PlaybackOptions,
1621) -> Result<TablaturePerformanceReport, crate::Error> {
1622 let playback = to_playback_events(score, options);
1623 if playback.len() > MAX_TAB_PERFORMANCE_EVENTS {
1624 return Err(crate::Error::TabPerformanceTooLarge(playback.len()));
1625 }
1626 let mut events = Vec::new();
1627 let mut diagnostics = Vec::new();
1628 for event in playback {
1629 let Some(address) = event.address.as_deref() else {
1630 continue;
1631 };
1632 let Some((part_index, staff_index, measure_index, voice_index, note_index)) =
1633 parse_playback_address(address)
1634 else {
1635 continue;
1636 };
1637 let Some(staff) = score
1638 .parts
1639 .get(part_index)
1640 .and_then(|part| part.staves.get(staff_index))
1641 else {
1642 continue;
1643 };
1644 let Some(note) = staff
1645 .measures
1646 .get(measure_index)
1647 .and_then(|measure| measure.voices.get(voice_index))
1648 .and_then(|voice| voice.get(note_index))
1649 else {
1650 continue;
1651 };
1652 let Some(tab) = staff.tablature_at(measure_index) else {
1653 push_tab_diagnostic(
1654 &mut diagnostics,
1655 TablaturePerformanceDiagnostic::NoTablatureStaff {
1656 address: address.into(),
1657 },
1658 );
1659 continue;
1660 };
1661 let transpose_cents = if event.channel == 9 {
1662 0
1663 } else {
1664 i32::from(staff.transpose_semitones) * 100
1665 };
1666 let written_pitch_cents = event.pitch_midi_cents - transpose_cents;
1667 let pitch_index = note
1668 .pitches
1669 .iter()
1670 .position(|pitch| pitch.to_midi_cents() == written_pitch_cents)
1671 .unwrap_or(0);
1672 let position = note
1673 .tab_positions
1674 .get(pitch_index)
1675 .or(note.tab_position.as_ref());
1676 let Some(position) = position else {
1677 push_tab_diagnostic(
1678 &mut diagnostics,
1679 TablaturePerformanceDiagnostic::MissingPosition {
1680 address: address.into(),
1681 pitch_index,
1682 },
1683 );
1684 continue;
1685 };
1686 if position.string == 0 || position.string > tab.lines {
1687 push_tab_diagnostic(
1688 &mut diagnostics,
1689 TablaturePerformanceDiagnostic::StringOutOfRange {
1690 address: address.into(),
1691 string: position.string,
1692 lines: tab.lines,
1693 },
1694 );
1695 continue;
1696 }
1697 let Some(tuning) = tab.tuning_midi.get(usize::from(position.string - 1)) else {
1698 push_tab_diagnostic(
1699 &mut diagnostics,
1700 TablaturePerformanceDiagnostic::TuningUnavailable {
1701 address: address.into(),
1702 string: position.string,
1703 },
1704 );
1705 continue;
1706 };
1707 let expected_pitch_midi_cents =
1708 tuning.saturating_add(i16::from(position.fret) + i16::from(tab.capo)) as i32 * 100;
1709 let pitch_error_cents = event.pitch_midi_cents - expected_pitch_midi_cents;
1710 if pitch_error_cents != 0 {
1711 push_tab_diagnostic(
1712 &mut diagnostics,
1713 TablaturePerformanceDiagnostic::PitchMismatch {
1714 address: address.into(),
1715 pitch_index,
1716 error_cents: pitch_error_cents,
1717 },
1718 );
1719 }
1720 events.push(TablaturePerformanceEvent {
1721 playback: event,
1722 string: position.string,
1723 fret: position.fret,
1724 technique: note.guitar_technique.clone(),
1725 bend_alter_cents: note.guitar_bend_alter_cents,
1726 bend_curve: note.guitar_bend_curve.clone(),
1727 expected_pitch_midi_cents,
1728 pitch_error_cents,
1729 });
1730 }
1731 Ok(TablaturePerformanceReport {
1732 contract_version: TAB_PERFORMANCE_CONTRACT_VERSION,
1733 events,
1734 diagnostics,
1735 })
1736}
1737
1738fn parse_playback_address(address: &str) -> Option<(usize, usize, usize, usize, usize)> {
1739 let mut fields = address.split(':').map(|field| field.parse::<usize>().ok());
1740 Some((
1741 fields.next()??,
1742 fields.next()??,
1743 fields.next()??,
1744 fields.next()??,
1745 fields.next()??,
1746 ))
1747}
1748
1749fn push_tab_diagnostic(
1750 diagnostics: &mut Vec<TablaturePerformanceDiagnostic>,
1751 diagnostic: TablaturePerformanceDiagnostic,
1752) {
1753 if diagnostics.len() < MAX_TAB_PERFORMANCE_DIAGNOSTICS {
1754 diagnostics.push(diagnostic);
1755 }
1756}
1757
1758#[derive(Debug, Clone, Default)]
1765pub struct DynamicTimeline {
1766 position_beats: Vec<f64>,
1768 changes: Vec<(f64, u8, u8)>,
1770 ramps: Vec<(f64, f64, u8, u8)>,
1772}
1773
1774#[derive(Clone, Copy)]
1776enum DynamicMark {
1777 Marking(crate::Dynamic),
1778 Level(u8),
1779}
1780
1781impl DynamicTimeline {
1782 pub const DEFAULT_VELOCITY: u8 = 64;
1784 const HAIRPIN_STEP: i16 = 24;
1786
1787 pub fn for_staff(staff: &crate::Staff, order: &[usize]) -> Self {
1790 let mut position_beats = Vec::with_capacity(order.len());
1791 let mut marks: Vec<(f64, DynamicMark)> = Vec::new();
1792 let mut hairpins: Vec<(f64, f64, bool)> = Vec::new();
1794 let mut open_hairpins: [Option<(f64, bool)>; 4] = [None; 4];
1795 let mut elapsed = 0.0;
1796 for &measure_index in order {
1797 position_beats.push(elapsed);
1798 let Some(measure) = staff.measures.get(measure_index) else {
1799 continue;
1800 };
1801 let mut length: f64 = 0.0;
1802 for (voice_index, voice) in measure.voices.iter().enumerate() {
1803 let mut beats = 0.0;
1804 for note in voice {
1805 let onset = elapsed + beats;
1806 if let Some(dynamic) = note.dynamic
1807 && !note.is_rest
1808 {
1809 marks.push((onset, DynamicMark::Marking(dynamic)));
1810 }
1811 let end = onset + note.beats();
1812 let ends_earlier = note.hairpin_end && open_hairpins[voice_index].is_some();
1814 if ends_earlier && let Some((start, up)) = open_hairpins[voice_index].take() {
1815 hairpins.push((start, end, up));
1816 }
1817 if let Some(kind) = note.hairpin_start {
1818 open_hairpins[voice_index] =
1819 Some((onset, kind == crate::HairpinKind::Crescendo));
1820 }
1821 if note.hairpin_end
1822 && !ends_earlier
1823 && let Some((start, up)) = open_hairpins[voice_index].take()
1824 {
1825 hairpins.push((start, end, up));
1826 }
1827 beats += note.beats();
1828 }
1829 length = length.max(beats);
1830 }
1831 elapsed += length;
1832 }
1833 marks.sort_by(|a, b| a.0.total_cmp(&b.0));
1834 let written = Self::resolve(&marks);
1835 let mut ramps = Vec::new();
1836 for (start, end, up) in hairpins {
1837 if end <= start {
1838 continue;
1839 }
1840 let from = Self::level_at(&written, start);
1841 let next = written
1843 .iter()
1844 .find(|&&(time, _, _)| time >= end - 1e-9)
1845 .filter(|&&(time, _, _)| time <= end + 1.0)
1846 .map(|&(_, attack, _)| attack)
1847 .filter(|&attack| if up { attack > from } else { attack < from });
1848 let to = next.unwrap_or_else(|| {
1849 let step = if up {
1850 Self::HAIRPIN_STEP
1851 } else {
1852 -Self::HAIRPIN_STEP
1853 };
1854 (i16::from(from) + step).clamp(1, 127) as u8
1855 });
1856 if next.is_none() {
1857 marks.push((end, DynamicMark::Level(to)));
1858 }
1859 ramps.push((start, end, from, to));
1860 }
1861 marks.sort_by(|a, b| a.0.total_cmp(&b.0));
1862 Self {
1863 position_beats,
1864 changes: Self::resolve(&marks),
1865 ramps,
1866 }
1867 }
1868
1869 fn resolve(marks: &[(f64, DynamicMark)]) -> Vec<(f64, u8, u8)> {
1870 let mut level = Self::DEFAULT_VELOCITY;
1871 marks
1872 .iter()
1873 .map(|&(time, mark)| match mark {
1874 DynamicMark::Marking(dynamic) => {
1875 if let Some(next) = dynamic.sustained_level() {
1876 level = next.to_velocity();
1877 }
1878 (time, dynamic.to_velocity(), level)
1879 }
1880 DynamicMark::Level(velocity) => {
1881 level = velocity;
1882 (time, velocity, level)
1883 }
1884 })
1885 .collect()
1886 }
1887
1888 fn level_at(changes: &[(f64, u8, u8)], time: f64) -> u8 {
1889 let before = changes.partition_point(|&(t, _, _)| t <= time + 1e-9);
1890 before
1891 .checked_sub(1)
1892 .map_or(Self::DEFAULT_VELOCITY, |index| changes[index].2)
1893 }
1894
1895 pub fn velocity(&self, position: usize, beats: f64, own: Option<crate::Dynamic>) -> u8 {
1899 if let Some(dynamic) = own {
1900 return dynamic.to_velocity();
1901 }
1902 let time = self.position_beats.get(position).copied().unwrap_or(0.0) + beats;
1903 let before = self.changes.partition_point(|&(t, _, _)| t <= time + 1e-9);
1904 let current = before.checked_sub(1).map(|index| self.changes[index]);
1905 if let Some((t, attack, _)) = current
1906 && (t - time).abs() <= 1e-9
1907 {
1908 return attack;
1909 }
1910 if let Some(&(start, end, from, to)) = self
1911 .ramps
1912 .iter()
1913 .rev()
1914 .find(|&&(start, end, _, _)| start <= time + 1e-9 && time < end - 1e-9)
1915 {
1916 let fraction = ((time - start) / (end - start)).clamp(0.0, 1.0);
1917 let value = f64::from(from) + (f64::from(to) - f64::from(from)) * fraction;
1918 return value.round().clamp(1.0, 127.0) as u8;
1919 }
1920 current.map_or(Self::DEFAULT_VELOCITY, |(_, _, level)| level)
1921 }
1922}
1923
1924pub fn to_playback_events(score: &Score, options: &PlaybackOptions) -> Vec<PlaybackEvent> {
1930 let materialized = score.with_legacy_spanner_flags();
1932 let score: &Score = &materialized;
1933 let bpm = initial_tempo_bpm(score, options);
1934 let tempo_marks = score_tempo_marks(score);
1935 let measure_lengths: Vec<f64> = (0..tempo_marks.len())
1936 .map(|index| score.playback_measure_beats(index))
1937 .collect();
1938 let mut measure_tick_starts = Vec::with_capacity(measure_lengths.len());
1939 let mut next_tick = 0_u64;
1940 for beats in &measure_lengths {
1941 measure_tick_starts.push(next_tick);
1942 next_tick = next_tick.saturating_add((beats * 480.0).round() as u64);
1943 }
1944 let swing_ratio = options
1945 .swing
1946 .filter(|ratio| ratio.is_finite() && *ratio > 0.5 && *ratio < 1.0);
1947 let full_seq = measure_sequence(score);
1948 let seq: Vec<usize> = if let Some((lo, hi)) = options.loop_region {
1949 full_seq
1950 .into_iter()
1951 .filter(|&idx| idx >= lo && idx <= hi)
1952 .collect()
1953 } else {
1954 full_seq
1955 };
1956 let mut events: Vec<PlaybackEvent> = Vec::new();
1957
1958 for (part_index, part) in score.parts.iter().enumerate() {
1959 if options.muted_parts.contains(&part_index) {
1960 continue;
1961 }
1962 for (staff_index, staff) in part.staves.iter().enumerate() {
1963 let dynamics = DynamicTimeline::for_staff(staff, &seq);
1964 for voice_idx in 0..4usize {
1965 let mut time_beats = 0.0f64;
1966 let mut time_secs_cursor = 0.0f64;
1967 let mut current_bpm = bpm;
1968 let mut prior_note_events = 0..0;
1969 for (seq_position, &idx) in seq.iter().enumerate() {
1970 let measure = match staff.measures.get(idx) {
1971 Some(m) => m,
1972 None => continue,
1973 };
1974 let instrument = measure
1975 .instrument_change
1976 .as_ref()
1977 .or(part.instrument.as_ref());
1978 let channel = instrument.map_or(part.midi_channel, |value| value.midi_channel);
1979 let default_program =
1980 instrument.map_or(part.midi_program, |value| value.midi_program);
1981 let instrument_id = instrument.map(|value| value.id.clone());
1982 let mark = tempo_marks.get(idx).copied().unwrap_or_default();
1983 if let Some(b) = mark.bpm.and_then(|tempo| written_tempo_bpm(options, tempo)) {
1984 current_bpm = b;
1985 }
1986 let measure_beats = measure_lengths[idx];
1987 let grid_offset_beats = if idx == 0
1988 && measure_beats
1989 < staff
1990 .meter_at(idx, &score.settings.time_signature)
1991 .total_beats()
1992 {
1993 (staff
1994 .meter_at(idx, &score.settings.time_signature)
1995 .total_beats()
1996 - measure_beats)
1997 .max(0.0)
1998 } else {
1999 0.0
2000 };
2001 let ramp_end_bpm = written_tempo_ramp_bpm(options, mark.ramp_to_bpm);
2002 let measure_start_beats = time_beats;
2003 let measure_start_secs = time_secs_cursor;
2004 let mut local_beats = 0.0f64;
2005 let mut written_beats = 0.0f64;
2007 let mut swing_second = false;
2008 for (note_index, note) in measure.voices[voice_idx].iter().enumerate() {
2009 if note.is_grace || note.is_cue {
2010 continue;
2011 }
2012 let onset_written_beats = written_beats;
2013 written_beats += note.beats();
2014 let dur = if let Some(ratio) = swing_ratio {
2015 if swing_second {
2016 swing_second = false;
2017 (1.0 - ratio) * note.beats() * 2.0
2018 } else if starts_swing_pair(
2019 &measure.voices[voice_idx],
2020 note_index,
2021 onset_written_beats,
2022 grid_offset_beats,
2023 measure_beats,
2024 &options.swing_unit,
2025 ) {
2026 swing_second = true;
2027 ratio * note.beats() * 2.0
2028 } else {
2029 note.beats()
2030 }
2031 } else {
2032 note.beats()
2033 };
2034 if !note.is_rest {
2035 let source_tick = measure_tick_starts[idx]
2039 .saturating_add((onset_written_beats * 480.0).round() as u64);
2040 let mut program = default_program;
2041 let mut latest_change_tick = 0_u64;
2042 for change in &part.midi_program_changes {
2043 if change.channel == channel
2044 && change.tick <= source_tick
2045 && change.tick >= latest_change_tick
2046 {
2047 program = change.program;
2048 latest_change_tick = change.tick;
2049 }
2050 }
2051 let mut velocity =
2052 dynamics.velocity(seq_position, onset_written_beats, note.dynamic);
2053 let mut sounding_dur = dur;
2054 for art in ¬e.articulations {
2055 match art {
2056 Articulation::Staccato | Articulation::Staccatissimo => {
2057 sounding_dur *= 0.5;
2058 }
2059 Articulation::Accent | Articulation::Marcato => {
2060 velocity = velocity.saturating_add(20).min(127);
2061 }
2062 Articulation::Fermata => {
2063 sounding_dur *= options.fermata_multiplier;
2064 }
2065 _ => {}
2066 }
2067 }
2068 let pedal = note.pedal_start;
2069 let fermata_hold_beats = if options.fermata_hold_beats.is_finite() {
2070 options.fermata_hold_beats.max(0.0)
2071 } else {
2072 0.0
2073 };
2074 let post_note_pause_beats =
2075 note.articulations
2076 .iter()
2077 .fold(0.0f64, |pause, articulation| {
2078 pause.max(match articulation {
2079 Articulation::BreathMark => 0.25,
2080 Articulation::Caesura => 0.5,
2081 Articulation::Fermata => fermata_hold_beats,
2082 _ => 0.0,
2083 })
2084 });
2085 let transpose = if channel == 9 {
2086 0i8
2087 } else {
2088 staff.transpose_semitones.saturating_add(
2089 instrument.map_or(0, |value| value.transpose_semitones),
2090 )
2091 };
2092 let main_event_start = events.len();
2093 for (pitch_index, pitch) in note.pitches.iter().enumerate() {
2094 let percussion = part.percussion_key(note, pitch_index);
2096 let transpose = if percussion.is_some() { 0 } else { transpose };
2097 let midi = percussion.map_or_else(
2098 || (pitch.to_midi() + transpose as i16).clamp(0, 127) as u8,
2099 |key| key.min(127),
2100 );
2101 events.push(PlaybackEvent {
2102 address: Some(format!(
2103 "{part_index}:{staff_index}:{idx}:{voice_idx}:{note_index}"
2104 )),
2105 source: Some(NoteAddr {
2106 part: part_index,
2107 staff: staff_index,
2108 measure: idx,
2109 voice: voice_idx,
2110 note: note_index,
2111 }),
2112 source_voice_number: measure.source_voice_numbers[voice_idx],
2113 time_beats: measure_start_beats + local_beats,
2114 time_secs: measure_start_secs
2115 + tempo_ramp_seconds(
2116 current_bpm,
2117 ramp_end_bpm,
2118 measure_beats,
2119 local_beats,
2120 ),
2121 pitch_midi: midi,
2122 pitch_midi_cents: if percussion.is_some() {
2123 i32::from(midi) * 100
2124 } else {
2125 pitch.to_midi_cents() + transpose as i32 * 100
2126 },
2127 pitch_bend_curve: note.guitar_bend_curve.clone(),
2128 post_note_pause_beats,
2129 articulations: note.articulations.clone(),
2130 chord_symbol: note.chord_symbol.clone(),
2131 guitar_technique: note.guitar_technique.clone(),
2132 velocity,
2133 duration_beats: sounding_dur,
2134 duration_secs: tempo_ramp_seconds(
2135 current_bpm,
2136 ramp_end_bpm,
2137 measure_beats,
2138 local_beats + sounding_dur,
2139 ) - tempo_ramp_seconds(
2140 current_bpm,
2141 ramp_end_bpm,
2142 measure_beats,
2143 local_beats,
2144 ),
2145 pedal,
2146 part_index,
2147 channel,
2148 program,
2149 instrument_id: instrument_id.clone(),
2150 is_metronome: false,
2151 });
2152 }
2153 let main_event_end = events.len();
2154 let voice = &measure.voices[voice_idx];
2155 let mut grace_start = note_index;
2156 while grace_start > 0 && voice[grace_start - 1].is_grace {
2157 grace_start -= 1;
2158 }
2159 if grace_start < note_index && main_event_start < main_event_end {
2160 let graces = &voice[grace_start..note_index];
2161 let written: Vec<f64> = graces
2165 .iter()
2166 .map(|grace| {
2167 (grace.duration.beats(grace.dot_count) * 0.25).min(0.25)
2168 })
2169 .collect();
2170 let requested: f64 = written.iter().sum();
2171 let prior_onset = prior_note_events
2172 .clone()
2173 .next()
2174 .map(|index| events[index].time_beats - measure_start_beats)
2175 .unwrap_or(0.0);
2176 let before = requested
2177 .min(local_beats * 0.5)
2178 .min((local_beats - prior_onset).max(0.0) * 0.5);
2179 let take_from_previous = before > 0.0;
2180 let grace_total = if take_from_previous {
2181 before
2182 } else {
2183 requested.min(sounding_dur * 0.5)
2184 };
2185 if grace_total > 0.0 {
2186 let grace_start_beat = if take_from_previous {
2187 local_beats - grace_total
2188 } else {
2189 local_beats
2190 };
2191 let grace_start_secs = measure_start_secs
2192 + tempo_ramp_seconds(
2193 current_bpm,
2194 ramp_end_bpm,
2195 measure_beats,
2196 grace_start_beat,
2197 );
2198 if take_from_previous {
2199 for event in &mut events[prior_note_events.clone()] {
2200 if event.time_beats + event.duration_beats
2201 > measure_start_beats + grace_start_beat
2202 {
2203 event.duration_beats = measure_start_beats
2204 + grace_start_beat
2205 - event.time_beats;
2206 event.duration_secs =
2207 grace_start_secs - event.time_secs;
2208 }
2209 }
2210 } else {
2211 let shifted_beat = local_beats + grace_total;
2212 let shifted_secs = measure_start_secs
2213 + tempo_ramp_seconds(
2214 current_bpm,
2215 ramp_end_bpm,
2216 measure_beats,
2217 shifted_beat,
2218 );
2219 for event in &mut events[main_event_start..main_event_end] {
2220 event.time_beats = measure_start_beats + shifted_beat;
2221 event.time_secs = shifted_secs;
2222 event.duration_beats -= grace_total;
2223 event.duration_secs = tempo_ramp_seconds(
2224 current_bpm,
2225 ramp_end_bpm,
2226 measure_beats,
2227 local_beats + sounding_dur,
2228 ) - tempo_ramp_seconds(
2229 current_bpm,
2230 ramp_end_bpm,
2231 measure_beats,
2232 shifted_beat,
2233 );
2234 }
2235 }
2236 let mut cursor = grace_start_beat;
2237 for (offset, grace) in graces.iter().enumerate() {
2238 let grace_beats = grace_total * written[offset] / requested;
2239 let grace_end = cursor + grace_beats;
2240 for (pitch_index, pitch) in grace.pitches.iter().enumerate()
2241 {
2242 let percussion =
2243 part.percussion_key(grace, pitch_index);
2244 let pitch_transpose =
2245 if percussion.is_some() { 0 } else { transpose };
2246 let mut event = events[main_event_start].clone();
2247 event.address = Some(format!(
2248 "{part_index}:{staff_index}:{idx}:{voice_idx}:{}",
2249 grace_start + offset
2250 ));
2251 event.source = Some(NoteAddr {
2252 part: part_index,
2253 staff: staff_index,
2254 measure: idx,
2255 voice: voice_idx,
2256 note: grace_start + offset,
2257 });
2258 event.pitch_midi = percussion.map_or_else(
2259 || {
2260 (pitch.to_midi() + pitch_transpose as i16)
2261 .clamp(0, 127)
2262 as u8
2263 },
2264 |key| key.min(127),
2265 );
2266 event.pitch_midi_cents = if percussion.is_some() {
2267 i32::from(event.pitch_midi) * 100
2268 } else {
2269 pitch.to_midi_cents()
2270 + i32::from(pitch_transpose) * 100
2271 };
2272 event.time_beats = measure_start_beats + cursor;
2273 event.time_secs = measure_start_secs
2274 + tempo_ramp_seconds(
2275 current_bpm,
2276 ramp_end_bpm,
2277 measure_beats,
2278 cursor,
2279 );
2280 event.duration_beats = grace_beats;
2281 event.duration_secs = tempo_ramp_seconds(
2282 current_bpm,
2283 ramp_end_bpm,
2284 measure_beats,
2285 grace_end,
2286 ) - tempo_ramp_seconds(
2287 current_bpm,
2288 ramp_end_bpm,
2289 measure_beats,
2290 cursor,
2291 );
2292 event.articulations = grace.articulations.clone();
2293 event.pitch_bend_curve =
2294 grace.guitar_bend_curve.clone();
2295 event.guitar_technique = grace.guitar_technique.clone();
2296 event.chord_symbol = grace.chord_symbol.clone();
2297 event.post_note_pause_beats = 0.0;
2298 event.velocity = dynamics.velocity(
2299 seq_position,
2300 onset_written_beats,
2301 grace.dynamic,
2302 );
2303 event.pedal = grace.pedal_start;
2304 events.push(event);
2305 }
2306 cursor = grace_end;
2307 }
2308 }
2309 }
2310 prior_note_events = main_event_start..main_event_end;
2311 }
2312 local_beats += dur;
2313 }
2314 time_beats = measure_start_beats + measure_beats;
2318 time_secs_cursor = measure_start_secs
2319 + tempo_ramp_seconds(
2320 current_bpm,
2321 ramp_end_bpm,
2322 measure_beats,
2323 measure_beats,
2324 );
2325 if let Some(end_bpm) = ramp_end_bpm {
2326 current_bpm = end_bpm;
2327 }
2328 }
2329 }
2330 }
2331 }
2332
2333 if let Some(ref metro) = options.metronome {
2334 let mut cursor_secs = 0.0f64;
2335 let mut cursor_beats = 0.0f64;
2336 let mut metro_bpm = bpm;
2337 for &idx in &seq {
2338 let first_staff = score.parts.first().and_then(|p| p.staves.first());
2339 let mark = tempo_marks.get(idx).copied().unwrap_or_default();
2340 if let Some(t) = mark.bpm.and_then(|tempo| written_tempo_bpm(options, tempo)) {
2341 metro_bpm = t;
2342 }
2343 let ts = first_staff.map_or(&score.settings.time_signature, |s| {
2344 s.meter_at(idx, &score.settings.time_signature)
2345 });
2346 let measure_beats = measure_lengths[idx];
2347 let ramp_end_bpm = written_tempo_ramp_bpm(options, mark.ramp_to_bpm);
2348 let beat_unit = ts.beat_unit_beats();
2349 let num_beats = (measure_beats / beat_unit).round() as u32;
2350 for b in 0..num_beats {
2351 let is_accent = b == 0;
2352 let beat_offset_beats = b as f64 * beat_unit;
2353 let beat_offset_secs =
2354 tempo_ramp_seconds(metro_bpm, ramp_end_bpm, measure_beats, beat_offset_beats);
2355 events.push(PlaybackEvent {
2356 address: None,
2357 source: None,
2358 source_voice_number: None,
2359 time_beats: cursor_beats + b as f64 * beat_unit,
2360 time_secs: cursor_secs + beat_offset_secs,
2361 pitch_midi: if is_accent {
2362 metro.accent_pitch
2363 } else {
2364 metro.beat_pitch
2365 },
2366 pitch_midi_cents: i32::from(if is_accent {
2367 metro.accent_pitch
2368 } else {
2369 metro.beat_pitch
2370 }) * 100,
2371 pitch_bend_curve: Vec::new(),
2372 post_note_pause_beats: 0.0,
2373 articulations: Vec::new(),
2374 chord_symbol: None,
2375 guitar_technique: None,
2376 velocity: if is_accent {
2377 metro.accent_velocity
2378 } else {
2379 metro.beat_velocity
2380 },
2381 duration_beats: beat_unit * 0.1,
2382 duration_secs: tempo_ramp_seconds(
2383 metro_bpm,
2384 ramp_end_bpm,
2385 measure_beats,
2386 beat_offset_beats + beat_unit * 0.1,
2387 ) - beat_offset_secs,
2388 pedal: false,
2389 part_index: usize::MAX,
2390 channel: metro.channel,
2391 program: 0,
2392 instrument_id: None,
2393 is_metronome: true,
2394 });
2395 }
2396 cursor_secs +=
2397 tempo_ramp_seconds(metro_bpm, ramp_end_bpm, measure_beats, measure_beats);
2398 cursor_beats += measure_beats;
2399 if let Some(end_bpm) = ramp_end_bpm {
2400 metro_bpm = end_bpm;
2401 }
2402 }
2403 }
2404
2405 events.sort_by(|a, b| {
2408 a.time_beats
2409 .partial_cmp(&b.time_beats)
2410 .unwrap_or(std::cmp::Ordering::Equal)
2411 });
2412 events = merge_tied_events(score, options, events);
2413 events = apply_realization_profile(score, events, options.realization_profile);
2414 events.sort_by(|a, b| {
2415 a.time_beats
2416 .partial_cmp(&b.time_beats)
2417 .unwrap_or(std::cmp::Ordering::Equal)
2418 });
2419 events
2420}
2421
2422const ORNAMENT_ATTACK_COUNT: usize = 4;
2423const ARPEGGIO_MAX_SPREAD_BEATS: f64 = 0.18;
2424
2425fn apply_realization_profile(
2426 score: &Score,
2427 mut events: Vec<PlaybackEvent>,
2428 profile: PlaybackRealizationProfile,
2429) -> Vec<PlaybackEvent> {
2430 if profile == PlaybackRealizationProfile::Authored {
2431 return events;
2432 }
2433
2434 let mut chord_groups: BTreeMap<String, Vec<usize>> = BTreeMap::new();
2435 for (index, event) in events.iter().enumerate() {
2436 if !event.is_metronome
2437 && let Some(address) = &event.address
2438 {
2439 chord_groups.entry(address.clone()).or_default().push(index);
2440 }
2441 }
2442 for indices in chord_groups.into_values() {
2443 let Some(source) = events[indices[0]].source.as_ref() else {
2444 continue;
2445 };
2446 let Some(direction) = score
2447 .parts
2448 .get(source.part)
2449 .and_then(|part| part.staves.get(source.staff))
2450 .and_then(|staff| staff.measures.get(source.measure))
2451 .and_then(|measure| measure.voices.get(source.voice))
2452 .and_then(|voice| voice.get(source.note))
2453 .and_then(|note| note.arpeggiate)
2454 else {
2455 continue;
2456 };
2457 if indices.len() < 2 {
2458 continue;
2459 }
2460 let mut ordered = indices;
2461 ordered.sort_by_key(|&index| events[index].pitch_midi_cents);
2462 if !direction {
2463 ordered.reverse();
2464 }
2465 let max_spread = events[ordered[0]]
2466 .duration_beats
2467 .max(0.0)
2468 .mul_add(0.5, 0.0)
2469 .min(ARPEGGIO_MAX_SPREAD_BEATS);
2470 let step = max_spread / (ordered.len() - 1) as f64;
2471 for (ordinal, index) in ordered.into_iter().enumerate() {
2472 shift_event_time(&mut events[index], step * ordinal as f64);
2473 }
2474 }
2475
2476 let mut realized = Vec::with_capacity(events.len());
2477 for event in events {
2478 let offsets = ornament_offsets(&event.articulations);
2479 if offsets.is_empty()
2480 || event.is_metronome
2481 || event.duration_beats <= 0.0
2482 || event
2483 .articulations
2484 .iter()
2485 .filter(|mark| is_playback_ornament(mark))
2486 .count()
2487 != 1
2488 {
2489 realized.push(event);
2490 continue;
2491 }
2492 let attack_beats = event.duration_beats / ORNAMENT_ATTACK_COUNT as f64;
2493 let attack_secs = event.duration_secs / ORNAMENT_ATTACK_COUNT as f64;
2494 for (ordinal, semitones) in offsets.into_iter().enumerate() {
2495 let mut attack = event.clone();
2496 let displacement = i32::from(semitones) * 100;
2497 attack.pitch_midi_cents = (attack.pitch_midi_cents + displacement).clamp(0, 12_700);
2498 attack.pitch_midi =
2499 ((i32::from(attack.pitch_midi) + i32::from(semitones)).clamp(0, 127)) as u8;
2500 attack.time_beats += attack_beats * ordinal as f64;
2501 attack.time_secs += attack_secs * ordinal as f64;
2502 attack.duration_beats = attack_beats;
2503 attack.duration_secs = attack_secs;
2504 attack
2507 .articulations
2508 .retain(|mark| !is_playback_ornament(mark));
2509 if ordinal > 0 {
2510 attack.chord_symbol = None;
2511 }
2512 realized.push(attack);
2513 }
2514 }
2515 realized
2516}
2517
2518fn shift_event_time(event: &mut PlaybackEvent, offset_beats: f64) {
2519 if offset_beats <= 0.0 || event.duration_beats <= 0.0 {
2520 return;
2521 }
2522 event.time_beats += offset_beats;
2523 event.time_secs += event.duration_secs * (offset_beats / event.duration_beats);
2524}
2525
2526fn is_playback_ornament(mark: &Articulation) -> bool {
2527 matches!(
2528 mark,
2529 Articulation::Trill
2530 | Articulation::Shake
2531 | Articulation::Mordent
2532 | Articulation::InvertedMordent
2533 | Articulation::Turn
2534 | Articulation::InvertedTurn
2535 )
2536}
2537
2538fn ornament_offsets(articulations: &[Articulation]) -> Vec<i8> {
2539 if articulations
2540 .iter()
2541 .any(|articulation| matches!(articulation, Articulation::Trill | Articulation::Shake))
2542 {
2543 return vec![0, 1, 0, 1];
2544 }
2545 if articulations
2546 .iter()
2547 .any(|articulation| matches!(articulation, Articulation::Mordent))
2548 {
2549 return vec![0, 1, 0, 0];
2550 }
2551 if articulations
2552 .iter()
2553 .any(|articulation| matches!(articulation, Articulation::InvertedMordent))
2554 {
2555 return vec![0, -1, 0, 0];
2556 }
2557 if articulations
2558 .iter()
2559 .any(|articulation| matches!(articulation, Articulation::Turn))
2560 {
2561 return vec![1, 0, -1, 0];
2562 }
2563 if articulations
2564 .iter()
2565 .any(|articulation| matches!(articulation, Articulation::InvertedTurn))
2566 {
2567 return vec![-1, 0, 1, 0];
2568 }
2569 Vec::new()
2570}
2571
2572fn tempo_ramp_seconds(start_bpm: f64, end_bpm: Option<f64>, measure_beats: f64, beats: f64) -> f64 {
2574 let beats = beats.max(0.0);
2575 let Some(end_bpm) = end_bpm else {
2576 return beats / start_bpm * 60.0;
2577 };
2578 if measure_beats <= 0.0 || (end_bpm - start_bpm).abs() < f64::EPSILON {
2579 return beats / start_bpm * 60.0;
2580 }
2581 let delta = end_bpm - start_bpm;
2582 let bpm_at_beats = start_bpm + delta * beats / measure_beats;
2583 if bpm_at_beats <= 0.0 {
2584 return beats / start_bpm * 60.0;
2585 }
2586 60.0 * measure_beats / delta * (bpm_at_beats / start_bpm).ln()
2587}
2588
2589fn tempo_ramp_beats_at_seconds(
2594 start_bpm: f64,
2595 end_bpm: Option<f64>,
2596 measure_beats: f64,
2597 seconds: f64,
2598) -> f64 {
2599 let seconds = seconds.max(0.0);
2600 let Some(end_bpm) = end_bpm else {
2601 return seconds * start_bpm / 60.0;
2602 };
2603 if measure_beats <= 0.0 || (end_bpm - start_bpm).abs() < f64::EPSILON {
2604 return seconds * start_bpm / 60.0;
2605 }
2606 let delta = end_bpm - start_bpm;
2607 let exponent = seconds * delta / (60.0 * measure_beats);
2608 let ratio = exponent.exp();
2609 if !ratio.is_finite() {
2610 return measure_beats;
2611 }
2612 let beats = measure_beats * start_bpm * (ratio - 1.0) / delta;
2613 if beats.is_finite() {
2614 beats
2615 } else {
2616 measure_beats
2617 }
2618}
2619
2620fn merge_tied_events(
2626 score: &Score,
2627 options: &PlaybackOptions,
2628 events: Vec<PlaybackEvent>,
2629) -> Vec<PlaybackEvent> {
2630 use std::collections::HashMap;
2631
2632 struct PendingTie {
2633 index: usize,
2634 staff: usize,
2635 voice: usize,
2636 notated_end_beats: f64,
2637 staccato: bool,
2638 }
2639
2640 let mut merged = Vec::with_capacity(events.len());
2641 let mut pending: HashMap<(usize, i32), Vec<PendingTie>> = HashMap::new();
2642 let mut clock = None;
2643 for event in events {
2644 let Some(source) = event.source.as_ref() else {
2645 merged.push(event);
2646 continue;
2647 };
2648 let tied_staff = score
2649 .parts
2650 .get(source.part)
2651 .and_then(|part| part.staves.get(source.staff));
2652 let tied_note = tied_staff
2653 .and_then(|staff| staff.measures.get(source.measure))
2654 .and_then(|measure| measure.voices.get(source.voice))
2655 .and_then(|voice| voice.get(source.note));
2656 let (Some(staff), Some(note)) = (tied_staff, tied_note) else {
2657 merged.push(event);
2658 continue;
2659 };
2660 let transpose_cents = i32::from(staff.transpose_semitones) * 100;
2662 let pitch_index = note
2663 .pitches
2664 .iter()
2665 .position(|pitch| pitch.to_midi_cents() + transpose_cents == event.pitch_midi_cents);
2666 let (tie_start, tie_end) = match pitch_index {
2667 Some(index) => (note.pitch_tie_start(index), note.pitch_tie_end(index)),
2668 None => (note.tie_start, note.tie_end),
2669 };
2670 let mut articulation_scale = 1.0;
2671 let mut staccato = false;
2672 for articulation in ¬e.articulations {
2673 match articulation {
2674 Articulation::Staccato | Articulation::Staccatissimo => {
2675 articulation_scale *= 0.5;
2676 staccato = true;
2677 }
2678 Articulation::Fermata => articulation_scale *= options.fermata_multiplier,
2679 _ => {}
2680 }
2681 }
2682 let notated_duration = if articulation_scale.is_finite()
2683 && articulation_scale > 0.0
2684 && event.duration_beats.is_finite()
2685 {
2686 event.duration_beats / articulation_scale
2687 } else {
2688 note.beats()
2689 };
2690 let source_staff = source.staff;
2691 let source_voice = source.voice;
2692 let key = (source.part, event.pitch_midi_cents);
2693 if let Some(candidates) = pending.get_mut(&key) {
2694 candidates.retain(|candidate| candidate.notated_end_beats >= event.time_beats - 1e-9);
2695 if tie_end {
2696 let matching = candidates
2697 .iter()
2698 .enumerate()
2699 .filter(|(_, candidate)| {
2700 (candidate.notated_end_beats - event.time_beats).abs() < 1e-9
2701 })
2702 .max_by_key(|(_, candidate)| {
2703 (
2704 candidate.staff == source.staff,
2705 candidate.voice == source.voice,
2706 )
2707 })
2708 .map(|(position, _)| position);
2709 if let Some(position) = matching {
2710 let mut candidate = candidates.remove(position);
2711 let previous = &mut merged[candidate.index];
2712 for articulation in event.articulations.iter().filter(|articulation| {
2713 matches!(
2714 articulation,
2715 Articulation::Staccato | Articulation::Staccatissimo
2716 )
2717 }) {
2718 if !previous.articulations.contains(articulation) {
2719 previous.articulations.push(articulation.clone());
2720 }
2721 }
2722 candidate.notated_end_beats = event.time_beats + notated_duration;
2723 candidate.staccato |= staccato;
2724 previous.duration_beats += event.duration_beats;
2725 previous.duration_secs += event.duration_secs;
2726 if candidate.staccato {
2727 previous.duration_beats =
2728 (candidate.notated_end_beats - previous.time_beats) * 0.5;
2729 let timeline =
2730 clock.get_or_insert_with(|| playback_measure_clock(score, options));
2731 if let Some(end_secs) = playback_secs_at_beat(
2732 timeline,
2733 previous.time_beats + previous.duration_beats,
2734 ) {
2735 previous.duration_secs = end_secs - previous.time_secs;
2736 }
2737 }
2738 if tie_start {
2739 candidate.staff = source.staff;
2740 candidate.voice = source.voice;
2741 candidates.push(candidate);
2742 }
2743 continue;
2744 }
2745 }
2746 }
2747
2748 let index = merged.len();
2749 merged.push(event);
2750 if tie_start {
2751 pending.entry(key).or_default().push(PendingTie {
2752 index,
2753 staff: source_staff,
2754 voice: source_voice,
2755 notated_end_beats: merged[index].time_beats + notated_duration,
2756 staccato,
2757 });
2758 }
2759 }
2760 merged
2761}
2762
2763#[derive(Clone, Copy)]
2764struct PlaybackClockMeasure {
2765 start_beats: f64,
2766 start_secs: f64,
2767 bpm: f64,
2768 ramp_to_bpm: Option<f64>,
2769 beats: f64,
2770}
2771
2772fn playback_measure_clock(score: &Score, options: &PlaybackOptions) -> Vec<PlaybackClockMeasure> {
2773 let marks = score_tempo_marks(score);
2774 let mut bpm = initial_tempo_bpm(score, options);
2775 let mut start_beats = 0.0;
2776 let mut start_secs = 0.0;
2777 let mut clock = Vec::new();
2778 for index in measure_sequence(score).into_iter().filter(|index| {
2779 options
2780 .loop_region
2781 .is_none_or(|(start, end)| *index >= start && *index <= end)
2782 }) {
2783 let mark = marks.get(index).copied().unwrap_or_default();
2784 if let Some(next) = mark.bpm.and_then(|tempo| written_tempo_bpm(options, tempo)) {
2785 bpm = next;
2786 }
2787 let beats = score.playback_measure_beats(index);
2788 let ramp_to_bpm = written_tempo_ramp_bpm(options, mark.ramp_to_bpm);
2789 clock.push(PlaybackClockMeasure {
2790 start_beats,
2791 start_secs,
2792 bpm,
2793 ramp_to_bpm,
2794 beats,
2795 });
2796 start_beats += beats;
2797 start_secs += tempo_ramp_seconds(bpm, ramp_to_bpm, beats, beats);
2798 if let Some(next) = ramp_to_bpm {
2799 bpm = next;
2800 }
2801 }
2802 clock
2803}
2804
2805fn playback_secs_at_beat(clock: &[PlaybackClockMeasure], beat: f64) -> Option<f64> {
2806 let index = clock.partition_point(|measure| measure.start_beats <= beat);
2807 let measure = clock.get(index.saturating_sub(1))?;
2808 let offset = beat - measure.start_beats;
2809 if offset > measure.beats + 1e-9 {
2810 return None;
2811 }
2812 Some(
2813 measure.start_secs
2814 + tempo_ramp_seconds(
2815 measure.bpm,
2816 measure.ramp_to_bpm,
2817 measure.beats,
2818 offset.clamp(0.0, measure.beats),
2819 ),
2820 )
2821}
2822
2823pub fn to_playback_events_bounded(
2825 score: &Score,
2826 options: &PlaybackOptions,
2827) -> Result<Vec<PlaybackEvent>, crate::Error> {
2828 let events = to_playback_events(score, options);
2829 if events.len() > MAX_PLAYBACK_COMPARISON_EVENTS {
2830 return Err(crate::Error::PlaybackComparisonTooLarge(events.len()));
2831 }
2832 Ok(events)
2833}
2834
2835#[derive(Debug, Clone, Serialize, Deserialize)]
2839pub struct PlaybackPosition {
2840 pub measure_index: usize,
2842 pub beat: f64,
2844}
2845
2846struct MeasureSegment {
2847 measure_idx: usize,
2848 start_secs: f64,
2849 duration_secs: f64,
2850 beats: f64,
2851 bpm: f64,
2852 ramp_to_bpm: Option<f64>,
2853}
2854
2855fn build_measure_segments(score: &Score, options: &PlaybackOptions) -> Vec<MeasureSegment> {
2856 let init_bpm = initial_tempo_bpm(score, options);
2857 let tempo_marks = score_tempo_marks(score);
2858 let full_seq = measure_sequence(score);
2859 let seq: Vec<usize> = if let Some((lo, hi)) = options.loop_region {
2860 full_seq
2861 .into_iter()
2862 .filter(|&i| i >= lo && i <= hi)
2863 .collect()
2864 } else {
2865 full_seq
2866 };
2867
2868 let mut segments = Vec::with_capacity(seq.len());
2869 let mut cursor_secs = 0.0f64;
2870 let mut current_bpm = init_bpm;
2871
2872 for idx in seq {
2873 let mark = tempo_marks.get(idx).copied().unwrap_or_default();
2874 if let Some(t) = mark.bpm.and_then(|tempo| written_tempo_bpm(options, tempo)) {
2875 current_bpm = t;
2876 }
2877 let beats = score.playback_measure_beats(idx);
2878 let ramp_to_bpm = written_tempo_ramp_bpm(options, mark.ramp_to_bpm);
2879 let duration_secs = tempo_ramp_seconds(current_bpm, ramp_to_bpm, beats, beats);
2880
2881 segments.push(MeasureSegment {
2882 measure_idx: idx,
2883 start_secs: cursor_secs,
2884 duration_secs,
2885 beats,
2886 bpm: current_bpm,
2887 ramp_to_bpm,
2888 });
2889 cursor_secs += duration_secs;
2890 if let Some(end_bpm) = ramp_to_bpm {
2891 current_bpm = end_bpm;
2892 }
2893 }
2894 segments
2895}
2896
2897pub fn compute_playback_position(
2903 score: &Score,
2904 options: &PlaybackOptions,
2905 elapsed_secs: f64,
2906) -> Option<PlaybackPosition> {
2907 if elapsed_secs < 0.0 {
2908 return None;
2909 }
2910 let segments = build_measure_segments(score, options);
2911 for seg in &segments {
2912 if elapsed_secs < seg.start_secs + seg.duration_secs + 1e-9 {
2913 let beat = tempo_ramp_beats_at_seconds(
2914 seg.bpm,
2915 seg.ramp_to_bpm,
2916 seg.beats,
2917 elapsed_secs - seg.start_secs,
2918 )
2919 .clamp(0.0, seg.beats);
2920 return Some(PlaybackPosition {
2921 measure_index: seg.measure_idx,
2922 beat,
2923 });
2924 }
2925 }
2926 None
2927}
2928
2929#[cfg(test)]
2930mod tests {
2931 use super::*;
2932 use crate::model::{
2933 duration::Duration,
2934 pitch::{Pitch, Step},
2935 score::{MidiAftertouch, MidiControlChange, MidiPitchBend, MidiProgramChange, Note, Score},
2936 };
2937
2938 fn opts(bpm: Option<u16>) -> PlaybackOptions {
2939 PlaybackOptions {
2940 bpm_override: bpm,
2941 ..Default::default()
2942 }
2943 }
2944
2945 #[test]
2946 fn playback_options_accepts_an_empty_json_object_as_defaults() {
2947 let parsed: PlaybackOptions = serde_json::from_str("{}").expect("default playback options");
2948 assert_eq!(parsed, PlaybackOptions::default());
2949 }
2950
2951 #[test]
2952 fn bounded_playback_events_match_unbounded_schedule_within_limit() {
2953 let score = Score::new("bounded", 120, 4, 4, 0, 1);
2954 let options = opts(Some(120));
2955 let unbounded = to_playback_events(&score, &options);
2956 let bounded = to_playback_events_bounded(&score, &options).expect("bounded schedule");
2957 assert_eq!(bounded, unbounded);
2958 }
2959
2960 #[test]
2961 fn bars_after_a_meter_change_keep_the_new_meter() {
2962 use crate::model::duration::Duration;
2963 use crate::{Note, Pitch, Step, TimeSignature};
2964 let mut score = Score::new("meter", 120, 4, 4, 0, 3);
2965 let staff = &mut score.parts[0].staves[0];
2966 staff.measures[1].time_sig = Some(TimeSignature {
2967 numerator: 3,
2968 denominator: 4,
2969 });
2970 for measure in &mut staff.measures {
2971 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
2972 }
2973 let onsets: Vec<f64> = to_playback_events(&score, &opts(Some(120)))
2974 .iter()
2975 .filter(|event| event.address.is_some())
2976 .map(|event| event.time_beats)
2977 .collect();
2978 assert_eq!(onsets, vec![0.0, 4.0, 7.0]);
2980 }
2981
2982 #[test]
2983 fn tempo_written_on_either_part_governs_the_whole_score() {
2984 let mut score = Score::new("shared tempo", 120, 4, 4, 0, 2);
2985 let mut second = score.parts[0].clone();
2986 second.id = "second".into();
2987 score.parts.push(second);
2988 for part in &mut score.parts {
2989 for measure in &mut part.staves[0].measures {
2990 measure.voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
2991 }
2992 }
2993 score.parts[0].staves[0].measures[0].tempo = Some(92);
2994 score.parts[1].staves[0].measures[1].tempo = Some(60);
2995 let options = PlaybackOptions {
2996 metronome: Some(MetronomeConfig::default()),
2997 ..Default::default()
2998 };
2999 let events = to_playback_events(&score, &options);
3000 for part_index in 0..2 {
3001 let first = events
3002 .iter()
3003 .find(|event| {
3004 event.part_index == part_index
3005 && event
3006 .source
3007 .as_ref()
3008 .is_some_and(|source| source.measure == 0)
3009 })
3010 .unwrap();
3011 let second = events
3012 .iter()
3013 .find(|event| {
3014 event.part_index == part_index
3015 && event
3016 .source
3017 .as_ref()
3018 .is_some_and(|source| source.measure == 1)
3019 })
3020 .unwrap();
3021 assert!((first.duration_secs - 60.0 / 92.0).abs() < 1e-9);
3022 assert!((second.time_secs - 4.0 * 60.0 / 92.0).abs() < 1e-9);
3023 assert!((second.duration_secs - 1.0).abs() < 1e-9);
3024 }
3025 let clicks: Vec<_> = events.iter().filter(|event| event.is_metronome).collect();
3026 assert!((clicks[1].time_secs - 60.0 / 92.0).abs() < 1e-9);
3027 assert!((clicks[4].time_secs - 4.0 * 60.0 / 92.0).abs() < 1e-9);
3028 let position = compute_playback_position(&score, &options, 1.5 * 60.0 / 92.0).unwrap();
3029 assert_eq!(position.measure_index, 0);
3030 assert!((position.beat - 1.5).abs() < 1e-9);
3031 let timeline_1 = offline_measure_times(&score, &options, 1, 0, &score_tempo_marks(&score));
3032 assert!((timeline_1[0].bpm - 92.0).abs() < 1e-9);
3033 assert!((timeline_1[1].bpm - 60.0).abs() < 1e-9);
3034 }
3035
3036 #[test]
3037 fn empty_score_no_events() {
3038 let score = Score::new("T", 120, 4, 4, 0, 1);
3039 assert!(to_playback_events(&score, &opts(None)).is_empty());
3040 }
3041
3042 #[test]
3043 fn single_note_at_beat_zero() {
3044 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3045 score.parts[0].staves[0].measures[0].voices[0] =
3046 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3047 let events = to_playback_events(&score, &opts(None));
3048 assert_eq!(events.len(), 1);
3049 assert_eq!(events[0].address.as_deref(), Some("0:0:0:0:0"));
3050 assert_eq!(
3051 events[0].source,
3052 Some(NoteAddr {
3053 part: 0,
3054 staff: 0,
3055 measure: 0,
3056 voice: 0,
3057 note: 0,
3058 })
3059 );
3060 assert!((events[0].time_beats).abs() < 1e-9);
3061 assert_eq!(events[0].pitch_midi, 60);
3062 assert_eq!(events[0].velocity, 64);
3063 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
3064 assert_eq!(events[0].part_index, 0);
3065 }
3066
3067 #[test]
3068 fn tied_notes_are_one_continuous_playback_event() {
3069 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3070 let mut first = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3071 first.tie_start = true;
3072 let mut second = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3073 second.tie_end = true;
3074 score.parts[0].staves[0].measures[0].voices[0] = vec![first, second];
3075
3076 let events = to_playback_events(&score, &opts(None));
3077 assert_eq!(events.len(), 1);
3078 assert_eq!(
3079 events[0].source.as_ref().map(|address| address.note),
3080 Some(0)
3081 );
3082 assert!((events[0].time_beats).abs() < 1e-9);
3083 assert!((events[0].duration_beats - 2.0).abs() < 1e-9);
3084 assert!((events[0].duration_secs - 1.0).abs() < 1e-9);
3085 }
3086
3087 #[test]
3088 fn staccato_tied_across_barline_has_one_half_length_attack() {
3089 for marks in [[true, false], [false, true], [true, true]] {
3090 let mut score = Score::new("tied staccato", 120, 4, 4, 0, 2);
3091 let mut first = Note::new(Pitch::new(Step::D, 5), Duration::Eighth);
3092 first.tie_start = true;
3093 let mut second = Note::new(Pitch::new(Step::D, 5), Duration::Eighth);
3094 second.tie_end = true;
3095 if marks[0] {
3096 first.articulations.push(Articulation::Staccato);
3097 }
3098 if marks[1] {
3099 second.articulations.push(Articulation::Staccato);
3100 }
3101 score.parts[0].staves[0].measures[0].voices[0] = vec![
3102 Note::rest(Duration::Half),
3103 Note::rest(Duration::Quarter),
3104 Note::rest(Duration::Eighth),
3105 first,
3106 ];
3107 score.parts[0].staves[0].measures[1].voices[0] = vec![second];
3108 let events = to_playback_events(&score, &opts(None));
3109 assert_eq!(events.len(), 1, "articulations: {marks:?}");
3110 assert_eq!(events[0].pitch_midi, 74);
3111 assert_eq!(events[0].time_beats, 3.5);
3112 assert!(events[0].articulations.contains(&Articulation::Staccato));
3113 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
3114 assert!((events[0].duration_secs - 0.25).abs() < 1e-9);
3115 }
3116 }
3117
3118 #[test]
3119 fn tied_staccato_uses_tempo_at_sounding_end() {
3120 let mut score = Score::new("tied tempo", 120, 4, 4, 0, 2);
3121 let mut first = Note::new(Pitch::new(Step::D, 5), Duration::Eighth);
3122 first.tie_start = true;
3123 first.articulations.push(Articulation::Staccato);
3124 let mut second = Note::new(Pitch::new(Step::D, 5), Duration::Quarter);
3125 second.dot_count = 1;
3126 second.tie_end = true;
3127 score.parts[0].staves[0].measures[0].voices[0] = vec![
3128 Note::rest(Duration::Half),
3129 Note::rest(Duration::Quarter),
3130 Note::rest(Duration::Eighth),
3131 first,
3132 ];
3133 score.parts[0].staves[0].measures[1].tempo = Some(60);
3134 score.parts[0].staves[0].measures[1].voices[0] = vec![second];
3135 let events = to_playback_events(&score, &opts(None));
3136 assert_eq!(events.len(), 1);
3137 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
3138 assert!((events[0].duration_secs - 0.75).abs() < 1e-9);
3139 }
3140
3141 #[test]
3142 fn a_tie_on_one_chord_pitch_holds_only_that_pitch() {
3143 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3144 let chord = || {
3146 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3147 note.pitches.push(Pitch::new(Step::G, 4));
3148 note
3149 };
3150 let mut first = chord();
3151 first.set_pitch_ties(&[true, false], &[false, false]);
3152 let mut second = chord();
3153 second.set_pitch_ties(&[false, false], &[true, false]);
3154 assert!(first.tie_start && second.tie_end);
3155 score.parts[0].staves[0].measures[0].voices[0] = vec![first, second];
3156
3157 let events = to_playback_events(&score, &opts(None));
3158 let durations = |midi: u8| {
3159 events
3160 .iter()
3161 .filter(|event| event.pitch_midi == midi)
3162 .map(|event| event.duration_beats)
3163 .collect::<Vec<_>>()
3164 };
3165 assert_eq!(durations(60), vec![2.0]);
3166 assert_eq!(durations(67), vec![1.0, 1.0]);
3167 }
3168
3169 #[test]
3170 fn ties_cross_measure_boundaries_and_tempo_changes_without_retriggering() {
3171 let mut score = Score::new("T", 120, 4, 4, 0, 2);
3172 let first_measure = &mut score.parts[0].staves[0].measures[0];
3173 let mut first = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
3174 first.tie_start = true;
3175 first_measure.voices[0] = vec![first];
3176
3177 let second_measure = &mut score.parts[0].staves[0].measures[1];
3178 second_measure.tempo = Some(60);
3179 let mut second = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
3180 second.tie_end = true;
3181 second_measure.voices[0] = vec![second];
3182
3183 let events = to_playback_events(&score, &opts(None));
3184 assert_eq!(events.len(), 1);
3185 assert_eq!(
3186 events[0].source.as_ref().map(|address| address.measure),
3187 Some(0)
3188 );
3189 assert!((events[0].time_beats).abs() < 1e-9);
3190 assert!((events[0].duration_beats - 8.0).abs() < 1e-9);
3191 assert!((events[0].duration_secs - 6.0).abs() < 1e-9);
3192 }
3193
3194 #[test]
3195 fn ties_continue_across_voice_and_staff_changes() {
3196 let whole_c = || Note::new(Pitch::new(Step::C, 4), Duration::Whole);
3197
3198 let mut voice_score = Score::new("voice change", 120, 4, 4, 0, 2);
3199 let mut start = whole_c();
3200 start.tie_start = true;
3201 let mut end = whole_c();
3202 end.tie_end = true;
3203 voice_score.parts[0].staves[0].measures[0].voices[0] = vec![start.clone()];
3204 voice_score.parts[0].staves[0].measures[1].voices[1] = vec![end.clone()];
3205 let events = to_playback_events(&voice_score, &opts(None));
3206 assert_eq!(events.len(), 1);
3207 assert_eq!(events[0].time_beats, 0.0);
3208 assert_eq!(events[0].duration_beats, 8.0);
3209
3210 let mut staff_score = Score::new("staff change", 120, 4, 4, 0, 2);
3211 let second_staff = staff_score.parts[0].staves[0].clone();
3212 staff_score.parts[0].staves.push(second_staff);
3213 staff_score.parts[0].staves[0].measures[0].voices[0] = vec![start];
3214 staff_score.parts[0].staves[1].measures[1].voices[0] = vec![end];
3215 let events = to_playback_events(&staff_score, &opts(None));
3216 assert_eq!(events.len(), 1);
3217 assert_eq!(events[0].time_beats, 0.0);
3218 assert_eq!(events[0].duration_beats, 8.0);
3219 }
3220
3221 #[test]
3222 fn cross_voice_tie_keeps_independent_unison_attacks() {
3223 let mut score = Score::new("unison", 120, 4, 4, 0, 2);
3224 let mut start = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
3225 start.tie_start = true;
3226 let mut end = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
3227 end.tie_end = true;
3228 score.parts[0].staves[0].measures[0].voices[0] = vec![start];
3229 score.parts[0].staves[0].measures[0].voices[1] =
3230 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
3231 score.parts[0].staves[0].measures[1].voices[1] = vec![end];
3232 score.parts[0].staves[0].measures[1].voices[2] =
3233 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
3234
3235 let mut durations: Vec<_> = to_playback_events(&score, &opts(None))
3236 .into_iter()
3237 .map(|event| event.duration_beats)
3238 .collect();
3239 durations.sort_by(f64::total_cmp);
3240 assert_eq!(durations, vec![4.0, 4.0, 8.0]);
3241 }
3242
3243 #[test]
3244 fn malformed_tie_end_does_not_drop_playback_event() {
3245 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3246 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3247 note.tie_end = true;
3248 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3249 let events = to_playback_events(&score, &opts(None));
3250 assert_eq!(events.len(), 1);
3251 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
3252 }
3253
3254 #[test]
3255 fn microtonal_playback_event_keeps_exact_midi_cents() {
3256 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3257 score.parts[0].staves[0].measures[0].voices[0] = vec![Note::new(
3258 Pitch::with_microtone(Step::C, 4, 0, 50),
3259 Duration::Quarter,
3260 )];
3261 let events = to_playback_events(&score, &opts(None));
3262 assert_eq!(events[0].pitch_midi, 61);
3263 assert_eq!(events[0].pitch_midi_cents, 6050);
3264 }
3265
3266 #[test]
3267 fn chord_expands_to_multiple_events() {
3268 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3269 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3270 note.pitches.push(Pitch::new(Step::E, 4));
3271 note.pitches.push(Pitch::new(Step::G, 4));
3272 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3273 let events = to_playback_events(&score, &opts(None));
3274 assert_eq!(events.len(), 3);
3275 assert!(
3276 events
3277 .iter()
3278 .all(|event| event.address.as_deref() == Some("0:0:0:0:0"))
3279 );
3280 assert!(events.iter().all(|e| e.time_beats.abs() < 1e-9));
3281 }
3282
3283 #[test]
3284 fn playback_events_preserve_authored_chord_symbol_without_inventing_notes() {
3285 let mut score = Score::new("Harmony", 120, 4, 4, 0, 1);
3286 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3287 note.chord_symbol = Some(ChordSymbol {
3288 root: "C".into(),
3289 kind: "major-seventh".into(),
3290 bass: Some("E".into()),
3291 placement: None,
3292 extender: false,
3293 harmonic_degree: None,
3294 harmony_function: None,
3295 harmony_type: None,
3296 chord_ref: None,
3297 range_end: None,
3298 degrees: Vec::new(),
3299 });
3300 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3301
3302 let events = to_playback_events(&score, &PlaybackOptions::default());
3303 assert_eq!(events.len(), 1);
3304 assert_eq!(events[0].pitch_midi, 60);
3305 assert_eq!(
3306 events[0]
3307 .chord_symbol
3308 .as_ref()
3309 .map(ChordSymbol::display_text),
3310 Some("Cmaj7/E".into())
3311 );
3312 }
3313
3314 #[test]
3315 fn authored_realization_default_keeps_ornament_as_one_semantic_event() {
3316 let mut score = Score::new("authored ornament", 120, 4, 4, 0, 1);
3317 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3318 note.articulations.push(Articulation::Trill);
3319 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3320
3321 let events = to_playback_events(&score, &PlaybackOptions::default());
3322 assert_eq!(events.len(), 1);
3323 assert_eq!(events[0].pitch_midi, 60);
3324 assert_eq!(events[0].articulations, vec![Articulation::Trill]);
3325 }
3326
3327 #[test]
3328 fn ornament_arpeggio_profile_realizes_trill_with_pinned_timing() {
3329 let mut score = Score::new("realized ornament", 120, 4, 4, 0, 1);
3330 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3331 note.articulations.push(Articulation::Trill);
3332 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3333
3334 let events = to_playback_events(
3335 &score,
3336 &PlaybackOptions {
3337 realization_profile: PlaybackRealizationProfile::OrnamentArpeggioV1,
3338 ..Default::default()
3339 },
3340 );
3341 assert_eq!(events.len(), 4);
3342 assert_eq!(
3343 events
3344 .iter()
3345 .map(|event| event.pitch_midi)
3346 .collect::<Vec<_>>(),
3347 vec![60, 61, 60, 61]
3348 );
3349 for (index, event) in events.iter().enumerate() {
3350 assert!((event.time_beats - index as f64 * 0.25).abs() < 1e-9);
3351 assert!((event.duration_beats - 0.25).abs() < 1e-9);
3352 assert!((event.time_secs - index as f64 * 0.125).abs() < 1e-9);
3353 assert!((event.duration_secs - 0.125).abs() < 1e-9);
3354 assert!(!event.articulations.contains(&Articulation::Trill));
3355 }
3356 }
3357
3358 #[test]
3359 fn ornament_arpeggio_profile_staggers_chord_in_authored_direction() {
3360 let mut score = Score::new("realized arpeggio", 120, 4, 4, 0, 1);
3361 let mut chord = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3362 chord.pitches.push(Pitch::new(Step::E, 4));
3363 chord.pitches.push(Pitch::new(Step::G, 4));
3364 chord.arpeggiate = Some(false);
3365 score.parts[0].staves[0].measures[0].voices[0] = vec![chord];
3366
3367 let events = to_playback_events(
3368 &score,
3369 &PlaybackOptions {
3370 realization_profile: PlaybackRealizationProfile::OrnamentArpeggioV1,
3371 ..Default::default()
3372 },
3373 );
3374 assert_eq!(events.len(), 3);
3375 let by_pitch: BTreeMap<_, _> = events
3376 .iter()
3377 .map(|event| (event.pitch_midi, event.time_beats))
3378 .collect();
3379 assert!((by_pitch[&67] - 0.0).abs() < 1e-9);
3380 assert!((by_pitch[&64] - 0.09).abs() < 1e-9);
3381 assert!((by_pitch[&60] - 0.18).abs() < 1e-9);
3382 }
3383
3384 #[test]
3385 fn written_grace_steals_time_from_preceding_note() {
3386 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3387 let previous = Note::new(Pitch::new(Step::F, 4), Duration::Half);
3388 let mut grace = Note::new(Pitch::new(Step::B, 4), Duration::Eighth);
3389 grace.is_grace = true;
3390 let regular = Note::new(Pitch::new(Step::A, 4), Duration::Quarter);
3391 score.parts[0].staves[0].measures[0].voices[0] = vec![previous, grace, regular];
3392 let events = to_playback_events(&score, &opts(None));
3393 assert_eq!(events.len(), 3);
3394 assert_eq!(
3395 events
3396 .iter()
3397 .map(|event| event.pitch_midi)
3398 .collect::<Vec<_>>(),
3399 vec![65, 71, 69]
3400 );
3401 assert!((events[0].duration_beats - 1.875).abs() < 1e-9);
3402 assert!((events[1].time_beats - 1.875).abs() < 1e-9);
3403 assert!((events[1].duration_beats - 0.125).abs() < 1e-9);
3404 assert!((events[2].time_beats - 2.0).abs() < 1e-9);
3405 assert!((events[1].time_secs - 0.9375).abs() < 1e-9);
3406 }
3407
3408 #[test]
3409 fn opening_grace_borrows_from_main_note_without_moving_later_notes() {
3410 let mut score = Score::new("opening grace", 120, 4, 4, 0, 1);
3411 let mut grace = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
3412 grace.is_grace = true;
3413 let main = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3414 let next = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
3415 score.parts[0].staves[0].measures[0].voices[0] = vec![grace, main, next];
3416 let events = to_playback_events(&score, &opts(None));
3417 assert_eq!(events.len(), 3);
3418 assert_eq!(
3419 events
3420 .iter()
3421 .map(|event| event.pitch_midi)
3422 .collect::<Vec<_>>(),
3423 vec![62, 60, 64]
3424 );
3425 assert!((events[0].time_beats).abs() < 1e-9);
3426 assert!((events[0].duration_beats - 0.125).abs() < 1e-9);
3427 assert!((events[1].time_beats - 0.125).abs() < 1e-9);
3428 assert!((events[1].duration_beats - 0.875).abs() < 1e-9);
3429 assert!((events[2].time_beats - 1.0).abs() < 1e-9);
3430 }
3431
3432 #[test]
3433 fn consecutive_graces_are_ordered_and_cue_notes_remain_silent() {
3434 let mut score = Score::new("two graces", 120, 4, 4, 0, 1);
3435 let mut first_grace = Note::new(Pitch::new(Step::D, 4), Duration::Sixteenth);
3436 first_grace.is_grace = true;
3437 let mut second_grace = Note::new(Pitch::new(Step::E, 4), Duration::Sixteenth);
3438 second_grace.is_grace = true;
3439 let mut cue = Note::new(Pitch::new(Step::B, 4), Duration::Eighth);
3440 cue.is_cue = true;
3441 score.parts[0].staves[0].measures[0].voices[0] = vec![
3442 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
3443 first_grace,
3444 second_grace,
3445 Note::new(Pitch::new(Step::F, 4), Duration::Quarter),
3446 cue,
3447 ];
3448 let events = to_playback_events(&score, &opts(None));
3449 assert_eq!(
3450 events
3451 .iter()
3452 .map(|event| event.pitch_midi)
3453 .collect::<Vec<_>>(),
3454 vec![60, 62, 64, 65]
3455 );
3456 assert_eq!(
3457 events
3458 .iter()
3459 .map(|event| event.time_beats)
3460 .collect::<Vec<_>>(),
3461 vec![0.0, 0.875, 0.9375, 1.0]
3462 );
3463 assert!((events[0].duration_beats - 0.875).abs() < 1e-9);
3464 }
3465
3466 #[test]
3467 fn metronome_events_have_no_source_address() {
3468 let score = Score::new("T", 120, 4, 4, 0, 1);
3469 let options = PlaybackOptions {
3470 metronome: Some(MetronomeConfig::default()),
3471 ..Default::default()
3472 };
3473 let events = to_playback_events(&score, &options);
3474 assert!(!events.is_empty());
3475 assert!(events.iter().all(|event| event.address.is_none()));
3476 }
3477
3478 #[test]
3479 fn second_note_has_correct_time() {
3480 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3481 score.parts[0].staves[0].measures[0].voices[0] = vec![
3482 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
3483 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
3484 ];
3485 let events = to_playback_events(&score, &opts(None));
3486 assert_eq!(events.len(), 2);
3487 assert!((events[0].time_beats).abs() < 1e-9);
3488 assert!((events[1].time_beats - 1.0).abs() < 1e-9);
3489 }
3490
3491 #[test]
3492 fn sparse_voice_keeps_measure_boundaries() {
3493 let mut score = Score::new("T", 120, 4, 4, 0, 2);
3494 let note = || Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3495 score.parts[0].staves[0].measures[0].voices[1] = vec![note()];
3496 score.parts[0].staves[0].measures[1].voices[1] = vec![note()];
3497
3498 let events = to_playback_events(&score, &opts(None));
3499 assert_eq!(events.len(), 2);
3500 assert!((events[0].time_beats).abs() < 1e-9);
3501 assert!((events[1].time_beats - 4.0).abs() < 1e-9);
3502 assert!((events[1].time_secs - 2.0).abs() < 1e-9);
3503 }
3504
3505 #[test]
3506 fn time_secs_120_bpm_quarter_note_is_half_second() {
3507 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3508 score.parts[0].staves[0].measures[0].voices[0] =
3509 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3510 let events = to_playback_events(&score, &opts(None));
3511 assert!((events[0].time_secs).abs() < 1e-9);
3512 assert!((events[0].duration_secs - 0.5).abs() < 1e-9);
3513 }
3514
3515 #[test]
3516 fn bpm_override_changes_time_secs() {
3517 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3518 score.parts[0].staves[0].measures[0].voices[0] = vec![
3519 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
3520 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
3521 ];
3522 let events = to_playback_events(&score, &opts(Some(60)));
3523 assert!((events[0].time_secs).abs() < 1e-9);
3524 assert!((events[1].time_secs - 1.0).abs() < 1e-9);
3525 }
3526
3527 #[test]
3528 fn tempo_multiplier_preserves_written_tempo_changes_and_ramps() {
3529 let mut score = Score::new("T", 120, 4, 4, 0, 2);
3530 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
3531 score.parts[0].staves[0].measures[1].tempo = Some(90);
3532 score.parts[0].staves[0].measures[0].voices[0] =
3533 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3534 score.parts[0].staves[0].measures[1].voices[0] =
3535 vec![Note::new(Pitch::new(Step::D, 4), Duration::Quarter)];
3536 let options = PlaybackOptions {
3537 tempo_multiplier: 0.5,
3538 ..Default::default()
3539 };
3540
3541 let events = to_playback_events(&score, &options);
3542 let second_measure_start = tempo_ramp_seconds(60.0, Some(30.0), 4.0, 4.0);
3543 assert!((events[1].time_secs - second_measure_start).abs() < 1e-9);
3544
3545 let one_beat_into_ramp = tempo_ramp_seconds(60.0, Some(30.0), 4.0, 1.0);
3546 let position = compute_playback_position(&score, &options, one_beat_into_ramp).unwrap();
3547 assert_eq!(position.measure_index, 0);
3548 assert!((position.beat - 1.0).abs() < 1e-9);
3549
3550 let second_measure_position =
3551 compute_playback_position(&score, &options, second_measure_start + 60.0 / 45.0)
3552 .unwrap();
3553 assert_eq!(second_measure_position.measure_index, 1);
3554 assert!((second_measure_position.beat - 1.0).abs() < 1e-9);
3555 }
3556
3557 #[test]
3558 fn bpm_override_suppresses_written_tempo_changes_and_ramps() {
3559 let mut score = Score::new("T", 120, 4, 4, 0, 2);
3560 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
3561 score.parts[0].staves[0].measures[1].tempo = Some(90);
3562 score.parts[0].staves[0].measures[0].voices[0] =
3563 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3564 score.parts[0].staves[0].measures[1].voices[0] =
3565 vec![Note::new(Pitch::new(Step::D, 4), Duration::Quarter)];
3566 let options = PlaybackOptions {
3567 bpm_override: Some(120),
3568 ..Default::default()
3569 };
3570
3571 let events = to_playback_events(&score, &options);
3572 assert!((events[1].time_secs - 2.0).abs() < 1e-9);
3573 let position = compute_playback_position(&score, &options, 2.5).unwrap();
3574 assert_eq!(position.measure_index, 1);
3575 assert!((position.beat - 1.0).abs() < 1e-9);
3576 }
3577
3578 #[test]
3579 fn playback_position_tracks_repeats_rests_and_meter_changes_with_a_tempo_multiplier() {
3580 use crate::model::notation::{Barline, TimeSignature};
3581
3582 let mut score = Score::new("transport", 120, 4, 4, 0, 3);
3583 let staff = &mut score.parts[0].staves[0];
3584 staff.measures[0].barline_left = Barline::RepeatStart;
3585 staff.measures[1].time_sig = Some(TimeSignature {
3586 numerator: 3,
3587 denominator: 4,
3588 });
3589 staff.measures[2].barline_right = Barline::RepeatEnd;
3590 staff.measures[0].voices[0] = vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3591 staff.measures[1].voices[0] = vec![Note::rest(Duration::Quarter)];
3592 staff.measures[2].voices[0] = vec![Note::new(Pitch::new(Step::D, 4), Duration::Quarter)];
3593 let options = PlaybackOptions {
3594 tempo_multiplier: 0.5,
3595 ..Default::default()
3596 };
3597
3598 let events = to_playback_events(&score, &options);
3599 let note_starts: Vec<f64> = events
3600 .iter()
3601 .filter(|event| event.address.is_some())
3602 .map(|event| event.time_secs)
3603 .collect();
3604 assert_eq!(note_starts, vec![0.0, 7.0, 10.0, 17.0]);
3606
3607 let middle = compute_playback_position(&score, &options, 5.0).unwrap();
3608 assert_eq!(middle.measure_index, 1);
3609 assert!((middle.beat - 1.0).abs() < 1e-9);
3610
3611 let repeated = compute_playback_position(&score, &options, 10.5).unwrap();
3612 assert_eq!(repeated.measure_index, 0);
3613 assert!((repeated.beat - 0.5).abs() < 1e-9);
3614 }
3615
3616 #[test]
3617 fn transpose_semitones_shifts_midi_output() {
3618 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3619 score.parts[0].staves[0].transpose_semitones = -2;
3620 score.parts[0].staves[0].measures[0].voices[0] =
3621 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3622 let events = to_playback_events(&score, &opts(None));
3623 assert_eq!(events.len(), 1);
3624 assert_eq!(events[0].pitch_midi, 58);
3625 }
3626
3627 #[test]
3628 fn percussion_channel_9_ignores_transpose_semitones() {
3629 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3630 score.parts[0].midi_channel = 9;
3631 score.parts[0].staves[0].transpose_semitones = -2;
3632 score.parts[0].staves[0].measures[0].voices[0] =
3633 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3634 let events = to_playback_events(&score, &opts(None));
3635 assert_eq!(events.len(), 1);
3636 assert_eq!(events[0].pitch_midi, 60);
3637 }
3638
3639 #[test]
3640 fn staccato_halves_duration_beats() {
3641 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3642 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3643 note.articulations
3644 .push(crate::model::notation::Articulation::Staccato);
3645 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3646 let events = to_playback_events(&score, &opts(None));
3647 assert_eq!(events.len(), 1);
3648 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
3649 }
3650
3651 #[test]
3652 fn staccatissimo_also_halves_duration() {
3653 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3654 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3655 note.articulations
3656 .push(crate::model::notation::Articulation::Staccatissimo);
3657 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3658 let events = to_playback_events(&score, &opts(None));
3659 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
3660 }
3661
3662 #[test]
3663 fn staccato_does_not_shift_next_note_time() {
3664 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3665 let mut n1 = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3666 n1.articulations
3667 .push(crate::model::notation::Articulation::Staccato);
3668 let n2 = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
3669 score.parts[0].staves[0].measures[0].voices[0] = vec![n1, n2];
3670 let events = to_playback_events(&score, &opts(None));
3671 assert_eq!(events.len(), 2);
3672 assert!((events[1].time_beats - 1.0).abs() < 1e-9);
3673 }
3674
3675 #[test]
3676 fn dynamics_hold_until_the_next_marking_across_voices_and_bars() {
3677 use crate::model::notation::Dynamic;
3678 let mut score = Score::new("T", 120, 4, 4, 0, 2);
3679 let quarters = |dynamics: [Option<Dynamic>; 4]| -> Vec<Note> {
3680 dynamics
3681 .into_iter()
3682 .map(|dynamic| {
3683 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3684 note.dynamic = dynamic;
3685 note
3686 })
3687 .collect()
3688 };
3689 score.parts[0].staves[0].measures[0].voices[0] =
3692 quarters([Some(Dynamic::P), None, Some(Dynamic::Sf), None]);
3693 score.parts[0].staves[0].measures[0].voices[1] = vec![
3694 Note::new(Pitch::new(Step::C, 3), Duration::Half),
3695 Note::new(Pitch::new(Step::C, 3), Duration::Half),
3696 ];
3697 score.parts[0].staves[0].measures[1].voices[0] =
3698 quarters([Some(Dynamic::Fp), None, Some(Dynamic::Pf), None]);
3699 let events = to_playback_events(&score, &opts(None));
3700 let velocities = |pitch: u8| -> Vec<u8> {
3701 events
3702 .iter()
3703 .filter(|event| event.pitch_midi == pitch)
3704 .map(|event| event.velocity)
3705 .collect()
3706 };
3707 let (p, f, sf) = (
3708 Dynamic::P.to_velocity(),
3709 Dynamic::F.to_velocity(),
3710 Dynamic::Sf.to_velocity(),
3711 );
3712 assert_eq!(velocities(60), vec![p, p, sf, p, f, p, p, f]);
3713 assert_eq!(velocities(48), vec![p, sf]);
3714 assert_eq!(
3716 DynamicTimeline::default().velocity(0, 0.0, None),
3717 DynamicTimeline::DEFAULT_VELOCITY
3718 );
3719 }
3720
3721 #[test]
3722 fn hairpins_ramp_towards_the_following_marking_or_two_steps() {
3723 use crate::model::notation::{Dynamic, HairpinKind};
3724 let mut score = Score::new("T", 120, 4, 4, 0, 2);
3725 let mut bar = |index: usize, dynamics: [Option<Dynamic>; 4], end: Option<Dynamic>| {
3726 let mut notes: Vec<Note> = dynamics
3727 .into_iter()
3728 .map(|dynamic| {
3729 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3730 note.dynamic = dynamic;
3731 note
3732 })
3733 .collect();
3734 notes[0].hairpin_start = Some(HairpinKind::Crescendo);
3735 notes[2].hairpin_end = true;
3736 notes[3].dynamic = end;
3737 score.parts[0].staves[0].measures[index].voices[0] = notes;
3738 };
3739 bar(0, [Some(Dynamic::P), None, None, None], Some(Dynamic::F));
3741 bar(1, [None, None, None, None], None);
3742 let events = to_playback_events(&score, &opts(None));
3743 let velocities: Vec<u8> = events.iter().map(|event| event.velocity).collect();
3744 assert_eq!(&velocities[..4], &[48, 60, 72, 84]);
3745 assert_eq!(&velocities[4..], &[84, 92, 100, 108]);
3746 }
3747
3748 #[test]
3749 fn dynamic_names_round_trip_and_compounds_continue_at_their_second_level() {
3750 use crate::model::notation::Dynamic;
3751 for dynamic in Dynamic::ALL {
3752 assert_eq!(
3753 Dynamic::from_musicxml_str(dynamic.to_musicxml_str()),
3754 Some(dynamic)
3755 );
3756 }
3757 assert_eq!(Dynamic::from_musicxml_str("fffff"), Some(Dynamic::Ffff));
3758 assert_eq!(Dynamic::Sfpp.sustained_level(), Some(Dynamic::Pp));
3759 assert_eq!(Dynamic::Sfz.sustained_level(), None);
3760 assert_eq!(Dynamic::from_musicxml_str("other"), None);
3761 }
3762
3763 #[test]
3764 fn accent_boosts_velocity_clamped() {
3765 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3766 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3767 note.articulations
3768 .push(crate::model::notation::Articulation::Accent);
3769 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3770 let events = to_playback_events(&score, &opts(None));
3771 assert_eq!(events[0].velocity, 84);
3772 }
3773
3774 #[test]
3775 fn accent_clamped_at_127() {
3776 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3777 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3778 note.dynamic = Some(crate::model::notation::Dynamic::Ffff);
3779 note.articulations
3780 .push(crate::model::notation::Articulation::Accent);
3781 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3782 let events = to_playback_events(&score, &opts(None));
3783 assert_eq!(events[0].velocity, 127);
3784 }
3785
3786 #[test]
3787 fn tenuto_keeps_full_duration() {
3788 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3789 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3790 note.articulations
3791 .push(crate::model::notation::Articulation::Tenuto);
3792 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3793 let events = to_playback_events(&score, &opts(None));
3794 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
3795 }
3796
3797 #[test]
3798 fn pedal_start_sets_pedal_field() {
3799 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3800 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3801 note.pedal_start = true;
3802 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3803 let events = to_playback_events(&score, &opts(None));
3804 assert!(events[0].pedal);
3805 }
3806
3807 #[test]
3808 fn no_pedal_start_pedal_is_false() {
3809 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3810 score.parts[0].staves[0].measures[0].voices[0] =
3811 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3812 let events = to_playback_events(&score, &opts(None));
3813 assert!(!events[0].pedal);
3814 }
3815
3816 #[test]
3817 fn set_tempo_at_measure_changes_time_secs() {
3818 let mut score = Score::new("T", 120, 4, 4, 0, 2);
3819 score.parts[0].staves[0].measures[0].voices[0] =
3820 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
3821 score.parts[0].staves[0].measures[1].tempo = Some(60);
3822 score.parts[0].staves[0].measures[1].voices[0] =
3823 vec![Note::new(Pitch::new(Step::D, 4), Duration::Whole)];
3824 let events = to_playback_events(&score, &opts(None));
3825 assert_eq!(events.len(), 2);
3826 assert!((events[0].time_secs).abs() < 1e-9);
3827 assert!((events[0].duration_secs - 2.0).abs() < 1e-9);
3828 assert!((events[1].time_secs - 2.0).abs() < 1e-9);
3829 assert!((events[1].duration_secs - 4.0).abs() < 1e-9);
3830 }
3831
3832 #[test]
3833 fn muted_part_produces_no_events() {
3834 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3835 score.parts[0].staves[0].measures[0].voices[0] =
3836 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3837 let options = PlaybackOptions {
3838 muted_parts: vec![0],
3839 ..Default::default()
3840 };
3841 assert!(to_playback_events(&score, &options).is_empty());
3842 }
3843
3844 #[test]
3845 fn part_index_field_set_correctly() {
3846 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3847 score.parts[0].staves[0].measures[0].voices[0] =
3848 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3849 let events = to_playback_events(&score, &opts(None));
3850 assert_eq!(events[0].part_index, 0);
3851 }
3852
3853 #[test]
3856 fn loop_region_filters_measures() {
3857 let mut score = Score::new("T", 120, 4, 4, 0, 3);
3859 for mi in 0..3 {
3860 score.parts[0].staves[0].measures[mi].voices[0] =
3861 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
3862 }
3863 let options = PlaybackOptions {
3864 loop_region: Some((1, 2)),
3865 ..Default::default()
3866 };
3867 let events = to_playback_events(&score, &options);
3868 assert_eq!(events.len(), 2);
3869 assert!((events[0].time_beats).abs() < 1e-9);
3871 }
3872
3873 #[test]
3874 fn loop_region_none_plays_all_measures() {
3875 let mut score = Score::new("T", 120, 4, 4, 0, 3);
3876 for mi in 0..3 {
3877 score.parts[0].staves[0].measures[mi].voices[0] =
3878 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
3879 }
3880 let events = to_playback_events(&score, &opts(None));
3881 assert_eq!(events.len(), 3);
3882 }
3883
3884 #[test]
3887 fn fermata_multiplier_extends_duration() {
3888 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3889 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3890 note.articulations
3891 .push(crate::model::notation::Articulation::Fermata);
3892 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3893 let options = PlaybackOptions {
3894 fermata_multiplier: 2.0,
3895 ..Default::default()
3896 };
3897 let events = to_playback_events(&score, &options);
3898 assert_eq!(events.len(), 1);
3899 assert!((events[0].duration_beats - 2.0).abs() < 1e-9);
3900 }
3901
3902 #[test]
3903 fn fermata_default_multiplier_is_1_5() {
3904 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3905 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3906 note.articulations
3907 .push(crate::model::notation::Articulation::Fermata);
3908 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3909 let events = to_playback_events(&score, &PlaybackOptions::default());
3910 assert_eq!(events.len(), 1);
3911 assert!((events[0].duration_beats - 1.5).abs() < 1e-9);
3912 }
3913
3914 #[test]
3915 fn fermata_hold_is_an_explicit_post_note_request_separate_from_extension() {
3916 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3917 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3918 note.articulations
3919 .push(crate::model::notation::Articulation::Fermata);
3920 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3921 let options = PlaybackOptions {
3922 fermata_multiplier: 2.0,
3923 fermata_hold_beats: 0.75,
3924 ..Default::default()
3925 };
3926 let events = to_playback_events(&score, &options);
3927 assert!((events[0].duration_beats - 2.0).abs() < 1e-9);
3928 assert!((events[0].post_note_pause_beats - 0.75).abs() < 1e-9);
3929 }
3930
3931 #[test]
3932 fn invalid_fermata_hold_is_safely_ignored() {
3933 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3934 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
3935 note.articulations
3936 .push(crate::model::notation::Articulation::Fermata);
3937 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
3938 let options = PlaybackOptions {
3939 fermata_hold_beats: f64::NAN,
3940 ..Default::default()
3941 };
3942 assert_eq!(
3943 to_playback_events(&score, &options)[0].post_note_pause_beats,
3944 0.0
3945 );
3946 }
3947
3948 #[test]
3949 fn non_fermata_note_unaffected_by_fermata_multiplier() {
3950 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3951 score.parts[0].staves[0].measures[0].voices[0] =
3952 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
3953 let options = PlaybackOptions {
3954 fermata_multiplier: 3.0,
3955 ..Default::default()
3956 };
3957 let events = to_playback_events(&score, &options);
3958 assert!((events[0].duration_beats - 1.0).abs() < 1e-9);
3959 }
3960
3961 #[test]
3964 fn swing_triplet_first_eighth_is_long() {
3965 let mut score = Score::new("T", 120, 4, 4, 0, 1);
3967 score.parts[0].staves[0].measures[0].voices[0] = vec![
3968 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
3969 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
3970 ];
3971 let options = PlaybackOptions {
3972 swing: Some(0.67),
3973 ..Default::default()
3974 };
3975 let events = to_playback_events(&score, &options);
3976 let e_c = events.iter().find(|e| e.pitch_midi == 60).unwrap();
3978 let e_d = events.iter().find(|e| e.pitch_midi == 62).unwrap();
3979 assert!(
3980 (e_c.duration_beats - 0.67).abs() < 1e-9,
3981 "first eighth should be 0.67"
3982 );
3983 assert!(
3984 (e_d.duration_beats - 0.33).abs() < 1e-9,
3985 "second eighth should be 0.33"
3986 );
3987 assert!(
3989 (e_d.time_beats - 0.67).abs() < 1e-9,
3990 "second note start should be at 0.67"
3991 );
3992 }
3993
3994 #[test]
3995 fn swing_does_not_stretch_an_unpaired_eighth() {
3996 let mut score = Score::new("unpaired", 120, 4, 4, 0, 1);
3997 score.parts[0].staves[0].measures[0].voices[0] = vec![
3998 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
3999 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
4000 ];
4001 let events = to_playback_events(
4002 &score,
4003 &PlaybackOptions {
4004 swing: Some(2.0 / 3.0),
4005 ..Default::default()
4006 },
4007 );
4008 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
4009 assert!((events[1].time_beats - 0.5).abs() < 1e-9);
4010 }
4011
4012 #[test]
4013 fn cue_note_does_not_sound_or_pair_with_a_written_eighth() {
4014 let mut score = Score::new("cue", 120, 4, 4, 0, 1);
4015 let mut cue = Note::new(Pitch::new(Step::D, 4), Duration::Eighth);
4016 cue.is_cue = true;
4017 score.parts[0].staves[0].measures[0].voices[0] = vec![
4018 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
4019 cue,
4020 Note::new(Pitch::new(Step::E, 4), Duration::Eighth),
4021 ];
4022 let events = to_playback_events(
4023 &score,
4024 &PlaybackOptions {
4025 swing: Some(2.0 / 3.0),
4026 ..Default::default()
4027 },
4028 );
4029 assert_eq!(events.len(), 2);
4030 assert_eq!(events[0].pitch_midi, 60);
4031 assert_eq!(events[0].duration_beats, 0.5);
4032 assert_eq!(events[1].pitch_midi, 64);
4033 assert_eq!(events[1].time_beats, 0.5);
4034 }
4035
4036 #[test]
4037 fn swing_requires_metric_alignment_and_a_matching_next_note() {
4038 let mut score = Score::new("metric boundary", 120, 4, 4, 0, 1);
4039 score.parts[0].staves[0].measures[0].voices[0] = vec![
4040 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
4041 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
4042 Note::new(Pitch::new(Step::E, 4), Duration::Eighth),
4043 Note::new(Pitch::new(Step::F, 4), Duration::Eighth),
4044 Note::new(Pitch::new(Step::G, 4), Duration::Eighth),
4045 Note::new(Pitch::new(Step::A, 4), Duration::Eighth),
4046 ];
4047 let events = to_playback_events(
4048 &score,
4049 &PlaybackOptions {
4050 swing: Some(2.0 / 3.0),
4051 ..Default::default()
4052 },
4053 );
4054 assert!((events[0].time_beats).abs() < 1e-9);
4055 assert!((events[0].duration_beats - 0.5).abs() < 1e-9);
4056 assert!((events[2].time_beats - 1.5).abs() < 1e-9);
4057 assert!((events[2].duration_beats - 0.5).abs() < 1e-9);
4058 assert!((events[3].time_beats - 2.0).abs() < 1e-9);
4059 assert!((events[3].duration_beats - 2.0 / 3.0).abs() < 1e-9);
4060 assert!((events[4].time_beats - 8.0 / 3.0).abs() < 1e-9);
4061 assert!((events[4].duration_beats - 1.0 / 3.0).abs() < 1e-9);
4062 assert!((events[5].time_beats - 3.0).abs() < 1e-9);
4063 assert!((events[5].duration_beats - 0.5).abs() < 1e-9);
4064 }
4065
4066 #[test]
4067 fn swing_keeps_the_offbeat_delay_after_an_eighth_rest() {
4068 let mut score = Score::new("rest swing", 120, 4, 4, 0, 1);
4069 score.parts[0].staves[0].measures[0].voices[0] = vec![
4070 Note::rest(Duration::Eighth),
4071 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
4072 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
4073 ];
4074 let events = to_playback_events(
4075 &score,
4076 &PlaybackOptions {
4077 swing: Some(2.0 / 3.0),
4078 ..Default::default()
4079 },
4080 );
4081 assert!((events[0].time_beats - 2.0 / 3.0).abs() < 1e-9);
4082 assert!((events[0].duration_beats - 1.0 / 3.0).abs() < 1e-9);
4083 assert!((events[1].time_beats - 1.0).abs() < 1e-9);
4084 }
4085
4086 #[test]
4087 fn invalid_swing_ratios_keep_straight_finite_timing() {
4088 let mut score = Score::new("invalid swing", 120, 4, 4, 0, 1);
4089 score.parts[0].staves[0].measures[0].voices[0] = vec![
4090 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
4091 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
4092 ];
4093 for ratio in [f64::NAN, f64::INFINITY, -1.0, 0.5, 1.0] {
4094 let events = to_playback_events(
4095 &score,
4096 &PlaybackOptions {
4097 swing: Some(ratio),
4098 ..Default::default()
4099 },
4100 );
4101 assert_eq!(events.len(), 2);
4102 assert_eq!(events[0].duration_beats, 0.5);
4103 assert_eq!(events[1].time_beats, 0.5);
4104 assert_eq!(events[1].duration_beats, 0.5);
4105 assert!(events.iter().all(|event| event.time_secs.is_finite()));
4106 }
4107 }
4108
4109 #[test]
4110 fn pickup_swing_uses_metric_position_in_the_full_bar() {
4111 let mut score = Score::new("pickup swing", 120, 4, 4, 0, 1);
4112 let measure = &mut score.parts[0].staves[0].measures[0];
4113 measure.actual_length = Some(crate::MeasureLength {
4114 numerator: 3,
4115 denominator: 8,
4116 });
4117 measure.voices[0] = vec![
4118 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
4119 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
4120 Note::new(Pitch::new(Step::E, 4), Duration::Eighth),
4121 ];
4122 let events = to_playback_events(
4123 &score,
4124 &PlaybackOptions {
4125 swing: Some(2.0 / 3.0),
4126 ..Default::default()
4127 },
4128 );
4129 assert_eq!(events[0].duration_beats, 0.5);
4130 assert!((events[1].time_beats - 0.5).abs() < 1e-9);
4131 assert!((events[1].duration_beats - 2.0 / 3.0).abs() < 1e-9);
4132 assert!((events[2].time_beats - 7.0 / 6.0).abs() < 1e-9);
4133 assert!((events[2].duration_beats - 1.0 / 3.0).abs() < 1e-9);
4134 }
4135
4136 #[test]
4137 fn swing_non_eighth_not_affected() {
4138 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4139 score.parts[0].staves[0].measures[0].voices[0] =
4140 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4141 let options = PlaybackOptions {
4142 swing: Some(0.67),
4143 ..Default::default()
4144 };
4145 let events = to_playback_events(&score, &options);
4146 assert!(
4147 (events[0].duration_beats - 1.0).abs() < 1e-9,
4148 "quarter note unaffected"
4149 );
4150 }
4151
4152 #[test]
4153 fn swing_none_is_straight() {
4154 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4155 score.parts[0].staves[0].measures[0].voices[0] =
4156 vec![Note::new(Pitch::new(Step::C, 4), Duration::Eighth)];
4157 let options = PlaybackOptions {
4158 swing: None,
4159 ..Default::default()
4160 };
4161 let events = to_playback_events(&score, &options);
4162 assert!(
4163 (events[0].duration_beats - 0.5).abs() < 1e-9,
4164 "no swing = straight eighth"
4165 );
4166 }
4167
4168 #[test]
4169 fn channel_matches_part_midi_channel() {
4170 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4171 score.parts[0].midi_channel = 3;
4172 score.parts[0].staves[0].measures[0].voices[0] =
4173 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4174 let events = to_playback_events(&score, &opts(None));
4175 assert_eq!(events[0].channel, 3);
4176 }
4177
4178 #[test]
4179 fn swing_unit_default_is_eighth() {
4180 assert_eq!(PlaybackOptions::default().swing_unit, Duration::Eighth);
4181 }
4182
4183 #[test]
4184 fn swing_unit_sixteenth() {
4185 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4186 score.parts[0].staves[0].measures[0].voices[0] = vec![
4187 Note::new(Pitch::new(Step::C, 4), Duration::Sixteenth),
4188 Note::new(Pitch::new(Step::D, 4), Duration::Sixteenth),
4189 ];
4190 let options = PlaybackOptions {
4191 swing: Some(0.67),
4192 swing_unit: Duration::Sixteenth,
4193 ..Default::default()
4194 };
4195 let events = to_playback_events(&score, &options);
4196 let e_c = events.iter().find(|e| e.pitch_midi == 60).unwrap();
4197 let e_d = events.iter().find(|e| e.pitch_midi == 62).unwrap();
4198 assert!(
4199 (e_c.duration_beats - 0.335).abs() < 1e-9,
4200 "first 16th should be 0.335"
4201 );
4202 assert!(
4203 (e_d.duration_beats - 0.165).abs() < 1e-9,
4204 "second 16th should be 0.165"
4205 );
4206 }
4207
4208 #[test]
4209 fn swing_resets_per_measure() {
4210 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4212 let pair = || {
4213 vec![
4214 Note::new(Pitch::new(Step::C, 4), Duration::Eighth),
4215 Note::new(Pitch::new(Step::D, 4), Duration::Eighth),
4216 ]
4217 };
4218 score.parts[0].staves[0].measures[0].voices[0] = pair();
4219 score.parts[0].staves[0].measures[1].voices[0] = pair();
4220 let options = PlaybackOptions {
4221 swing: Some(0.67),
4222 ..Default::default()
4223 };
4224 let events = to_playback_events(&score, &options);
4225 let durations: Vec<f64> = events.iter().map(|e| e.duration_beats).collect();
4227 assert!((durations[0] - 0.67).abs() < 1e-9, "m0 first note long");
4229 assert!((durations[1] - 0.33).abs() < 1e-9, "m0 second note short");
4231 assert!(
4233 (durations[2] - 0.67).abs() < 1e-9,
4234 "m1 first note long (reset)"
4235 );
4236 assert!((durations[3] - 0.33).abs() < 1e-9, "m1 second note short");
4238 }
4239
4240 #[test]
4241 fn multi_voice_events_preserve_source_voice_addresses() {
4242 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4243 score.parts[0].staves[0].measures[0].voices[0] =
4244 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4245 score.parts[0].staves[0].measures[0].voices[1] =
4246 vec![Note::new(Pitch::new(Step::E, 4), Duration::Quarter)];
4247
4248 let events = to_playback_events(&score, &PlaybackOptions::default());
4249 let addresses: Vec<&str> = events
4250 .iter()
4251 .filter_map(|event| event.address.as_deref())
4252 .collect();
4253 assert!(addresses.contains(&"0:0:0:0:0"));
4254 assert!(addresses.contains(&"0:0:0:1:0"));
4255 }
4256
4257 #[test]
4258 fn playback_exposes_original_musicxml_voice_number_alongside_slot_address() {
4259 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4260 let measure = &mut score.parts[0].staves[0].measures[0];
4261 measure.voices[1] = vec![Note::new(Pitch::new(Step::E, 4), Duration::Quarter)];
4262 measure.source_voice_numbers[1] = Some(5);
4263
4264 let events = to_playback_events(&score, &PlaybackOptions::default());
4265 let event = events
4266 .iter()
4267 .find(|event| event.address.as_deref() == Some("0:0:0:1:0"))
4268 .expect("event from slot 1");
4269 assert_eq!(event.source.as_ref().map(|source| source.voice), Some(1));
4270 assert_eq!(event.source_voice_number, Some(5));
4271 }
4272
4273 #[test]
4276 fn playback_position_at_zero_is_measure_0_beat_0() {
4277 let score = Score::new("T", 120, 4, 4, 0, 4);
4278 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 0.0).unwrap();
4279 assert_eq!(pos.measure_index, 0);
4280 assert!(pos.beat.abs() < 1e-9);
4281 }
4282
4283 #[test]
4284 fn playback_position_at_half_measure_is_beat_2() {
4285 let score = Score::new("T", 120, 4, 4, 0, 4);
4287 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 0.5).unwrap();
4288 assert_eq!(pos.measure_index, 0);
4289 assert!(
4290 (pos.beat - 1.0).abs() < 1e-9,
4291 "expected beat 1.0, got {}",
4292 pos.beat
4293 );
4294 }
4295
4296 #[test]
4297 fn playback_position_beyond_score_is_none() {
4298 let score = Score::new("T", 120, 4, 4, 0, 1);
4300 assert!(compute_playback_position(&score, &PlaybackOptions::default(), 10.0).is_none());
4301 }
4302
4303 #[test]
4304 fn playback_position_tempo_change_takes_effect() {
4305 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4308 score.parts[0].staves[0].measures[1].tempo = Some(60);
4309 let pos = compute_playback_position(&score, &PlaybackOptions::default(), 2.5).unwrap();
4310 assert_eq!(pos.measure_index, 1);
4311 assert!(
4312 (pos.beat - 0.5).abs() < 1e-9,
4313 "expected beat 0.5, got {}",
4314 pos.beat
4315 );
4316 }
4317
4318 #[test]
4319 fn playback_position_loop_region_starts_at_zero() {
4320 let score = Score::new("T", 120, 4, 4, 0, 4);
4322 let options = PlaybackOptions {
4323 loop_region: Some((1, 2)),
4324 ..Default::default()
4325 };
4326 let pos = compute_playback_position(&score, &options, 0.0).unwrap();
4327 assert_eq!(pos.measure_index, 1);
4328 assert!(pos.beat.abs() < 1e-9);
4329 }
4330
4331 #[test]
4334 fn metronome_injects_beat_events() {
4335 let score = Score::new("T", 120, 4, 4, 0, 1);
4337 let options = PlaybackOptions {
4338 metronome: Some(MetronomeConfig::default()),
4339 ..Default::default()
4340 };
4341 let events = to_playback_events(&score, &options);
4342 let metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
4343 assert_eq!(metro_events.len(), 4, "expected 4 metronome clicks in 4/4");
4344 }
4345
4346 #[test]
4347 fn metronome_accent_is_first_beat() {
4348 let score = Score::new("T", 120, 4, 4, 0, 1);
4349 let metro = MetronomeConfig::default();
4350 let options = PlaybackOptions {
4351 metronome: Some(metro.clone()),
4352 ..Default::default()
4353 };
4354 let events = to_playback_events(&score, &options);
4355 let mut metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
4356 metro_events.sort_by(|a, b| a.time_beats.partial_cmp(&b.time_beats).unwrap());
4357 assert_eq!(metro_events[0].pitch_midi, metro.accent_pitch);
4358 assert_eq!(metro_events[0].velocity, metro.accent_velocity);
4359 }
4360
4361 #[test]
4362 fn metronome_regular_beat_pitch() {
4363 let score = Score::new("T", 120, 4, 4, 0, 1);
4364 let metro = MetronomeConfig::default();
4365 let options = PlaybackOptions {
4366 metronome: Some(metro.clone()),
4367 ..Default::default()
4368 };
4369 let events = to_playback_events(&score, &options);
4370 let mut metro_events: Vec<_> = events.iter().filter(|e| e.is_metronome).collect();
4371 metro_events.sort_by(|a, b| a.time_beats.partial_cmp(&b.time_beats).unwrap());
4372 for ev in &metro_events[1..] {
4373 assert_eq!(ev.pitch_midi, metro.beat_pitch);
4374 assert_eq!(ev.velocity, metro.beat_velocity);
4375 }
4376 }
4377
4378 #[test]
4379 fn metronome_integrates_measure_tempo_ramp_and_carries_ending_tempo() {
4380 let mut score = Score::new("T", 120, 4, 4, 0, 2);
4381 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
4382 let options = PlaybackOptions {
4383 metronome: Some(MetronomeConfig::default()),
4384 ..Default::default()
4385 };
4386
4387 let mut clicks: Vec<_> = to_playback_events(&score, &options)
4388 .into_iter()
4389 .filter(|event| event.is_metronome)
4390 .collect();
4391 clicks.sort_by(|left, right| left.time_beats.total_cmp(&right.time_beats));
4392
4393 let expected_second_beat = tempo_ramp_seconds(120.0, Some(60.0), 4.0, 1.0);
4394 let expected_second_measure = tempo_ramp_seconds(120.0, Some(60.0), 4.0, 4.0);
4395 assert!((clicks[1].time_secs - expected_second_beat).abs() < 1e-9);
4396 assert!((clicks[4].time_secs - expected_second_measure).abs() < 1e-9);
4397 assert!((clicks[5].time_secs - (expected_second_measure + 1.0)).abs() < 1e-9);
4398 }
4399
4400 #[test]
4401 fn metronome_events_are_marked() {
4402 let score = Score::new("T", 120, 4, 4, 0, 1);
4403 let options = PlaybackOptions {
4404 metronome: Some(MetronomeConfig::default()),
4405 ..Default::default()
4406 };
4407 let events = to_playback_events(&score, &options);
4408 assert!(events.iter().any(|e| e.is_metronome));
4409 }
4410
4411 #[test]
4412 fn metronome_none_produces_no_extra_events() {
4413 let score = Score::new("T", 120, 4, 4, 0, 1);
4414 let events = to_playback_events(&score, &PlaybackOptions::default());
4415 assert!(events.iter().all(|e| !e.is_metronome));
4416 }
4417
4418 #[test]
4419 fn offline_render_manifest_uses_event_end_as_exact_frame_length() {
4420 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4421 score.parts[0].staves[0].measures[0].voices[0][0] =
4422 Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4423 let request = OfflineRenderRequest {
4424 format: OfflineRenderFormat::Flac,
4425 sample_rate_hz: 48_000,
4426 channels: 2,
4427 provider_identity: Some("test-provider@1".into()),
4428 ..Default::default()
4429 };
4430
4431 let manifest = build_offline_render_manifest(&score, &PlaybackOptions::default(), &request)
4432 .expect("valid render manifest");
4433 assert_eq!(manifest.contract_version, OFFLINE_RENDER_CONTRACT_VERSION);
4434 assert!(manifest.validate().is_ok());
4435 assert_eq!(manifest.format, OfflineRenderFormat::Flac);
4436 assert_eq!(manifest.duration_frames, 24_000);
4437 assert_eq!(manifest.events.len(), 1);
4438 assert_eq!(manifest.frame_events.len(), 1);
4439 assert_eq!(manifest.frame_events[0].start_frame, 0);
4440 assert_eq!(manifest.frame_events[0].duration_frames, 24_000);
4441 assert_eq!(manifest.frame_events[0].end_frame, 24_000);
4442 assert_eq!(manifest.playback_options, PlaybackOptions::default());
4443 assert_eq!(
4444 manifest.provider_identity.as_deref(),
4445 Some("test-provider@1")
4446 );
4447 }
4448
4449 #[test]
4450 fn offline_render_manifest_rejects_out_of_range_audio_shape() {
4451 let score = Score::new("T", 120, 4, 4, 0, 1);
4452 let request = OfflineRenderRequest {
4453 sample_rate_hz: 0,
4454 ..Default::default()
4455 };
4456 assert!(matches!(
4457 build_offline_render_manifest(&score, &PlaybackOptions::default(), &request),
4458 Err(crate::Error::InvalidOfflineRenderRequest)
4459 ));
4460 }
4461
4462 #[test]
4463 fn offline_render_manifest_selection_keeps_source_address_and_release_tail() {
4464 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4465 score.parts[0].staves[0].measures[0].voices[0] = vec![
4466 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
4467 Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
4468 ];
4469 let events = to_playback_events(&score, &PlaybackOptions::default());
4470 score.parts[0].midi_control_changes.push(MidiControlChange {
4471 tick: 480,
4472 channel: 0,
4473 controller: 7,
4474 value: 100,
4475 });
4476 let request = OfflineRenderRequest {
4477 sample_rate_hz: 8_000,
4478 release_tail_millis: 125,
4479 scope: OfflineRenderScope::Selection {
4480 addresses: vec![events[1].source.clone().expect("source address")],
4481 },
4482 ..Default::default()
4483 };
4484 let manifest = build_offline_render_manifest(&score, &PlaybackOptions::default(), &request)
4485 .expect("selection manifest");
4486 assert_eq!(manifest.events.len(), 1);
4487 assert_eq!(manifest.events[0].source, events[1].source);
4488 assert_eq!(manifest.frame_events[0].start_frame, 0);
4489 assert!(manifest.semantic_events.iter().any(|event| matches!(
4490 event.kind,
4491 OfflineRenderSemanticEventKind::Controller {
4492 channel: 0,
4493 controller: 7,
4494 value: 100
4495 }
4496 ) && event.frame == 0));
4497 assert_eq!(manifest.release_tail_frames, 1_000);
4498 assert_eq!(manifest.duration_frames, 5_000);
4499 }
4500
4501 #[test]
4502 fn offline_render_request_owned_options_override_legacy_argument() {
4503 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4504 score.parts[0].staves[0].measures[0].voices[0][0] =
4505 Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4506 let request_options = PlaybackOptions {
4507 bpm_override: Some(60),
4508 ..Default::default()
4509 };
4510 let manifest = build_offline_render_manifest(
4511 &score,
4512 &PlaybackOptions::default(),
4513 &OfflineRenderRequest {
4514 sample_rate_hz: 48_000,
4515 playback_options: Some(request_options.clone()),
4516 ..Default::default()
4517 },
4518 )
4519 .expect("request-owned playback options");
4520 assert_eq!(manifest.playback_options, request_options);
4521 assert_eq!(manifest.frame_events[0].duration_frames, 48_000);
4522 }
4523
4524 #[test]
4525 fn offline_manifest_projects_typed_midi_events_to_exact_frames() {
4526 let mut score = Score::new("MIDI semantics", 120, 4, 4, 0, 1);
4527 score.parts[0].midi_pitch_bends.push(MidiPitchBend {
4528 tick: 480,
4529 channel: 2,
4530 value: -512,
4531 });
4532 score.parts[0].midi_program_changes.push(MidiProgramChange {
4533 tick: 480,
4534 channel: 2,
4535 program: 41,
4536 });
4537 score.parts[0].midi_aftertouch.push(MidiAftertouch {
4538 tick: 480,
4539 channel: 2,
4540 key: Some(64),
4541 value: 88,
4542 });
4543 let manifest = build_offline_render_manifest(
4544 &score,
4545 &PlaybackOptions::default(),
4546 &OfflineRenderRequest {
4547 sample_rate_hz: 48_000,
4548 ..Default::default()
4549 },
4550 )
4551 .expect("manifest builds");
4552 for kind in [
4553 OfflineRenderSemanticEventKind::PitchBend {
4554 channel: 2,
4555 value: -512,
4556 },
4557 OfflineRenderSemanticEventKind::ProgramChange {
4558 channel: 2,
4559 program: 41,
4560 },
4561 OfflineRenderSemanticEventKind::Aftertouch {
4562 channel: 2,
4563 key: Some(64),
4564 value: 88,
4565 },
4566 ] {
4567 assert!(manifest.semantic_events.iter().any(|event| {
4568 event.kind == kind
4569 && event.frame == 24_000
4570 && event
4571 .measure
4572 .as_ref()
4573 .is_some_and(|address| address.part == 0 && address.measure == 0)
4574 }));
4575 }
4576 }
4577
4578 #[test]
4579 fn offline_manifest_frames_cover_repeat_ramp_fermata_pedal_bend_and_instrument_change() {
4580 let mut score = Score::new("fixture", 120, 4, 4, 0, 2);
4581 let mut first = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
4582 first.pedal_start = true;
4583 first.articulations = vec![Articulation::Fermata, Articulation::BreathMark];
4584 first.guitar_bend_curve = vec![GuitarBendPoint {
4585 position_per_mille: 500,
4586 alter_cents: 100,
4587 }];
4588 score.parts[0].staves[0].measures[0].voices[0] = vec![first];
4589 score.parts[0].staves[0].measures[0].barline_left =
4590 super::super::notation::Barline::RepeatStart;
4591 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
4592 score.parts[0].staves[0].measures[0].navigation = Some("Segno".to_string());
4593 score.parts[0].midi_control_changes.push(MidiControlChange {
4594 tick: 480,
4595 channel: 0,
4596 controller: 11,
4597 value: 96,
4598 });
4599
4600 let mut second = Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
4601 second.pedal_end = true;
4602 score.parts[0].staves[0].measures[1].voices[0] = vec![second];
4603 score.parts[0].staves[0].measures[1].barline_right =
4604 super::super::notation::Barline::RepeatEnd;
4605 let mut flute = crate::InstrumentDefinition::new("flute", "Flute");
4606 flute.midi_channel = 2;
4607 flute.midi_program = 73;
4608 score.parts[0].staves[0].measures[1].instrument_change = Some(flute);
4609
4610 let request = OfflineRenderRequest {
4611 sample_rate_hz: 48_000,
4612 playback_options: Some(PlaybackOptions {
4613 fermata_multiplier: 1.5,
4614 fermata_hold_beats: 0.25,
4615 ..Default::default()
4616 }),
4617 ..Default::default()
4618 };
4619 let manifest = build_offline_render_manifest(&score, &PlaybackOptions::default(), &request)
4620 .expect("fixture manifest");
4621 assert_eq!(manifest.frame_events.len(), 4);
4622 let measures: Vec<usize> = manifest
4623 .frame_events
4624 .iter()
4625 .map(|event| event.event.source.as_ref().unwrap().measure)
4626 .collect();
4627 assert_eq!(measures, vec![0, 1, 0, 1]);
4628 assert!(manifest.frame_events[0].event.pedal);
4629 assert_eq!(manifest.frame_events[0].event.post_note_pause_beats, 0.25);
4630 assert_eq!(manifest.frame_events[0].event.pitch_bend_curve.len(), 1);
4631 assert_eq!(
4632 manifest.frame_events[1].event.instrument_id.as_deref(),
4633 Some("flute")
4634 );
4635 assert_eq!(manifest.frame_events[3].event.program, 73);
4636 assert!(manifest.semantic_events.iter().any(|event| {
4637 matches!(
4638 &event.kind,
4639 OfflineRenderSemanticEventKind::InstrumentChange {
4640 instrument_id,
4641 channel: 2,
4642 program: 73,
4643 transpose_semitones: 0,
4644 } if instrument_id == "flute"
4645 ) && event
4646 .measure
4647 .as_ref()
4648 .is_some_and(|address| address.measure == 1)
4649 && event.frame == manifest.frame_events[1].start_frame
4650 }));
4651 assert!(manifest.semantic_events.iter().any(|event| matches!(
4652 event.kind,
4653 OfflineRenderSemanticEventKind::Pedal { down: true }
4654 ) && event.frame
4655 == manifest.frame_events[0].start_frame));
4656 assert!(manifest.semantic_events.iter().any(|event| matches!(
4657 event.kind,
4658 OfflineRenderSemanticEventKind::Pedal { down: false }
4659 ) && event.frame
4660 == manifest.frame_events[1].start_frame));
4661 assert!(manifest.semantic_events.iter().any(|event| matches!(
4662 event.kind,
4663 OfflineRenderSemanticEventKind::Articulation {
4664 articulation: Articulation::Fermata
4665 }
4666 )));
4667 assert!(manifest.semantic_events.iter().any(|event| matches!(
4668 event.kind,
4669 OfflineRenderSemanticEventKind::GuitarBend { .. }
4670 )));
4671 let tempo_events = manifest
4672 .semantic_events
4673 .iter()
4674 .filter(|event| matches!(event.kind, OfflineRenderSemanticEventKind::Tempo { .. }))
4675 .count();
4676 let navigation_events = manifest
4677 .semantic_events
4678 .iter()
4679 .filter(|event| {
4680 matches!(
4681 event.kind,
4682 OfflineRenderSemanticEventKind::Navigation { .. }
4683 )
4684 })
4685 .count();
4686 let control_frames: Vec<u64> = manifest
4687 .semantic_events
4688 .iter()
4689 .filter_map(|event| {
4690 matches!(
4691 event.kind,
4692 OfflineRenderSemanticEventKind::Controller {
4693 channel: 0,
4694 controller: 11,
4695 value: 96
4696 }
4697 )
4698 .then_some(event.frame)
4699 })
4700 .collect();
4701 assert_eq!(tempo_events, 2);
4702 assert_eq!(navigation_events, 2);
4703 assert_eq!(control_frames.len(), 2);
4704 let ramp_duration = tempo_ramp_seconds(120.0, Some(60.0), 4.0, 4.0);
4705 assert_eq!(
4706 control_frames[0],
4707 (tempo_ramp_seconds(120.0, Some(60.0), 4.0, 1.0) * 48_000.0).round() as u64
4708 );
4709 assert_eq!(control_frames[1], 373_084);
4710
4711 let expected_starts = [0.0, ramp_duration, ramp_duration + 4.0, ramp_duration + 8.0];
4712 for (frame_event, expected_secs) in manifest.frame_events.iter().zip(expected_starts) {
4713 assert_eq!(
4714 frame_event.start_frame,
4715 (expected_secs * 48_000.0).round() as u64
4716 );
4717 }
4718 }
4719
4720 #[test]
4721 fn offline_render_manifest_reports_unresolved_selection_address() {
4722 let score = Score::new("T", 120, 4, 4, 0, 1);
4723 let request = OfflineRenderRequest {
4724 scope: OfflineRenderScope::Selection {
4725 addresses: vec![NoteAddr {
4726 part: 0,
4727 staff: 0,
4728 measure: 0,
4729 voice: 0,
4730 note: 99,
4731 }],
4732 },
4733 ..Default::default()
4734 };
4735 let manifest = build_offline_render_manifest(&score, &PlaybackOptions::default(), &request)
4736 .expect("empty selection is representable");
4737 assert_eq!(manifest.events.len(), 0);
4738 assert_eq!(
4739 manifest.diagnostics,
4740 vec![
4741 OfflineRenderDiagnostic {
4742 kind: OfflineRenderDiagnosticKind::UnresolvedSelectionAddress,
4743 source: Some(NoteAddr {
4744 part: 0,
4745 staff: 0,
4746 measure: 0,
4747 voice: 0,
4748 note: 99,
4749 }),
4750 },
4751 OfflineRenderDiagnostic {
4752 kind: OfflineRenderDiagnosticKind::EmptySelection,
4753 source: None,
4754 },
4755 ]
4756 );
4757 }
4758
4759 #[test]
4760 fn offline_render_manifest_v1_json_migrates_with_v2_defaults() {
4761 let legacy = serde_json::json!({
4762 "contract_version": 1,
4763 "playback_contract_version": 2,
4764 "format": "Wav",
4765 "sample_rate_hz": 48_000,
4766 "channels": 2,
4767 "duration_frames": 0,
4768 "events": []
4769 });
4770 let manifest: OfflineRenderManifest =
4771 serde_json::from_value(legacy).expect("v1 manifest remains readable");
4772 assert_eq!(manifest.contract_version, 1);
4773 assert_eq!(manifest.scope, OfflineRenderScope::FullScore);
4774 assert_eq!(manifest.release_tail_frames, 0);
4775 assert_eq!(manifest.playback_options, PlaybackOptions::default());
4776 assert!(manifest.frame_events.is_empty());
4777 assert!(manifest.diagnostics.is_empty());
4778 assert!(manifest.validate().is_ok());
4779 }
4780
4781 #[test]
4782 fn offline_render_manifest_validation_rejects_tampered_frame_and_tail_boundaries() {
4783 let mut score = Score::new("T", 120, 4, 4, 0, 1);
4784 score.parts[0].staves[0].measures[0].voices[0] =
4785 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4786 let manifest = build_offline_render_manifest(
4787 &score,
4788 &PlaybackOptions::default(),
4789 &OfflineRenderRequest {
4790 sample_rate_hz: 48_000,
4791 release_tail_millis: 100,
4792 ..Default::default()
4793 },
4794 )
4795 .expect("manifest builds");
4796 assert!(manifest.validate().is_ok());
4797
4798 let mut invalid_tail = manifest.clone();
4799 invalid_tail.duration_frames = invalid_tail.duration_frames.saturating_sub(1);
4800 assert!(matches!(
4801 invalid_tail.validate(),
4802 Err(crate::Error::InvalidOfflineRenderRequest)
4803 ));
4804
4805 let mut invalid_frame = manifest;
4806 invalid_frame.frame_events[0].end_frame =
4807 invalid_frame.frame_events[0].end_frame.saturating_add(1);
4808 assert!(matches!(
4809 invalid_frame.validate(),
4810 Err(crate::Error::InvalidOfflineRenderRequest)
4811 ));
4812 }
4813
4814 #[test]
4815 fn routing_manifest_prefers_instrument_bus_and_isolates_metronome() {
4816 let mut score = Score::new("routing", 120, 4, 4, 0, 1);
4817 let mut instrument = crate::InstrumentDefinition::new("flute", "Flute");
4818 instrument.midi_channel = 2;
4819 instrument.midi_program = 73;
4820 score.parts[0].instrument = Some(instrument);
4821 score.parts[0].staves[0].measures[0].voices[0] =
4822 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4823 let options = PlaybackOptions {
4824 metronome: Some(MetronomeConfig::default()),
4825 ..Default::default()
4826 };
4827 let mut config = PlaybackRoutingConfig {
4828 provider_identity: Some("sf2-host@1".into()),
4829 ..Default::default()
4830 };
4831 config.part_buses.insert(0, "parts".into());
4832 config
4833 .instrument_buses
4834 .insert("flute".into(), "winds".into());
4835 config.aux_sends.push(PlaybackAuxSend {
4836 source_bus: "winds".into(),
4837 destination_bus: "reverb".into(),
4838 gain_db: -12.0,
4839 });
4840 config.effect_routes.push(PlaybackEffectRoute {
4841 bus_id: "reverb".into(),
4842 effect_id: "convolution".into(),
4843 enabled: true,
4844 });
4845
4846 let manifest = build_playback_routing_manifest(&score, &options, &config)
4847 .expect("valid routing manifest");
4848 assert_eq!(manifest.contract_version, PLAYBACK_ROUTING_CONTRACT_VERSION);
4849 assert_eq!(manifest.provider_identity.as_deref(), Some("sf2-host@1"));
4850 assert!(manifest.routes.iter().any(|route| {
4851 route.instrument_id.as_deref() == Some("flute") && route.bus_id == "winds"
4852 }));
4853 assert!(
4854 manifest
4855 .routes
4856 .iter()
4857 .any(|route| route.is_metronome && route.bus_id == "metronome")
4858 );
4859 assert_eq!(manifest.aux_sends.len(), 1);
4860 assert_eq!(manifest.effect_routes.len(), 1);
4861 }
4862
4863 #[test]
4864 fn routing_manifest_rejects_unsafe_bus_and_non_finite_send_gain() {
4865 let score = Score::new("routing", 120, 4, 4, 0, 1);
4866 let config = PlaybackRoutingConfig {
4867 master_bus: "not a bus".into(),
4868 ..Default::default()
4869 };
4870 assert!(matches!(
4871 build_playback_routing_manifest(&score, &PlaybackOptions::default(), &config),
4872 Err(crate::Error::InvalidPlaybackRouting)
4873 ));
4874
4875 let mut config = PlaybackRoutingConfig::default();
4876 config.aux_sends.push(PlaybackAuxSend {
4877 source_bus: "part".into(),
4878 destination_bus: "master".into(),
4879 gain_db: f64::NAN,
4880 });
4881 assert!(matches!(
4882 build_playback_routing_manifest(&score, &PlaybackOptions::default(), &config),
4883 Err(crate::Error::InvalidPlaybackRouting)
4884 ));
4885 }
4886
4887 #[test]
4888 fn timing_corpus_reports_score_schedule_results_without_audio_claims() {
4889 let mut score = Score::new("corpus", 120, 4, 4, 0, 1);
4890 score.parts[0].staves[0].measures[0].voices[0] =
4891 vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
4892 let expected_events = to_playback_events(&score, &PlaybackOptions::default());
4893 let report = evaluate_playback_timing_corpus(&[PlaybackTimingCase {
4894 id: "quarter-note".into(),
4895 score,
4896 options: PlaybackOptions::default(),
4897 expected_events,
4898 tolerance: PlaybackTimingTolerance::default(),
4899 }])
4900 .expect("valid timing corpus");
4901 assert_eq!(
4902 report.contract_version,
4903 PLAYBACK_TIMING_CORPUS_CONTRACT_VERSION
4904 );
4905 assert_eq!(report.total_cases, 1);
4906 assert_eq!(report.passed_cases, 1);
4907 assert!(report.cases[0].report.within_tolerance);
4908 }
4909
4910 #[test]
4911 fn timing_corpus_rejects_empty_or_duplicate_case_ids() {
4912 assert!(matches!(
4913 evaluate_playback_timing_corpus(&[]),
4914 Err(crate::Error::InvalidPlaybackTimingCorpus)
4915 ));
4916 let score = Score::new("corpus", 120, 4, 4, 0, 1);
4917 let case = PlaybackTimingCase {
4918 id: "duplicate".into(),
4919 expected_events: to_playback_events(&score, &PlaybackOptions::default()),
4920 score,
4921 options: PlaybackOptions::default(),
4922 tolerance: PlaybackTimingTolerance::default(),
4923 };
4924 assert!(matches!(
4925 evaluate_playback_timing_corpus(&[case.clone(), case]),
4926 Err(crate::Error::InvalidPlaybackTimingCorpus)
4927 ));
4928 }
4929
4930 fn comparison_event(time_secs: f64, duration_secs: f64) -> PlaybackEvent {
4931 PlaybackEvent {
4932 address: Some("0:0:0:0:0".into()),
4933 source: Some(NoteAddr {
4934 part: 0,
4935 staff: 0,
4936 measure: 0,
4937 voice: 0,
4938 note: 0,
4939 }),
4940 source_voice_number: None,
4941 time_beats: 0.0,
4942 time_secs,
4943 pitch_midi: 60,
4944 pitch_midi_cents: 6000,
4945 pitch_bend_curve: Vec::new(),
4946 post_note_pause_beats: 0.0,
4947 articulations: Vec::new(),
4948 chord_symbol: None,
4949 guitar_technique: None,
4950 velocity: 64,
4951 duration_beats: 1.0,
4952 duration_secs,
4953 pedal: false,
4954 part_index: 0,
4955 channel: 0,
4956 program: 0,
4957 instrument_id: None,
4958 is_metronome: false,
4959 }
4960 }
4961
4962 #[test]
4963 fn legacy_playback_event_json_defaults_bend_curve() {
4964 let event: PlaybackEvent = serde_json::from_str(
4965 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}"#,
4966 )
4967 .expect("legacy playback event remains readable");
4968 assert!(event.pitch_bend_curve.is_empty());
4969 assert_eq!(event.guitar_technique, None);
4970 }
4971
4972 #[test]
4973 fn tempo_ramp_uses_integrated_event_timing_and_carries_target_forward() {
4974 let mut score = Score::new("ramp", 120, 4, 4, 0, 2);
4975 for measure in &mut score.parts[0].staves[0].measures {
4976 measure.voices[0] = vec![
4977 crate::Note::new(crate::Pitch::new(crate::Step::C, 4), crate::Duration::Half),
4978 crate::Note::new(crate::Pitch::new(crate::Step::D, 4), crate::Duration::Half),
4979 ];
4980 }
4981 score.parts[0].staves[0].measures[0].tempo_ramp_to = Some(60);
4982 let events = to_playback_events(&score, &PlaybackOptions::default());
4983 assert_eq!(events.len(), 4);
4984 let expected_measure_secs = 4.0 * 60.0 / (60.0 - 120.0) * (60.0f64 / 120.0).ln();
4985 assert!((events[2].time_secs - expected_measure_secs).abs() < 1e-9);
4986 assert!((events[0].duration_secs - 1.1507282898).abs() < 1e-6);
4987 assert!((events[2].duration_secs - 2.0).abs() < 1e-9);
4988 }
4989
4990 #[test]
4991 fn breath_and_caesura_expose_deterministic_post_note_pause() {
4992 let mut score = Score::new("pause", 120, 4, 4, 0, 1);
4993 let mut breath = crate::Note::new(
4994 crate::Pitch::new(crate::Step::C, 4),
4995 crate::Duration::Quarter,
4996 );
4997 breath.articulations.push(Articulation::BreathMark);
4998 let mut caesura = crate::Note::new(
4999 crate::Pitch::new(crate::Step::D, 4),
5000 crate::Duration::Quarter,
5001 );
5002 caesura
5003 .articulations
5004 .extend([Articulation::BreathMark, Articulation::Caesura]);
5005 score.parts[0].staves[0].measures[0].voices[0] = vec![breath, caesura];
5006
5007 let events = to_playback_events(&score, &PlaybackOptions::default());
5008 assert_eq!(events.len(), 2);
5009 assert_eq!(events[0].post_note_pause_beats, 0.25);
5010 assert_eq!(events[1].post_note_pause_beats, 0.5);
5011 assert_eq!(
5012 events[1].articulations,
5013 vec![Articulation::BreathMark, Articulation::Caesura]
5014 );
5015 }
5016
5017 #[test]
5018 fn measure_instrument_change_resolves_on_playback_events() {
5019 let mut score = Score::new("instrument", 120, 4, 4, 0, 2);
5020 let mut base = crate::InstrumentDefinition::new("piano", "Piano");
5021 base.midi_channel = 1;
5022 base.midi_program = 0;
5023 score.parts[0].instrument = Some(base);
5024 let mut change = crate::InstrumentDefinition::new("flute", "Flute");
5025 change.midi_channel = 2;
5026 change.midi_program = 73;
5027 change.transpose_semitones = -2;
5028 score.parts[0].staves[0].measures[1].instrument_change = Some(change);
5029 for measure in &mut score.parts[0].staves[0].measures {
5030 measure.voices[0] = vec![crate::Note::new(
5031 crate::Pitch::new(crate::Step::C, 4),
5032 crate::Duration::Whole,
5033 )];
5034 }
5035
5036 let events = to_playback_events(&score, &PlaybackOptions::default());
5037 assert_eq!(events.len(), 2);
5038 assert_eq!(
5039 (
5040 events[0].channel,
5041 events[0].program,
5042 events[0].instrument_id.as_deref()
5043 ),
5044 (1, 0, Some("piano"))
5045 );
5046 assert_eq!(
5047 (
5048 events[1].channel,
5049 events[1].program,
5050 events[1].instrument_id.as_deref()
5051 ),
5052 (2, 73, Some("flute"))
5053 );
5054 assert_eq!(events[0].pitch_midi, 60);
5055 assert_eq!(events[1].pitch_midi, 58);
5056 }
5057
5058 #[test]
5059 fn authored_midi_program_change_selects_the_next_note_at_its_source_tick() {
5060 let mut score = Score::new("program", 120, 4, 4, 0, 1);
5061 score.parts[0].staves[0].measures[0].voices[0] = vec![
5062 Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
5063 Note::new(Pitch::new(Step::E, 4), Duration::Quarter),
5064 ];
5065 score.parts[0].midi_program_changes = vec![
5066 MidiProgramChange {
5067 tick: 480,
5068 channel: 1,
5069 program: 73,
5070 },
5071 MidiProgramChange {
5072 tick: 480,
5073 channel: 0,
5074 program: 40,
5075 },
5076 ];
5077 let events = to_playback_events(&score, &PlaybackOptions::default());
5078 assert_eq!(events.len(), 2);
5079 assert_eq!((events[0].program, events[1].program), (0, 40));
5080 assert_eq!((events[0].time_secs, events[1].time_secs), (0.0, 0.5));
5081 }
5082
5083 #[test]
5084 fn generic_playback_event_preserves_authored_guitar_technique() {
5085 let mut score = Score::new("technique", 120, 4, 4, 0, 1);
5086 let mut note = Note::new(Pitch::new(Step::E, 4), Duration::Quarter);
5087 note.guitar_technique = Some(GuitarTechnique::HammerOn);
5088 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
5089
5090 let event = to_playback_events(&score, &PlaybackOptions::default())
5091 .into_iter()
5092 .next()
5093 .expect("one note event");
5094 assert_eq!(event.guitar_technique, Some(GuitarTechnique::HammerOn));
5095 }
5096
5097 #[test]
5098 fn playback_timing_comparison_reports_tolerance_and_identity() {
5099 let expected = [comparison_event(1.0, 0.5)];
5100 let actual = [comparison_event(1.004, 0.503)];
5101 let report = compare_playback_timing(
5102 &expected,
5103 &actual,
5104 &PlaybackTimingTolerance {
5105 start_secs: 0.005,
5106 duration_secs: 0.005,
5107 },
5108 )
5109 .expect("comparison");
5110 assert!(report.within_tolerance);
5111 assert_eq!(report.matched_events, 1);
5112 assert_eq!(
5113 report.contract_version,
5114 PLAYBACK_COMPARISON_CONTRACT_VERSION
5115 );
5116
5117 let mut typed_only = comparison_event(1.004, 0.503);
5118 typed_only.address = None;
5119 let report = compare_playback_timing(
5120 &expected,
5121 &[typed_only],
5122 &PlaybackTimingTolerance::default(),
5123 )
5124 .expect("typed source comparison");
5125 assert_eq!(report.matched_events, 1);
5126
5127 let actual = [comparison_event(1.02, 0.6)];
5128 let report =
5129 compare_playback_timing(&expected, &actual, &PlaybackTimingTolerance::default())
5130 .expect("comparison");
5131 assert!(!report.within_tolerance);
5132 assert!(
5133 report
5134 .mismatches
5135 .iter()
5136 .any(|m| matches!(m, PlaybackTimingMismatch::StartTime { index: 0, .. }))
5137 );
5138 assert!(
5139 report
5140 .mismatches
5141 .iter()
5142 .any(|m| matches!(m, PlaybackTimingMismatch::Duration { index: 0, .. }))
5143 );
5144 }
5145
5146 #[test]
5147 fn playback_timing_comparison_rejects_instrument_route_mismatch() {
5148 let expected = comparison_event(0.0, 0.5);
5149 let mut actual = expected.clone();
5150 actual.program = 73;
5151 assert!(
5152 compare_playback_timing(&[expected], &[actual], &PlaybackTimingTolerance::default(),)
5153 .expect("comparison")
5154 .mismatches
5155 .iter()
5156 .any(|mismatch| matches!(
5157 mismatch,
5158 PlaybackTimingMismatch::EventIdentity { index: 0 }
5159 ))
5160 );
5161 }
5162
5163 #[test]
5164 fn playback_timing_comparison_rejects_invalid_tolerance() {
5165 let error = compare_playback_timing(
5166 &[],
5167 &[],
5168 &PlaybackTimingTolerance {
5169 start_secs: -0.1,
5170 duration_secs: 0.0,
5171 },
5172 );
5173 assert!(matches!(
5174 error,
5175 Err(crate::Error::InvalidPlaybackComparison)
5176 ));
5177 }
5178
5179 #[test]
5180 fn tablature_performance_projection_keeps_position_and_reports_pitch_error() {
5181 let mut score = Score::new("Tab", 120, 4, 4, 0, 1);
5182 score.parts[0].staves[0].tablature = Some(super::super::notation::TablatureConfig {
5183 lines: 6,
5184 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5185 capo: 0,
5186 });
5187 let notes = &mut score.parts[0].staves[0].measures[0].voices[0];
5188 *notes = vec![crate::Note::new(
5189 crate::Pitch::new(crate::Step::C, 4),
5190 crate::Duration::Quarter,
5191 )];
5192 notes[0].tab_position = Some(super::super::notation::TabPosition {
5193 string: 1,
5194 fret: 20,
5195 });
5196 notes[0].guitar_technique = Some(super::super::notation::GuitarTechnique::Bend);
5197 notes[0].guitar_bend_alter_cents = Some(150);
5198 notes[0].guitar_bend_curve = vec![
5199 super::super::score::GuitarBendPoint {
5200 position_per_mille: 0,
5201 alter_cents: 0,
5202 },
5203 super::super::score::GuitarBendPoint {
5204 position_per_mille: 500,
5205 alter_cents: 150,
5206 },
5207 super::super::score::GuitarBendPoint {
5208 position_per_mille: 1_000,
5209 alter_cents: 0,
5210 },
5211 ];
5212 let report = project_tablature_performance(&score, &PlaybackOptions::default())
5213 .expect("tablature projection");
5214 assert_eq!(report.events.len(), 1);
5215 assert_eq!(report.events[0].string, 1);
5216 assert_eq!(report.events[0].fret, 20);
5217 assert_eq!(
5218 report.events[0].technique,
5219 Some(super::super::notation::GuitarTechnique::Bend)
5220 );
5221 assert_eq!(report.events[0].bend_alter_cents, Some(150));
5222 assert_eq!(report.events[0].bend_curve.len(), 3);
5223 assert_eq!(report.events[0].bend_curve[1].position_per_mille, 500);
5224 assert_eq!(report.events[0].bend_curve[1].alter_cents, 150);
5225 assert_eq!(
5226 report.events[0].playback.pitch_bend_curve,
5227 report.events[0].bend_curve
5228 );
5229 assert!(report.diagnostics.is_empty());
5230
5231 score.parts[0].staves[0].measures[0].voices[0][0].tab_position =
5232 Some(super::super::notation::TabPosition {
5233 string: 1,
5234 fret: 19,
5235 });
5236 let report = project_tablature_performance(&score, &PlaybackOptions::default())
5237 .expect("tablature projection");
5238 assert_eq!(report.events[0].pitch_error_cents, 100);
5239 assert!(matches!(
5240 report.diagnostics[0],
5241 TablaturePerformanceDiagnostic::PitchMismatch {
5242 error_cents: 100,
5243 ..
5244 }
5245 ));
5246 }
5247
5248 #[test]
5249 fn tablature_performance_projection_does_not_invent_missing_positions() {
5250 let mut score = Score::new("Tab", 120, 4, 4, 0, 1);
5251 score.parts[0].staves[0].tablature = Some(super::super::notation::TablatureConfig {
5252 lines: 6,
5253 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5254 capo: 0,
5255 });
5256 score.parts[0].staves[0].measures[0].voices[0].push(crate::Note::new(
5257 crate::Pitch::new(crate::Step::C, 4),
5258 crate::Duration::Quarter,
5259 ));
5260 let report = project_tablature_performance(&score, &PlaybackOptions::default())
5261 .expect("tablature projection");
5262 assert!(report.events.is_empty());
5263 assert!(matches!(
5264 report.diagnostics[0],
5265 TablaturePerformanceDiagnostic::MissingPosition { .. }
5266 ));
5267 }
5268
5269 #[test]
5270 fn tablature_performance_event_accepts_legacy_json_without_technique() {
5271 let mut score = Score::new("Tab", 120, 4, 4, 0, 1);
5272 score.parts[0].staves[0].tablature = Some(super::super::notation::TablatureConfig {
5273 lines: 6,
5274 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5275 capo: 0,
5276 });
5277 let mut note = crate::Note::new(
5278 crate::Pitch::new(crate::Step::C, 4),
5279 crate::Duration::Quarter,
5280 );
5281 note.tab_position = Some(super::super::notation::TabPosition {
5282 string: 1,
5283 fret: 20,
5284 });
5285 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
5286 let report = project_tablature_performance(&score, &PlaybackOptions::default())
5287 .expect("tablature projection");
5288 let mut legacy = serde_json::to_value(&report.events[0]).expect("event JSON");
5289 legacy
5290 .as_object_mut()
5291 .expect("event object")
5292 .remove("technique");
5293 let restored: TablaturePerformanceEvent =
5294 serde_json::from_value(legacy).expect("legacy event JSON");
5295 assert_eq!(restored.technique, None);
5296 }
5297
5298 #[test]
5299 fn tablature_round_trip_report_preserves_authored_positions() {
5300 let mut score = Score::new("Tab", 120, 4, 4, 0, 1);
5301 score.parts[0].staves[0].tablature = Some(super::super::notation::TablatureConfig {
5302 lines: 6,
5303 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5304 capo: 2,
5305 });
5306 let mut note = crate::Note::new(
5307 crate::Pitch::new(crate::Step::C, 4),
5308 crate::Duration::Quarter,
5309 );
5310 note.tab_positions = vec![crate::TabPosition { string: 5, fret: 1 }];
5311 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
5312 let report = tablature_round_trip_report(&score).expect("tab round-trip report");
5313 assert!(report.equivalent);
5314 assert_eq!(report.checked_notes, 1);
5315 assert_eq!(report.positioned_notes, 1);
5316 assert!(report.diagnostics.is_empty());
5317 }
5318
5319 #[test]
5320 fn tablature_performance_uses_measure_local_tuning_change() {
5321 let mut score = Score::new("Local tuning", 120, 4, 4, 0, 2);
5322 let staff = &mut score.parts[0].staves[0];
5323 staff.tablature = Some(super::super::notation::TablatureConfig {
5324 lines: 6,
5325 tuning_midi: vec![40, 45, 50, 55, 59, 64],
5326 capo: 0,
5327 });
5328 staff.measures[1].tablature_change = Some(super::super::notation::TablatureConfig {
5329 lines: 6,
5330 tuning_midi: vec![38, 45, 50, 55, 59, 64],
5331 capo: 0,
5332 });
5333 let mut note = Note::new(Pitch::new(Step::C, 3), Duration::Quarter);
5334 note.tab_position = Some(TabPosition {
5335 string: 1,
5336 fret: 10,
5337 });
5338 staff.measures[1].voices[0] = vec![note];
5339
5340 let report = project_tablature_performance(&score, &PlaybackOptions::default())
5341 .expect("tablature projection");
5342 assert_eq!(report.events.len(), 1);
5343 assert_eq!(report.events[0].expected_pitch_midi_cents, 4_800);
5344 assert!(!report.diagnostics.iter().any(|diagnostic| matches!(
5345 diagnostic,
5346 TablaturePerformanceDiagnostic::PitchMismatch { .. }
5347 )));
5348 }
5349}