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