1use super::notation::{
9 Barline, Clef, FiguredBassFigure, KeySignature, StyledText, TablatureConfig, TimeSignature,
10};
11use super::score::{
12 HarpPedalDiagram, InstrumentDefinition, Measure, MidMeasureClef, NotationSpanner, Note,
13 NoteAddr, Score, VoltaBracket,
14};
15use crate::Error;
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeSet;
18
19pub const SCORE_FRAGMENT_CONTRACT_VERSION: u16 = 3;
21pub const MIN_SUPPORTED_SCORE_FRAGMENT_CONTRACT_VERSION: u16 = 1;
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct ScoreFragmentSelection {
31 pub start: NoteAddr,
32 pub end: NoteAddr,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ScoreFragment {
39 pub contract_version: u16,
40 #[serde(default)]
41 pub voices: Vec<ScoreFragmentVoice>,
42 #[serde(default)]
43 pub spanners: Vec<NotationSpanner>,
44 #[serde(default)]
45 pub diagnostics: Vec<ScoreFragmentDiagnostic>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ScoreFragmentVoice {
53 pub relative_part: usize,
54 pub relative_staff: usize,
55 pub relative_voice: usize,
56 #[serde(default)]
57 pub measures: Vec<ScoreFragmentMeasure>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ScoreFragmentMeasure {
63 pub relative_measure: usize,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub source_voice_number: Option<u32>,
69 #[serde(default)]
70 pub notes: Vec<Note>,
71 #[serde(default)]
74 pub cross_staff_targets: Vec<Option<ScoreFragmentCrossStaffTarget>>,
75 #[serde(default)]
79 pub attributes: ScoreFragmentMeasureAttributes,
80}
81
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
89pub struct ScoreFragmentMeasureAttributes {
90 #[serde(default)]
91 pub present: bool,
92 #[serde(default)]
93 pub time_sig: Option<TimeSignature>,
94 #[serde(default)]
95 pub key_sig: Option<KeySignature>,
96 #[serde(default)]
97 pub clef: Option<Clef>,
98 #[serde(default, skip_serializing_if = "Vec::is_empty")]
99 pub mid_clefs: Vec<MidMeasureClef>,
100 #[serde(default)]
101 pub tempo: Option<u16>,
102 #[serde(default)]
103 pub tempo_ramp_to: Option<u16>,
104 #[serde(default)]
105 pub instrument_change: Option<InstrumentDefinition>,
106 #[serde(default)]
107 pub tablature_change: Option<TablatureConfig>,
108 #[serde(default)]
109 pub barline_left: Option<Barline>,
110 #[serde(default)]
111 pub barline_right: Option<Barline>,
112 #[serde(default)]
113 pub volta: Option<VoltaBracket>,
114 #[serde(default)]
115 pub tempo_text: Option<String>,
116 #[serde(default)]
117 pub rehearsal: Option<String>,
118 #[serde(default)]
119 pub navigation: Option<String>,
120 #[serde(default)]
121 pub expression_text: Option<String>,
122 #[serde(default)]
123 pub texts: Vec<StyledText>,
124 #[serde(default)]
125 pub figured_bass: Vec<FiguredBassFigure>,
126 #[serde(default)]
127 pub harp_pedal_diagrams: Vec<HarpPedalDiagram>,
128 #[serde(default)]
129 pub multi_rest_count: Option<u8>,
130 #[serde(default)]
131 pub system_break: bool,
132 #[serde(default)]
133 pub page_break: bool,
134 #[serde(default)]
135 pub section_break: bool,
136}
137
138impl ScoreFragmentMeasureAttributes {
139 pub(crate) fn from_measure(measure: &Measure) -> Self {
140 Self {
141 present: true,
142 time_sig: measure.time_sig.clone(),
143 key_sig: measure.key_sig.clone(),
144 clef: measure.clef.clone(),
145 mid_clefs: measure.mid_clefs.clone(),
146 tempo: measure.tempo,
147 tempo_ramp_to: measure.tempo_ramp_to,
148 instrument_change: measure.instrument_change.clone(),
149 tablature_change: measure.tablature_change.clone(),
150 barline_left: Some(measure.barline_left.clone()),
151 barline_right: Some(measure.barline_right.clone()),
152 volta: measure.volta.clone(),
153 tempo_text: measure.tempo_text.clone(),
154 rehearsal: measure.rehearsal.clone(),
155 navigation: measure.navigation.clone(),
156 expression_text: measure.expression_text.clone(),
157 texts: measure.texts.clone(),
158 figured_bass: measure.figured_bass.clone(),
159 harp_pedal_diagrams: measure.harp_pedal_diagrams.clone(),
160 multi_rest_count: measure.multi_rest_count,
161 system_break: measure.system_break,
162 page_break: measure.page_break,
163 section_break: measure.section_break,
164 }
165 }
166
167 pub fn apply_to_measure(&self, measure: &mut Measure) {
170 if !self.present {
171 return;
172 }
173 measure.time_sig = self.time_sig.clone();
174 measure.key_sig = self.key_sig.clone();
175 measure.clef = self.clef.clone();
176 measure.mid_clefs = self.mid_clefs.clone();
177 measure.tempo = self.tempo;
178 measure.tempo_ramp_to = self.tempo_ramp_to;
179 measure.instrument_change = self.instrument_change.clone();
180 measure.tablature_change = self.tablature_change.clone();
181 if let Some(barline) = &self.barline_left {
182 measure.barline_left = barline.clone();
183 }
184 if let Some(barline) = &self.barline_right {
185 measure.barline_right = barline.clone();
186 }
187 measure.volta = self.volta.clone();
188 measure.tempo_text = self.tempo_text.clone();
189 measure.rehearsal = self.rehearsal.clone();
190 measure.navigation = self.navigation.clone();
191 measure.expression_text = self.expression_text.clone();
192 measure.texts = self.texts.clone();
193 measure.figured_bass = self.figured_bass.clone();
194 measure.harp_pedal_diagrams = self.harp_pedal_diagrams.clone();
195 measure.multi_rest_count = self.multi_rest_count;
196 measure.system_break = self.system_break;
197 measure.page_break = self.page_break;
198 measure.section_break = self.section_break;
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct ScoreFragmentCrossStaffTarget {
205 pub staff_offset: i64,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub target_voice: Option<usize>,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
212#[serde(rename_all = "kebab-case")]
213pub enum ScoreFragmentDiagnostic {
214 PartialSpanner { id: String },
217}
218
219pub fn extract_score_fragment(
225 score: &Score,
226 selections: &[ScoreFragmentSelection],
227) -> Result<ScoreFragment, Error> {
228 if selections.is_empty() {
229 return Err(Error::InvalidCommand(
230 "score fragment requires at least one voice selection".into(),
231 ));
232 }
233
234 let base_part = selections
235 .iter()
236 .map(|selection| selection.start.part)
237 .min()
238 .ok_or_else(|| Error::InvalidCommand("score fragment requires selections".into()))?;
239 let base_staff = selections
240 .iter()
241 .map(|selection| selection.start.staff)
242 .min()
243 .ok_or_else(|| Error::InvalidCommand("score fragment requires selections".into()))?;
244 let base_measure = selections
245 .iter()
246 .map(|selection| selection.start.measure.min(selection.end.measure))
247 .min()
248 .ok_or_else(|| Error::InvalidCommand("score fragment requires selections".into()))?;
249 let base_voice = selections
250 .iter()
251 .map(|selection| selection.start.voice)
252 .min()
253 .ok_or_else(|| Error::InvalidCommand("score fragment requires selections".into()))?;
254
255 let mut seen_lanes = BTreeSet::new();
256 let mut source_to_relative = Vec::new();
257 let mut voices = Vec::with_capacity(selections.len());
258 for selection in selections {
259 if selection.start.part != selection.end.part
260 || selection.start.staff != selection.end.staff
261 || selection.start.voice != selection.end.voice
262 {
263 return Err(Error::InvalidCommand(
264 "score fragment selection endpoints must share part, staff, and voice".into(),
265 ));
266 }
267 let lane = (
268 selection.start.part,
269 selection.start.staff,
270 selection.start.voice,
271 );
272 if !seen_lanes.insert(lane) {
273 return Err(Error::InvalidCommand(
274 "score fragment selections must not overlap a voice lane".into(),
275 ));
276 }
277 let part = score
278 .parts
279 .get(selection.start.part)
280 .ok_or(Error::PartNotFound(selection.start.part))?;
281 let staff = part
282 .staves
283 .get(selection.start.staff)
284 .ok_or(Error::StaffNotFound(selection.start.staff))?;
285 if selection.start.voice >= 4 {
286 return Err(Error::VoiceOutOfRange(selection.start.voice));
287 }
288 let from = selection.start.measure.min(selection.end.measure);
289 let to = selection.start.measure.max(selection.end.measure);
290 let mut measures = Vec::with_capacity(to - from + 1);
291 for measure_index in from..=to {
292 let measure = staff
293 .measures
294 .get(measure_index)
295 .ok_or(Error::MeasureNotFound(measure_index))?;
296 let relative = NoteAddr {
297 part: selection.start.part - base_part,
298 staff: selection.start.staff - base_staff,
299 measure: measure_index - base_measure,
300 voice: selection.start.voice - base_voice,
301 note: 0,
302 };
303 for note_index in 0..measure.voices[selection.start.voice].len() {
304 source_to_relative.push((
305 NoteAddr {
306 part: selection.start.part,
307 staff: selection.start.staff,
308 measure: measure_index,
309 voice: selection.start.voice,
310 note: note_index,
311 },
312 NoteAddr {
313 note: note_index,
314 ..relative.clone()
315 },
316 ));
317 }
318 let notes = measure.voices[selection.start.voice].clone();
319 let cross_staff_targets = notes
320 .iter()
321 .map(|note| {
322 note.cross_staff
323 .as_ref()
324 .map(|cross_staff| ScoreFragmentCrossStaffTarget {
325 staff_offset: cross_staff.target_staff as i64
326 - selection.start.staff as i64,
327 target_voice: cross_staff.target_voice,
328 })
329 })
330 .collect();
331 measures.push(ScoreFragmentMeasure {
332 relative_measure: measure_index - base_measure,
333 source_voice_number: measure.source_voice_numbers[selection.start.voice],
334 notes,
335 cross_staff_targets,
336 attributes: ScoreFragmentMeasureAttributes::from_measure(measure),
337 });
338 }
339 voices.push(ScoreFragmentVoice {
340 relative_part: selection.start.part - base_part,
341 relative_staff: selection.start.staff - base_staff,
342 relative_voice: selection.start.voice - base_voice,
343 measures,
344 });
345 }
346 voices.sort_by_key(|voice| {
347 (
348 voice.relative_part,
349 voice.relative_staff,
350 voice.relative_voice,
351 )
352 });
353
354 let mut diagnostics = Vec::new();
355 let mut spanners = Vec::new();
356 for spanner in &score.spanners {
357 let start = source_to_relative
358 .iter()
359 .find(|(source, _)| source == &spanner.start)
360 .map(|(_, relative)| relative);
361 let end = source_to_relative
362 .iter()
363 .find(|(source, _)| source == &spanner.end)
364 .map(|(_, relative)| relative);
365 match (start, end) {
366 (Some(start), Some(end)) => {
367 let mut copied = spanner.clone();
368 copied.start = start.clone();
369 copied.end = end.clone();
370 spanners.push(copied);
371 }
372 (Some(_), None) | (None, Some(_)) => {
373 diagnostics.push(ScoreFragmentDiagnostic::PartialSpanner {
374 id: spanner.id.clone(),
375 });
376 }
377 (None, None) => {}
378 }
379 }
380 spanners.sort_by(|left, right| left.id.cmp(&right.id));
381 diagnostics.sort_by(|left, right| match (left, right) {
382 (
383 ScoreFragmentDiagnostic::PartialSpanner { id: left },
384 ScoreFragmentDiagnostic::PartialSpanner { id: right },
385 ) => left.cmp(right),
386 });
387
388 Ok(ScoreFragment {
389 contract_version: SCORE_FRAGMENT_CONTRACT_VERSION,
390 voices,
391 spanners,
392 diagnostics,
393 })
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use crate::{Duration, NotationSpannerKind, Pitch, Step};
400
401 fn address(staff: usize, measure: usize, voice: usize, note: usize) -> NoteAddr {
402 NoteAddr {
403 part: 0,
404 staff,
405 measure,
406 voice,
407 note,
408 }
409 }
410
411 #[test]
412 fn extracts_multivoice_fragment_and_keeps_only_complete_spanners() {
413 let mut score = Score::template(crate::ScoreTemplate::Piano);
414 for staff in &mut score.parts[0].staves {
415 for measure in &mut staff.measures {
416 measure.voices[0] =
417 vec![crate::Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
418 }
419 }
420 score.parts[0].staves[0].measures[0].source_voice_numbers[0] = Some(5);
421 score.spanners = vec![
422 NotationSpanner {
423 id: "complete".into(),
424 kind: NotationSpannerKind::Slur,
425 start: address(0, 0, 0, 0),
426 end: address(1, 1, 0, 0),
427 number: None,
428 placement: None,
429 line_type: None,
430 ottava_size: None,
431 ottava_type: None,
432 text: None,
433 },
434 NotationSpanner {
435 id: "partial".into(),
436 kind: NotationSpannerKind::Slur,
437 start: address(0, 0, 0, 0),
438 end: address(0, 1, 0, 0),
439 number: None,
440 placement: None,
441 line_type: None,
442 ottava_size: None,
443 ottava_type: None,
444 text: None,
445 },
446 ];
447 let fragment = extract_score_fragment(
448 &score,
449 &[
450 ScoreFragmentSelection {
451 start: address(0, 0, 0, 0),
452 end: address(0, 0, 0, 0),
453 },
454 ScoreFragmentSelection {
455 start: address(1, 1, 0, 0),
456 end: address(1, 1, 0, 0),
457 },
458 ],
459 )
460 .expect("fragment extracts");
461
462 assert_eq!(fragment.contract_version, SCORE_FRAGMENT_CONTRACT_VERSION);
463 assert_eq!(fragment.voices.len(), 2);
464 assert_eq!(fragment.voices[0].measures[0].source_voice_number, Some(5));
465 assert_eq!(fragment.spanners.len(), 1);
466 assert_eq!(fragment.spanners[0].id, "complete");
467 assert_eq!(fragment.spanners[0].start.staff, 0);
468 assert_eq!(fragment.spanners[0].end.staff, 1);
469 assert_eq!(fragment.spanners[0].end.measure, 1);
470 assert_eq!(
471 fragment.diagnostics,
472 vec![ScoreFragmentDiagnostic::PartialSpanner {
473 id: "partial".into()
474 }]
475 );
476 }
477}