1use serde::{Deserialize, Serialize};
14use std::collections::BTreeMap;
15
16use super::duration::Duration;
17use super::gm::instrument_range;
18use super::notation::{Barline, ChordSymbol, Clef, TupletBoundary, TupletInfo};
19use super::pitch::Pitch;
20use super::score::{Measure, Note, NoteAddr, Part, Score, Staff};
21use crate::Error;
22
23const ACCORDION_PROGRAM: u8 = 21;
24const GUIDED_RIGHT_HAND_RANGE: (i16, i16) = (48, 96);
27const GUIDED_LEFT_HAND_RANGE: (i16, i16) = (36, 84);
28const GRID: f64 = 64.0;
31const SPLIT_MIDI: u8 = 60;
34
35#[derive(Debug, Clone, Serialize)]
36pub struct PartCandidate {
37 pub part_index: usize,
38 pub name: String,
39 pub mean_pitch: f64,
40}
41
42#[derive(Debug, Clone, Serialize)]
43pub struct AccordionAnalysis {
44 pub candidates: Vec<PartCandidate>,
45 pub ambiguous: bool,
48}
49
50#[derive(Debug, Clone, Serialize)]
51pub struct ArrangeResult {
52 pub score: Score,
53 pub notes: Vec<String>,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "kebab-case")]
59pub enum AccordionPattern {
60 HeldBassChord,
61 BassChordPulse,
62 AlternatingBassChord,
63 Arpeggio,
64 RisingArpeggio,
65 AlbertiBass,
66 OffbeatChords,
67 SwingAccompaniment,
68 WalkingBass,
69}
70
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72pub struct PositionedAccordionChord {
73 pub measure_index: usize,
74 pub offset_beats: f64,
76 pub chord: ChordSymbol,
77}
78
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct GuidedAccordionOptions {
81 pub pattern: AccordionPattern,
82 pub chords: Vec<PositionedAccordionChord>,
83}
84
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct GuidedArrangeDiagnostic {
87 pub code: String,
88 pub message: String,
89 pub measure_index: Option<usize>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct GuidedAccordionResult {
94 pub score: Option<Score>,
96 pub resolved_options: GuidedAccordionOptions,
97 pub diagnostics: Vec<GuidedArrangeDiagnostic>,
98 pub source_notes: Vec<NoteAddr>,
99 pub generated_notes: Vec<NoteAddr>,
100 pub contract_version: u16,
101}
102
103fn guided_error(
104 options: GuidedAccordionOptions,
105 code: &str,
106 message: impl Into<String>,
107 measure_index: Option<usize>,
108) -> GuidedAccordionResult {
109 GuidedAccordionResult {
110 score: None,
111 resolved_options: options,
112 diagnostics: vec![GuidedArrangeDiagnostic {
113 code: code.to_string(),
114 message: message.into(),
115 measure_index,
116 }],
117 source_notes: Vec::new(),
118 generated_notes: Vec::new(),
119 contract_version: 1,
120 }
121}
122
123fn accordion_pitch_class(name: &str) -> Option<u8> {
124 let mut characters = name.chars();
125 let base: i16 = match characters.next()? {
126 'C' => 0,
127 'D' => 2,
128 'E' => 4,
129 'F' => 5,
130 'G' => 7,
131 'A' => 9,
132 'B' => 11,
133 _ => return None,
134 };
135 let alter = match characters.next() {
136 None => 0,
137 Some('#') => 1,
138 Some('b') => -1,
139 _ => return None,
140 };
141 if characters.next().is_some() {
142 return None;
143 }
144 Some((base + alter).rem_euclid(12) as u8)
145}
146
147#[derive(Clone)]
148struct RealizedAccordionChord {
149 symbol: ChordSymbol,
150 bass_midi: u8,
151 chord_midi: Vec<u8>,
152}
153
154fn realize_accordion_chord(symbol: &ChordSymbol) -> Option<RealizedAccordionChord> {
155 if !symbol.degrees.is_empty() {
156 return None;
157 }
158 let root = accordion_pitch_class(&symbol.root)?;
159 let bass = symbol
160 .bass
161 .as_deref()
162 .map(accordion_pitch_class)
163 .unwrap_or(Some(root))?;
164 let intervals: &[u8] = match symbol.kind.as_str() {
165 "major" => &[0, 4, 7],
166 "minor" => &[0, 3, 7],
167 "diminished" => &[0, 3, 6],
168 "augmented" => &[0, 4, 8],
169 "major-seventh" => &[0, 4, 7, 11],
170 "minor-seventh" => &[0, 3, 7, 10],
171 "dominant" => &[0, 4, 7, 10],
172 _ => return None,
173 };
174 Some(RealizedAccordionChord {
175 symbol: symbol.clone(),
176 bass_midi: 36 + bass,
177 chord_midi: intervals
178 .iter()
179 .map(|interval| 60 + root + interval)
180 .collect(),
181 })
182}
183
184fn guided_note(pitches: &[u8], duration: Duration) -> Note {
185 let mut note = Note::new(Pitch::from_midi(pitches[0], false), duration);
186 note.pitches = pitches
187 .iter()
188 .map(|midi| Pitch::from_midi(*midi, false))
189 .collect();
190 note
191}
192
193fn guided_region(
194 pattern: AccordionPattern,
195 chord: &RealizedAccordionChord,
196 ticks: u32,
197 next_bass_midi: Option<u8>,
198) -> Option<Vec<Note>> {
199 let mut notes = Vec::new();
200 match pattern {
201 AccordionPattern::HeldBassChord => {
202 let mut pitches = vec![chord.bass_midi];
203 pitches.extend(&chord.chord_midi);
204 let mut remaining = ticks;
205 while remaining > 0 {
206 let duration = Duration::whole_filling_beats(f64::from(remaining) / GRID);
207 let filled = (duration.beats(0) * GRID).round() as u32;
208 if filled == 0 || filled > remaining {
209 return None;
210 }
211 let mut note = guided_note(&pitches, duration);
212 note.tie_end = !notes.is_empty();
213 note.tie_start = remaining > filled;
214 notes.push(note);
215 remaining -= filled;
216 }
217 }
218 AccordionPattern::BassChordPulse | AccordionPattern::AlternatingBassChord => {
219 if !ticks.is_multiple_of(64) {
220 return None;
221 }
222 for pulse in 0..ticks / 64 {
223 let pitches: Vec<u8> = match pulse % 4 {
224 0 => vec![chord.bass_midi],
225 2 if pattern == AccordionPattern::AlternatingBassChord => {
226 vec![chord.bass_midi + 7]
227 }
228 2 => vec![chord.bass_midi],
229 _ => chord.chord_midi.to_vec(),
230 };
231 notes.push(guided_note(&pitches, Duration::Quarter));
232 }
233 }
234 AccordionPattern::Arpeggio => {
235 if !ticks.is_multiple_of(32) {
236 return None;
237 }
238 for pulse in 0..ticks / 32 {
239 let midi = match pulse % 4 {
240 0 => chord.bass_midi + 12,
241 1 | 3 => chord.chord_midi[1],
242 _ => chord.chord_midi[2],
243 };
244 notes.push(guided_note(&[midi], Duration::Eighth));
245 }
246 }
247 AccordionPattern::RisingArpeggio | AccordionPattern::AlbertiBass => {
248 if !ticks.is_multiple_of(32) {
249 return None;
250 }
251 for pulse in 0..ticks / 32 {
252 let midi = if pattern == AccordionPattern::RisingArpeggio {
253 match pulse % 4 {
254 0 => chord.bass_midi + 12,
255 1 => chord.chord_midi[1],
256 2 => chord.chord_midi[2],
257 _ => chord
258 .chord_midi
259 .get(3)
260 .copied()
261 .unwrap_or(chord.chord_midi[0] + 12),
262 }
263 } else {
264 match pulse % 4 {
265 0 => chord.bass_midi + 12,
266 1 | 3 => chord.chord_midi[2],
267 _ => chord.chord_midi[1],
268 }
269 };
270 notes.push(guided_note(&[midi], Duration::Eighth));
271 }
272 }
273 AccordionPattern::OffbeatChords => {
274 if !ticks.is_multiple_of(64) {
275 return None;
276 }
277 for _ in 0..ticks / 64 {
278 notes.push(Note::rest(Duration::Eighth));
279 notes.push(guided_note(&chord.chord_midi, Duration::Eighth));
280 }
281 }
282 AccordionPattern::SwingAccompaniment => {
283 if !ticks.is_multiple_of(64) {
284 return None;
285 }
286 let triplet = Some(TupletInfo {
287 actual_notes: 3,
288 normal_notes: 2,
289 });
290 for _ in 0..ticks / 64 {
291 let mut bass = guided_note(&[chord.bass_midi], Duration::Quarter);
292 bass.tuplet = triplet.clone();
293 bass.tuplet_boundary = Some(TupletBoundary::Start);
294 let mut chord_note = guided_note(&chord.chord_midi, Duration::Eighth);
295 chord_note.tuplet = triplet.clone();
296 chord_note.tuplet_boundary = Some(TupletBoundary::Stop);
297 notes.push(bass);
298 notes.push(chord_note);
299 }
300 }
301 AccordionPattern::WalkingBass => {
302 if !ticks.is_multiple_of(64) {
303 return None;
304 }
305 let pulses = ticks / 64;
306 for pulse in 0..pulses {
307 let midi = if pulse + 1 == pulses {
308 next_bass_midi.map_or(chord.bass_midi, |next| {
309 if chord.bass_midi <= next {
310 next.saturating_sub(1)
311 } else {
312 next.saturating_add(1)
313 }
314 })
315 } else {
316 match pulse % 3 {
317 0 => chord.bass_midi,
318 1 => chord.bass_midi + (chord.chord_midi[1] - chord.chord_midi[0]),
319 _ => chord.bass_midi + 7,
320 }
321 };
322 notes.push(guided_note(&[midi], Duration::Quarter));
323 }
324 }
325 }
326 if let Some(first) = notes.iter_mut().find(|note| !note.is_rest) {
327 first.chord_symbol = Some(chord.symbol.clone());
328 }
329 Some(notes)
330}
331
332pub fn arrange_with_explicit_chords(
336 score: &Score,
337 options: GuidedAccordionOptions,
338) -> GuidedAccordionResult {
339 if score.parts.len() != 1 || score.parts[0].staves.len() != 1 {
340 return guided_error(
341 options,
342 "unsupported-source",
343 "expected one melody part and one staff",
344 None,
345 );
346 }
347 let source_staff = &score.parts[0].staves[0];
348 if source_staff.measures.is_empty() || source_staff.measures.len() > 2048 {
349 return guided_error(
350 options,
351 "unsupported-source",
352 "measure count is empty or exceeds 2048",
353 None,
354 );
355 }
356 if options.chords.is_empty() || options.chords.len() > 8192 {
357 return guided_error(
358 options,
359 "missing-chords",
360 "provide 1 to 8192 positioned chords",
361 None,
362 );
363 }
364 for (index, measure) in source_staff.measures.iter().enumerate() {
365 if measure.voices.iter().skip(1).any(|voice| !voice.is_empty())
366 || measure.voices[0]
367 .iter()
368 .any(|note| !note.is_rest && note.pitches.len() != 1)
369 || measure.multi_rest_count.is_some()
370 {
371 return guided_error(
372 options,
373 "unsupported-source",
374 "expected a monophonic melody without measure repeats",
375 Some(index),
376 );
377 }
378 }
379 let validation = super::validate::validate(score);
380 if !validation.is_valid() {
381 return guided_error(
382 options,
383 "invalid-source",
384 format!("source validation failed: {:?}", validation.errors[0]),
385 None,
386 );
387 }
388 let mut regions: Vec<Vec<(u32, RealizedAccordionChord)>> =
389 vec![Vec::new(); source_staff.measures.len()];
390 let mut length_ticks = Vec::with_capacity(source_staff.measures.len());
391 for index in 0..source_staff.measures.len() {
392 let beats = score.playback_measure_beats(index);
393 let ticks = (beats * GRID).round();
394 if !ticks.is_finite() || ticks <= 0.0 || (ticks / GRID - beats).abs() > 1e-9 {
395 return guided_error(
396 options,
397 "unsupported-rhythm",
398 "measure length must be on a 1/64-beat grid",
399 Some(index),
400 );
401 }
402 length_ticks.push(ticks as u32);
403 }
404 for placed in &options.chords {
405 let index = placed.measure_index;
406 let Some(&length) = length_ticks.get(index) else {
407 return guided_error(
408 options,
409 "chord-position",
410 "chord measure is outside the score",
411 Some(index),
412 );
413 };
414 let offset = (placed.offset_beats * GRID).round();
415 if !offset.is_finite()
416 || offset < 0.0
417 || offset >= f64::from(length)
418 || (offset / GRID - placed.offset_beats).abs() > 1e-9
419 {
420 return guided_error(
421 options,
422 "chord-position",
423 "chord offset must be inside the measure on a 1/64-beat grid",
424 Some(index),
425 );
426 }
427 let Some(chord) = realize_accordion_chord(&placed.chord) else {
428 return guided_error(
429 options.clone(),
430 "unsupported-chord",
431 format!("cannot realize chord {}", placed.chord.display_text()),
432 Some(index),
433 );
434 };
435 regions[index].push((offset as u32, chord));
436 }
437 let mut left = Staff::new(Clef::Bass);
438 let mut generated_notes = Vec::new();
439 let mut current_chord: Option<RealizedAccordionChord> = None;
440 for (index, source_measure) in source_staff.measures.iter().enumerate() {
441 regions[index].sort_by_key(|(offset, _)| *offset);
442 if regions[index]
443 .windows(2)
444 .any(|window| window[0].0 == window[1].0)
445 {
446 return guided_error(
447 options,
448 "chord-position",
449 "two chords occupy the same position",
450 Some(index),
451 );
452 }
453 if regions[index]
454 .first()
455 .is_none_or(|(offset, _)| *offset != 0)
456 {
457 let Some(chord) = current_chord.clone() else {
458 return guided_error(
459 options,
460 "missing-chords",
461 "no explicit chord covers the start of this measure",
462 Some(index),
463 );
464 };
465 regions[index].insert(0, (0, chord));
466 }
467 let mut generated = Vec::new();
468 for position in 0..regions[index].len() {
469 let (start, chord) = ®ions[index][position];
470 let end = regions[index]
471 .get(position + 1)
472 .map_or(length_ticks[index], |(offset, _)| *offset);
473 let next_bass = regions[index]
474 .get(position + 1)
475 .map(|(_, next)| next.bass_midi)
476 .or_else(|| {
477 regions.iter().skip(index + 1).find_map(|bar| {
478 bar.iter()
479 .min_by_key(|(offset, _)| *offset)
480 .map(|(_, next)| next.bass_midi)
481 })
482 });
483 let Some(mut notes) = guided_region(options.pattern, chord, end - start, next_bass)
484 else {
485 return guided_error(
486 options,
487 "unsupported-rhythm",
488 "pattern cannot fill the chord region exactly",
489 Some(index),
490 );
491 };
492 for (offset, note) in notes.iter().enumerate() {
493 if !note.is_rest {
494 generated_notes.push(NoteAddr {
495 part: 0,
496 staff: 1,
497 measure: index,
498 voice: 0,
499 note: generated.len() + offset,
500 });
501 }
502 }
503 generated.append(&mut notes);
504 current_chord = Some(chord.clone());
505 }
506 let mut measure = Measure::empty(
507 source_measure
508 .time_sig
509 .as_ref()
510 .map_or(score.settings.time_signature.numerator, |ts| ts.numerator),
511 source_measure
512 .time_sig
513 .as_ref()
514 .map_or(score.settings.time_signature.denominator, |ts| {
515 ts.denominator
516 }),
517 );
518 measure.number = source_measure.number;
519 measure.time_sig = source_measure.time_sig.clone();
520 measure.key_sig = source_measure.key_sig.clone();
521 measure.tempo = source_measure.tempo;
522 measure.tempo_ramp_to = source_measure.tempo_ramp_to;
523 measure.barline_left = source_measure.barline_left.clone();
524 measure.barline_right = source_measure.barline_right.clone();
525 measure.volta = source_measure.volta.clone();
526 measure.system_break = source_measure.system_break;
527 measure.page_break = source_measure.page_break;
528 measure.section_break = source_measure.section_break;
529 measure.actual_length = source_measure.actual_length;
530 measure.voices[0] = generated;
531 left.measures.push(measure);
532 }
533 let mut output = score.clone();
534 output.id = uuid::Uuid::new_v4().to_string();
535 let mut part = Part::new("Accordion", "Acc.");
536 part.midi_program = ACCORDION_PROGRAM;
537 part.staves = vec![source_staff.clone(), left];
538 output.parts = vec![part];
539 output.part_groups.clear();
540 output.views.clear();
541 for (staff_index, staff) in output.parts[0].staves.iter().enumerate() {
542 let (min_midi, max_midi) = if staff_index == 0 {
543 GUIDED_RIGHT_HAND_RANGE
544 } else {
545 GUIDED_LEFT_HAND_RANGE
546 };
547 for (measure_index, measure) in staff.measures.iter().enumerate() {
548 for note in &measure.voices[0] {
549 if note
550 .pitches
551 .iter()
552 .any(|pitch| pitch.to_midi() < min_midi || pitch.to_midi() > max_midi)
553 {
554 return guided_error(
555 options,
556 "out-of-range",
557 format!("hand {staff_index} requires MIDI range {min_midi}..={max_midi}"),
558 Some(measure_index),
559 );
560 }
561 }
562 }
563 }
564 let output_validation = super::validate::validate(&output);
565 if !output_validation.is_valid() {
566 return guided_error(
567 options,
568 "invalid-output",
569 format!(
570 "generated score validation failed: {:?}",
571 output_validation.errors[0]
572 ),
573 None,
574 );
575 }
576 let source_notes = source_staff
577 .measures
578 .iter()
579 .enumerate()
580 .flat_map(|(measure, bar)| {
581 bar.voices[0]
582 .iter()
583 .enumerate()
584 .filter(|(_, note)| !note.is_rest)
585 .map(move |(note, _)| NoteAddr {
586 part: 0,
587 staff: 0,
588 measure,
589 voice: 0,
590 note,
591 })
592 })
593 .collect();
594 GuidedAccordionResult {
595 score: Some(output),
596 resolved_options: options,
597 diagnostics: Vec::new(),
598 source_notes,
599 generated_notes,
600 contract_version: 1,
601 }
602}
603
604#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
605#[serde(rename_all = "kebab-case")]
606pub enum AccordionInversion {
607 Root,
608 First,
609 Second,
610 Third,
611 Nearby,
612}
613
614#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
615#[serde(rename_all = "kebab-case")]
616pub enum AccordionMelodicVoice {
617 Unchanged,
618 ThirdBelow,
619 SixthBelow,
620 OctaveBelow,
621}
622
623#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
624#[serde(rename_all = "kebab-case")]
625pub enum AccordionSwingPolicy {
626 Straight,
627 NotatedAccompanimentTwoToOne,
628}
629
630#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
631#[serde(rename_all = "kebab-case")]
632pub enum AccordionBassMode {
633 StandardBass,
634 FreeBass,
635}
636
637#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
638pub struct LayeredAccordionOptions {
639 pub chords: Vec<PositionedAccordionChord>,
640 pub alternative_chords: Option<Vec<PositionedAccordionChord>>,
642 pub inversion: Option<AccordionInversion>,
643 pub accompaniment: Option<AccordionPattern>,
644 pub melodic_voice: AccordionMelodicVoice,
645 pub swing: AccordionSwingPolicy,
646 pub bass_mode: AccordionBassMode,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct LayeredAccordionResult {
651 pub score: Option<Score>,
652 pub resolved_options: LayeredAccordionOptions,
653 pub resolved_chords: Vec<PositionedAccordionChord>,
654 pub diagnostics: Vec<GuidedArrangeDiagnostic>,
655 pub source_notes: Vec<NoteAddr>,
656 pub generated_notes: Vec<NoteAddr>,
657 pub notated_swing: bool,
659 pub contract_version: u16,
660}
661
662fn layered_error(
663 options: LayeredAccordionOptions,
664 code: &str,
665 message: impl Into<String>,
666 measure_index: Option<usize>,
667) -> LayeredAccordionResult {
668 LayeredAccordionResult {
669 score: None,
670 resolved_options: options,
671 resolved_chords: Vec::new(),
672 diagnostics: vec![GuidedArrangeDiagnostic {
673 code: code.to_string(),
674 message: message.into(),
675 measure_index,
676 }],
677 source_notes: Vec::new(),
678 generated_notes: Vec::new(),
679 notated_swing: false,
680 contract_version: 1,
681 }
682}
683
684fn chord_pitch_name(midi: u8) -> &'static str {
685 [
686 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
687 ][(midi % 12) as usize]
688}
689
690fn resolve_layered_chords(
691 chords: &mut [PositionedAccordionChord],
692 inversion: Option<AccordionInversion>,
693) -> Result<(), (usize, String)> {
694 let mut previous_bass: Option<u8> = None;
695 chords.sort_by(|a, b| {
696 (a.measure_index, a.offset_beats)
697 .partial_cmp(&(b.measure_index, b.offset_beats))
698 .unwrap_or(std::cmp::Ordering::Equal)
699 });
700 for placed in chords {
701 let Some(realized) = realize_accordion_chord(&placed.chord) else {
702 return Err((
703 placed.measure_index,
704 format!("cannot realize chord {}", placed.chord.display_text()),
705 ));
706 };
707 if let Some(policy) = inversion {
708 let index = match policy {
709 AccordionInversion::Root => 0,
710 AccordionInversion::First => 1,
711 AccordionInversion::Second => 2,
712 AccordionInversion::Third => 3,
713 AccordionInversion::Nearby => realized
714 .chord_midi
715 .iter()
716 .enumerate()
717 .min_by_key(|(_, midi)| {
718 previous_bass.map_or(0, |previous| {
719 (i16::from(**midi % 12 + 36) - i16::from(previous)).abs()
720 })
721 })
722 .map_or(0, |(index, _)| index),
723 };
724 let Some(&tone) = realized.chord_midi.get(index) else {
725 return Err((
726 placed.measure_index,
727 "requested inversion has no chord tone".to_string(),
728 ));
729 };
730 placed.chord.bass = Some(chord_pitch_name(tone).to_string());
731 previous_bass = Some(36 + tone % 12);
732 } else {
733 previous_bass = Some(realized.bass_midi);
734 }
735 }
736 Ok(())
737}
738
739fn chord_at_note(
740 chords: &[PositionedAccordionChord],
741 measure_index: usize,
742 offset_beats: f64,
743) -> Option<&ChordSymbol> {
744 chords
745 .iter()
746 .rfind(|placed| {
747 placed.measure_index < measure_index
748 || (placed.measure_index == measure_index
749 && placed.offset_beats <= offset_beats + 1e-9)
750 })
751 .map(|placed| &placed.chord)
752}
753
754fn harmony_voice_pitch(
755 source_midi: i16,
756 chord: &ChordSymbol,
757 voice: AccordionMelodicVoice,
758) -> Option<i16> {
759 if voice == AccordionMelodicVoice::OctaveBelow {
760 return Some(source_midi - 12);
761 }
762 let realized = realize_accordion_chord(chord)?;
763 let allowed: &[i16] = match voice {
764 AccordionMelodicVoice::ThirdBelow => &[3, 4],
765 AccordionMelodicVoice::SixthBelow => &[8, 9],
766 _ => return None,
767 };
768 (GUIDED_RIGHT_HAND_RANGE.0..=GUIDED_RIGHT_HAND_RANGE.1)
769 .rev()
770 .filter(|candidate| {
771 allowed.contains(&(source_midi - candidate))
772 && realized
773 .chord_midi
774 .iter()
775 .any(|tone| i16::from(*tone % 12) == candidate.rem_euclid(12))
776 })
777 .max()
778}
779
780pub fn arrange_with_layers(
784 score: &Score,
785 options: LayeredAccordionOptions,
786) -> LayeredAccordionResult {
787 let mut resolved_chords = options
788 .alternative_chords
789 .clone()
790 .unwrap_or_else(|| options.chords.clone());
791 if resolved_chords.is_empty() {
792 return layered_error(
793 options,
794 "missing-chords",
795 "supply an explicit chord progression",
796 None,
797 );
798 }
799 if let Err((measure, message)) = resolve_layered_chords(&mut resolved_chords, options.inversion)
800 {
801 return layered_error(options, "unsupported-chord", message, Some(measure));
802 }
803 let notated_swing = options.swing == AccordionSwingPolicy::NotatedAccompanimentTwoToOne;
804 if notated_swing && options.accompaniment.is_none() {
805 return layered_error(
806 options,
807 "unsupported-swing",
808 "notated swing needs an accompaniment layer",
809 None,
810 );
811 }
812 let pattern = if notated_swing {
813 AccordionPattern::SwingAccompaniment
814 } else {
815 options
816 .accompaniment
817 .unwrap_or(AccordionPattern::HeldBassChord)
818 };
819 if options.bass_mode == AccordionBassMode::StandardBass
820 && (resolved_chords.iter().any(|placed| {
821 realize_accordion_chord(&placed.chord).is_some_and(|chord| chord.chord_midi.len() > 3)
822 }) || matches!(
823 pattern,
824 AccordionPattern::Arpeggio
825 | AccordionPattern::RisingArpeggio
826 | AccordionPattern::AlbertiBass
827 | AccordionPattern::WalkingBass
828 ))
829 {
830 return layered_error(
831 options,
832 "unsupported-standard-bass",
833 "the selected seventh harmony or melodic bass pattern requires free bass",
834 None,
835 );
836 }
837 let base = arrange_with_explicit_chords(
838 score,
839 GuidedAccordionOptions {
840 pattern,
841 chords: resolved_chords.clone(),
842 },
843 );
844 let Some(mut output) = base.score else {
845 return LayeredAccordionResult {
846 score: None,
847 resolved_options: options,
848 resolved_chords,
849 diagnostics: base.diagnostics,
850 source_notes: Vec::new(),
851 generated_notes: Vec::new(),
852 notated_swing,
853 contract_version: 1,
854 };
855 };
856 let mut generated_notes = base.generated_notes;
857 if options.accompaniment.is_none() {
858 for measure in &mut output.parts[0].staves[1].measures {
859 measure.voices[0].clear();
860 }
861 generated_notes.clear();
862 }
863 if options.melodic_voice != AccordionMelodicVoice::Unchanged {
864 for (measure_index, measure) in output.parts[0].staves[0].measures.iter_mut().enumerate() {
865 let mut offset = 0.0;
866 let mut voice = Vec::new();
867 for source_note in &measure.voices[0] {
868 let mut added = if source_note.is_rest {
869 Note::rest(source_note.duration.clone())
870 } else {
871 Note::new(source_note.pitches[0].clone(), source_note.duration.clone())
872 };
873 added.dot_count = source_note.dot_count;
874 added.tuplet = source_note.tuplet.clone();
875 added.tuplet_boundary = source_note.tuplet_boundary;
876 added.beam = source_note.beam;
877 added.articulations = source_note.articulations.clone();
878 added.dynamic = source_note.dynamic;
879 added.is_grace = source_note.is_grace;
880 added.grace_slash = source_note.grace_slash;
881 added.is_cue = source_note.is_cue;
882 if options.melodic_voice == AccordionMelodicVoice::OctaveBelow {
883 added.tie_start = source_note.tie_start;
884 added.tie_end = source_note.tie_end;
885 }
886 if !source_note.is_rest {
887 let Some(chord) = chord_at_note(&resolved_chords, measure_index, offset) else {
888 return layered_error(
889 options,
890 "missing-chords",
891 "no chord covers a melody note",
892 Some(measure_index),
893 );
894 };
895 let source_midi = source_note.pitches[0].to_midi();
896 let Some(target) =
897 harmony_voice_pitch(source_midi, chord, options.melodic_voice)
898 else {
899 return layered_error(
900 options,
901 "incompatible-harmony",
902 "no chord tone fits the requested melodic interval",
903 Some(measure_index),
904 );
905 };
906 if !(GUIDED_RIGHT_HAND_RANGE.0..=GUIDED_RIGHT_HAND_RANGE.1).contains(&target) {
907 return layered_error(
908 options,
909 "out-of-range",
910 "added melodic voice exceeds the right-hand range",
911 Some(measure_index),
912 );
913 }
914 added.pitches[0] = Pitch::from_midi(target as u8, false);
915 generated_notes.push(NoteAddr {
916 part: 0,
917 staff: 0,
918 measure: measure_index,
919 voice: 1,
920 note: voice.len(),
921 });
922 }
923 offset += source_note.beats();
924 voice.push(added);
925 }
926 measure.voices[1] = voice;
927 }
928 }
929 let validation = super::validate::validate(&output);
930 if !validation.is_valid() {
931 return layered_error(
932 options,
933 "invalid-output",
934 format!(
935 "layered score validation failed: {:?}",
936 validation.errors[0]
937 ),
938 None,
939 );
940 }
941 LayeredAccordionResult {
942 score: Some(output),
943 resolved_options: options,
944 resolved_chords,
945 diagnostics: Vec::new(),
946 source_notes: base.source_notes,
947 generated_notes,
948 notated_swing,
949 contract_version: 1,
950 }
951}
952
953fn is_percussion_part(part: &Part) -> bool {
954 part.midi_channel == 9 || part.staves.iter().any(|s| s.clef == Clef::Percussion)
955}
956
957fn mean_pitch(part: &Part) -> Option<f64> {
958 let (sum, count) = part
959 .staves
960 .iter()
961 .flat_map(|s| s.measures.iter())
962 .flat_map(|m| m.voices.iter())
963 .flat_map(|v| v.iter())
964 .filter(|n| !n.is_rest && !n.is_grace)
965 .flat_map(|n| n.pitches.iter())
966 .fold((0i64, 0i64), |(sum, count), p| {
967 (sum + p.to_midi() as i64, count + 1)
968 });
969 if count == 0 {
970 None
971 } else {
972 Some(sum as f64 / count as f64)
973 }
974}
975
976pub fn analyze_for_accordion(score: &Score) -> AccordionAnalysis {
979 let mut candidates: Vec<PartCandidate> = score
980 .parts
981 .iter()
982 .enumerate()
983 .filter(|(_, p)| !is_percussion_part(p))
984 .filter_map(|(i, p)| {
985 mean_pitch(p).map(|mp| PartCandidate {
986 part_index: i,
987 name: p.name.clone(),
988 mean_pitch: mp,
989 })
990 })
991 .collect();
992 candidates.sort_by(|a, b| {
993 b.mean_pitch
994 .partial_cmp(&a.mean_pitch)
995 .unwrap_or(std::cmp::Ordering::Equal)
996 });
997 let ambiguous =
998 candidates.len() >= 2 && (candidates[0].mean_pitch - candidates[1].mean_pitch).abs() < 3.0;
999 AccordionAnalysis {
1000 candidates,
1001 ambiguous,
1002 }
1003}
1004
1005struct SourceEvent {
1006 onset_ticks: i64,
1007 beats: f64,
1008 pitches: Vec<Pitch>,
1009}
1010
1011fn events_from_parts(
1012 score: &Score,
1013 part_indices: &[usize],
1014 measure_idx: usize,
1015) -> Vec<SourceEvent> {
1016 let mut events = Vec::new();
1017 for &pi in part_indices {
1018 let part = &score.parts[pi];
1019 for staff in &part.staves {
1020 let Some(measure) = staff.measures.get(measure_idx) else {
1021 continue;
1022 };
1023 if measure.multi_rest_count.is_some() {
1024 continue;
1025 }
1026 for voice in &measure.voices {
1027 let mut onset = 0.0f64;
1028 for note in voice {
1029 let b = note.beats();
1030 if !note.is_rest && !note.is_grace && !note.pitches.is_empty() {
1031 let pitches = note
1032 .pitches
1033 .iter()
1034 .map(|p| {
1035 let midi = (p.to_midi() + staff.transpose_semitones as i16)
1036 .clamp(0, 127) as u8;
1037 Pitch::from_midi(midi, false)
1038 })
1039 .collect();
1040 events.push(SourceEvent {
1041 onset_ticks: (onset * GRID).round() as i64,
1042 beats: b,
1043 pitches,
1044 });
1045 }
1046 onset += b;
1047 }
1048 }
1049 }
1050 }
1051 events
1052}
1053
1054fn events_from_pitch_split(
1058 score: &Score,
1059 part_index: usize,
1060 measure_idx: usize,
1061 high: bool,
1062) -> Vec<SourceEvent> {
1063 let part = &score.parts[part_index];
1064 let mut events = Vec::new();
1065 for staff in &part.staves {
1066 let Some(measure) = staff.measures.get(measure_idx) else {
1067 continue;
1068 };
1069 if measure.multi_rest_count.is_some() {
1070 continue;
1071 }
1072 for voice in &measure.voices {
1073 let mut onset = 0.0f64;
1074 for note in voice {
1075 let b = note.beats();
1076 if !note.is_rest && !note.is_grace {
1077 let pitches: Vec<Pitch> = note
1078 .pitches
1079 .iter()
1080 .filter_map(|p| {
1081 let midi = (p.to_midi() + staff.transpose_semitones as i16)
1082 .clamp(0, 127) as u8;
1083 let keep = if high {
1084 midi >= SPLIT_MIDI
1085 } else {
1086 midi < SPLIT_MIDI
1087 };
1088 if keep {
1089 Some(Pitch::from_midi(midi, false))
1090 } else {
1091 None
1092 }
1093 })
1094 .collect();
1095 if !pitches.is_empty() {
1096 events.push(SourceEvent {
1097 onset_ticks: (onset * GRID).round() as i64,
1098 beats: b,
1099 pitches,
1100 });
1101 }
1102 }
1103 onset += b;
1104 }
1105 }
1106 }
1107 events
1108}
1109
1110fn fill_rests(notes: &mut Vec<Note>, mut remaining: f64) {
1111 while remaining > 1.0 / GRID {
1112 let dur = Duration::whole_filling_beats(remaining);
1113 let filled = dur.beats(0);
1114 notes.push(Note::rest(dur));
1115 remaining -= filled;
1116 }
1117}
1118
1119fn assemble_staff(
1123 clef: Clef,
1124 template_per_measure: Vec<Option<Measure>>,
1125 events_per_measure: Vec<Vec<SourceEvent>>,
1126 default_ts: super::notation::TimeSignature,
1127) -> Staff {
1128 let mut current_ts = default_ts;
1129 let mut measures = Vec::with_capacity(template_per_measure.len());
1130
1131 for (mi, (template, events)) in template_per_measure
1132 .into_iter()
1133 .zip(events_per_measure)
1134 .enumerate()
1135 {
1136 if let Some(ts) = template.as_ref().and_then(|m| m.time_sig.as_ref()) {
1137 current_ts = ts.clone();
1138 }
1139 let total_beats = current_ts.total_beats();
1140
1141 let mut buckets: BTreeMap<i64, Vec<SourceEvent>> = BTreeMap::new();
1142 for ev in events {
1143 buckets.entry(ev.onset_ticks).or_default().push(ev);
1144 }
1145 let keys: Vec<i64> = buckets.keys().copied().collect();
1146
1147 let mut voice0: Vec<Note> = Vec::new();
1148 let mut cursor = 0.0f64;
1149 for (idx, &key) in keys.iter().enumerate() {
1150 let onset = key as f64 / GRID;
1151 if onset < cursor - 1.0 / GRID {
1152 continue;
1153 } if onset > cursor {
1155 fill_rests(&mut voice0, onset - cursor);
1156 cursor = onset;
1157 }
1158
1159 let next_onset = keys
1160 .get(idx + 1)
1161 .map(|&k| k as f64 / GRID)
1162 .unwrap_or(total_beats);
1163 let group = &buckets[&key];
1164 let shortest = group.iter().map(|e| e.beats).fold(f64::MAX, f64::min);
1165 let cap = (next_onset - onset).max(1.0 / GRID);
1166 let sounding = shortest.min(cap).min((total_beats - onset).max(1.0 / GRID));
1167
1168 let mut pitches: Vec<Pitch> = Vec::new();
1169 let mut seen_midi: Vec<u8> = Vec::new();
1170 for ev in group {
1171 for p in &ev.pitches {
1172 let midi = p.to_midi().clamp(0, 127) as u8;
1173 if !seen_midi.contains(&midi) {
1174 seen_midi.push(midi);
1175 pitches.push(p.clone());
1176 }
1177 }
1178 }
1179 if pitches.is_empty() {
1180 continue;
1181 }
1182
1183 let dur = Duration::whole_filling_beats(sounding);
1184 let emitted = dur.beats(0);
1185 let mut note = Note::new(pitches[0].clone(), dur);
1186 note.pitches = pitches;
1187 voice0.push(note);
1188 cursor += emitted;
1189 }
1190 if total_beats - cursor > 1.0 / GRID {
1191 fill_rests(&mut voice0, total_beats - cursor);
1192 }
1193 if voice0.is_empty() {
1194 fill_rests(&mut voice0, total_beats);
1195 }
1196
1197 let mut measure = Measure::empty(current_ts.numerator, current_ts.denominator);
1198 measure.number = mi as u32 + 1;
1199 measure.time_sig = template.as_ref().and_then(|m| m.time_sig.clone());
1200 measure.key_sig = template.as_ref().and_then(|m| m.key_sig.clone());
1201 measure.tempo = template.as_ref().and_then(|m| m.tempo);
1202 measure.barline_left = template
1203 .as_ref()
1204 .map(|m| m.barline_left.clone())
1205 .unwrap_or(Barline::Normal);
1206 measure.barline_right = template
1207 .as_ref()
1208 .map(|m| m.barline_right.clone())
1209 .unwrap_or(Barline::Normal);
1210 measure.voices[0] = voice0;
1211 measures.push(measure);
1212 }
1213
1214 Staff {
1215 clef,
1216 measures,
1217 transpose_semitones: 0,
1218 tablature: None,
1219 presentation: Default::default(),
1220 }
1221}
1222
1223fn template_measure_for_parts(score: &Score, part_indices: &[usize], mi: usize) -> Option<Measure> {
1224 part_indices
1225 .iter()
1226 .filter_map(|&pi| score.parts[pi].staves.first())
1227 .find_map(|s| s.measures.get(mi).cloned())
1228}
1229
1230fn octave_fit(score: &Score) -> (Score, i8) {
1235 let (lo, hi) = instrument_range(ACCORDION_PROGRAM);
1236 let target_mid = (lo as f64 + hi as f64) / 2.0;
1237 let Some(mp) = score.parts.first().and_then(mean_pitch) else {
1238 return (score.clone(), 0);
1239 };
1240
1241 let shift = [-24i8, -12, 0, 12, 24]
1242 .into_iter()
1243 .min_by(|&a, &b| {
1244 let da = (mp + a as f64 - target_mid).abs();
1245 let db = (mp + b as f64 - target_mid).abs();
1246 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
1247 })
1248 .unwrap_or(0);
1249
1250 if shift == 0 {
1251 (score.clone(), 0)
1252 } else {
1253 (super::score::transpose(score, shift), shift)
1254 }
1255}
1256
1257pub fn arrange_for_accordion(
1265 score: &Score,
1266 right_hand_part_index: Option<usize>,
1267) -> Result<ArrangeResult, Error> {
1268 let analysis = analyze_for_accordion(score);
1269 if analysis.candidates.is_empty() {
1270 return Err(Error::InvalidCommand(
1271 "no pitched, non-percussion part to arrange".to_string(),
1272 ));
1273 }
1274
1275 let treble_index = match right_hand_part_index {
1276 Some(i) => {
1277 if i >= score.parts.len() || is_percussion_part(&score.parts[i]) {
1278 return Err(Error::PartNotFound(i));
1279 }
1280 i
1281 }
1282 None => analysis.candidates[0].part_index,
1283 };
1284 let bass_indices: Vec<usize> = analysis
1285 .candidates
1286 .iter()
1287 .map(|c| c.part_index)
1288 .filter(|&i| i != treble_index)
1289 .collect();
1290
1291 let mut notes = Vec::new();
1292 let measure_count = analysis
1293 .candidates
1294 .iter()
1295 .map(|c| {
1296 score.parts[c.part_index]
1297 .staves
1298 .iter()
1299 .map(|s| s.measures.len())
1300 .max()
1301 .unwrap_or(0)
1302 })
1303 .max()
1304 .unwrap_or(0);
1305 let default_ts = score.settings.time_signature.clone();
1306
1307 let (treble_staff, bass_staff) = if !bass_indices.is_empty() {
1308 let treble_template: Vec<Option<Measure>> = (0..measure_count)
1309 .map(|mi| template_measure_for_parts(score, &[treble_index], mi))
1310 .collect();
1311 let treble_events: Vec<Vec<SourceEvent>> = (0..measure_count)
1312 .map(|mi| events_from_parts(score, &[treble_index], mi))
1313 .collect();
1314 let bass_template: Vec<Option<Measure>> = (0..measure_count)
1315 .map(|mi| template_measure_for_parts(score, &bass_indices, mi))
1316 .collect();
1317 let bass_events: Vec<Vec<SourceEvent>> = (0..measure_count)
1318 .map(|mi| events_from_parts(score, &bass_indices, mi))
1319 .collect();
1320 notes.push(format!(
1321 "右手(高音部): {} / 左手(低音部): {}パートを統合",
1322 analysis
1323 .candidates
1324 .iter()
1325 .find(|c| c.part_index == treble_index)
1326 .map(|c| c.name.as_str())
1327 .unwrap_or(""),
1328 bass_indices.len()
1329 ));
1330 (
1331 assemble_staff(
1332 Clef::Treble,
1333 treble_template,
1334 treble_events,
1335 default_ts.clone(),
1336 ),
1337 assemble_staff(Clef::Bass, bass_template, bass_events, default_ts),
1338 )
1339 } else {
1340 let template: Vec<Option<Measure>> = (0..measure_count)
1341 .map(|mi| template_measure_for_parts(score, &[treble_index], mi))
1342 .collect();
1343 let treble_events: Vec<Vec<SourceEvent>> = (0..measure_count)
1344 .map(|mi| events_from_pitch_split(score, treble_index, mi, true))
1345 .collect();
1346 let bass_events: Vec<Vec<SourceEvent>> = (0..measure_count)
1347 .map(|mi| events_from_pitch_split(score, treble_index, mi, false))
1348 .collect();
1349 notes.push("単一パートのため中央ハ(MIDI 60)を基準に上下2段へ分割".to_string());
1350 (
1351 assemble_staff(
1352 Clef::Treble,
1353 template.clone(),
1354 treble_events,
1355 default_ts.clone(),
1356 ),
1357 assemble_staff(Clef::Bass, template, bass_events, default_ts),
1358 )
1359 };
1360
1361 let mut accordion_part = Part::new("Accordion", "Acc.");
1362 accordion_part.midi_program = ACCORDION_PROGRAM;
1363 accordion_part.staves = vec![treble_staff, bass_staff];
1364
1365 let mut merged = Score {
1366 id: uuid::Uuid::new_v4().to_string(),
1367 schema_version: 1,
1368 metadata: score.metadata.clone(),
1369 settings: score.settings.clone(),
1370 parts: vec![accordion_part],
1371 part_groups: Vec::new(),
1372 texts: score.texts.clone(),
1373 style_overrides: score.style_overrides.clone(),
1374 object_style_overrides: score.object_style_overrides.clone(),
1375 chord_definitions: score.chord_definitions.clone(),
1376 spanners: score.spanners.clone(),
1377 views: Vec::new(),
1378 };
1379
1380 let (fitted, shift) = octave_fit(&merged);
1381 merged = fitted;
1382 if shift != 0 {
1383 notes.push(format!(
1384 "アコーディオンの実用音域に合わせて{}オクターブ移調",
1385 shift / 12
1386 ));
1387 }
1388
1389 super::score::respell_score_to_key(&mut merged);
1390
1391 if analysis.ambiguous && right_hand_part_index.is_none() {
1392 notes.push("上位2パートの平均音高が僅差のため、右手パートの選択が曖昧です".to_string());
1393 }
1394
1395 Ok(ArrangeResult {
1396 score: merged,
1397 notes,
1398 })
1399}
1400
1401#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1402#[serde(rename_all = "kebab-case")]
1403pub enum AccordionLossPolicy {
1404 RequireAll,
1405 PermitReduction,
1406}
1407
1408#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1409pub struct AccordionReductionOptions {
1410 pub right_hand_part_index: Option<usize>,
1411 pub left_hand_part_indices: Vec<usize>,
1413 pub loss_policy: AccordionLossPolicy,
1414 pub octave_fit: bool,
1415}
1416
1417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1418pub struct AccordionReductionDiagnostic {
1419 pub code: String,
1420 pub message: String,
1421 pub source: Option<NoteAddr>,
1422 #[serde(default, skip_serializing_if = "Option::is_none")]
1423 pub source_location: Option<String>,
1424}
1425
1426#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1427pub struct AccordionSourceMapping {
1428 pub source: NoteAddr,
1429 pub target: Option<NoteAddr>,
1431}
1432
1433#[derive(Debug, Clone, Serialize, Deserialize)]
1434pub struct AccordionReductionResult {
1435 pub score: Option<Score>,
1437 pub resolved_options: AccordionReductionOptions,
1438 pub diagnostics: Vec<AccordionReductionDiagnostic>,
1439 pub source_map: Vec<AccordionSourceMapping>,
1440 pub transposition_semitones: i8,
1441 pub contract_version: u16,
1442}
1443
1444fn reduction_error(
1445 options: AccordionReductionOptions,
1446 code: &str,
1447 message: impl Into<String>,
1448) -> AccordionReductionResult {
1449 AccordionReductionResult {
1450 score: None,
1451 resolved_options: options,
1452 diagnostics: vec![AccordionReductionDiagnostic {
1453 code: code.to_string(),
1454 message: message.into(),
1455 source: None,
1456 source_location: None,
1457 }],
1458 source_map: Vec::new(),
1459 transposition_semitones: 0,
1460 contract_version: 1,
1461 }
1462}
1463
1464fn same_reduction_bar(a: &Measure, b: &Measure) -> bool {
1465 a.number == b.number
1466 && a.actual_length == b.actual_length
1467 && a.time_sig == b.time_sig
1468 && a.barline_left == b.barline_left
1469 && a.barline_right == b.barline_right
1470 && a.volta == b.volta
1471 && a.section_break == b.section_break
1472 && a.measure_repeat == b.measure_repeat
1473}
1474
1475fn reduction_measure_metadata(measure: &Measure) -> serde_json::Value {
1476 let mut metadata = measure.clone();
1477 metadata.voices = [vec![], vec![], vec![], vec![]];
1478 metadata.source_voice_numbers = [None; 4];
1479 serde_json::to_value(metadata).unwrap_or(serde_json::Value::Null)
1480}
1481
1482fn source_voice_addresses(score: &Score, part: usize, staff: usize, voice: usize) -> Vec<NoteAddr> {
1483 score.parts[part].staves[staff]
1484 .measures
1485 .iter()
1486 .enumerate()
1487 .flat_map(|(measure, bar)| {
1488 bar.voices[voice]
1489 .iter()
1490 .enumerate()
1491 .filter(|(_, note)| !note.is_rest)
1492 .map(move |(note, _)| NoteAddr {
1493 part,
1494 staff,
1495 measure,
1496 voice,
1497 note,
1498 })
1499 })
1500 .collect()
1501}
1502
1503pub fn arrange_for_accordion_with_options(
1508 score: &Score,
1509 mut options: AccordionReductionOptions,
1510) -> AccordionReductionResult {
1511 let analysis = analyze_for_accordion(score);
1512 let Some(right) = options.right_hand_part_index.or_else(|| {
1513 analysis
1514 .candidates
1515 .first()
1516 .map(|candidate| candidate.part_index)
1517 }) else {
1518 return reduction_error(
1519 options,
1520 "no-pitched-part",
1521 "no pitched source part is available",
1522 );
1523 };
1524 if right >= score.parts.len() || is_percussion_part(&score.parts[right]) {
1525 return reduction_error(
1526 options,
1527 "invalid-selection",
1528 "right-hand part is invalid or percussion",
1529 );
1530 }
1531 options.right_hand_part_index = Some(right);
1532 if options.left_hand_part_indices.is_empty() {
1533 options.left_hand_part_indices = analysis
1534 .candidates
1535 .iter()
1536 .map(|candidate| candidate.part_index)
1537 .filter(|index| *index != right)
1538 .collect();
1539 }
1540 let left_indices = &options.left_hand_part_indices;
1541 if left_indices.is_empty()
1542 || left_indices.iter().any(|index| {
1543 *index >= score.parts.len()
1544 || *index == right
1545 || is_percussion_part(&score.parts[*index])
1546 })
1547 || {
1548 let mut seen = std::collections::BTreeSet::new();
1549 left_indices.iter().any(|index| !seen.insert(index))
1550 }
1551 {
1552 return reduction_error(
1553 options,
1554 "invalid-selection",
1555 "left-hand selection must contain distinct pitched parts other than the right hand",
1556 );
1557 }
1558 let right_staff = match score.parts[right].staves.first() {
1559 Some(staff) if !staff.measures.is_empty() => staff,
1560 _ => {
1561 return reduction_error(
1562 options,
1563 "unsupported-source",
1564 "right-hand part has no staff or measures",
1565 );
1566 }
1567 };
1568 for &part_index in left_indices {
1569 let Some(staff) = score.parts[part_index].staves.first() else {
1570 return reduction_error(
1571 options,
1572 "unsupported-source",
1573 format!("left-hand part {part_index} has no staff"),
1574 );
1575 };
1576 if staff.measures.len() != right_staff.measures.len()
1577 || staff
1578 .measures
1579 .iter()
1580 .zip(&right_staff.measures)
1581 .any(|(a, b)| !same_reduction_bar(a, b))
1582 {
1583 return reduction_error(
1584 options,
1585 "misaligned-bars",
1586 format!(
1587 "part {part_index} has different bar numbering, lengths, repeats or endings"
1588 ),
1589 );
1590 }
1591 }
1592 let validation = super::validate::validate(score);
1593 if !validation.is_valid() {
1594 return reduction_error(
1595 options,
1596 "invalid-source",
1597 format!("source validation failed: {:?}", validation.errors[0]),
1598 );
1599 }
1600 if !score.spanners.is_empty() || !score.object_style_overrides.is_empty() {
1601 return reduction_error(
1602 options,
1603 "unsupported-notation",
1604 "source spanners or object style overrides require address remapping",
1605 );
1606 }
1607 let mut diagnostics = Vec::new();
1608 let primary_left = &score.parts[left_indices[0]].staves[0];
1609 for &part_index in left_indices.iter().skip(1) {
1610 for (measure_index, (primary, additional)) in primary_left
1611 .measures
1612 .iter()
1613 .zip(&score.parts[part_index].staves[0].measures)
1614 .enumerate()
1615 {
1616 if reduction_measure_metadata(primary) != reduction_measure_metadata(additional) {
1617 diagnostics.push(AccordionReductionDiagnostic {
1618 code: "unsupported-notation".to_string(),
1619 message: format!("part {part_index}, measure {measure_index} has distinct measure metadata that one left staff cannot retain"),
1620 source: None,
1621 source_location: Some(format!("/score/part/{}/staff/1/measure/{}", part_index + 1, measure_index + 1)),
1622 });
1623 }
1624 }
1625 }
1626 if !score.views.is_empty() || !score.part_groups.is_empty() {
1627 diagnostics.push(AccordionReductionDiagnostic {
1628 code: "unsupported-score-view".to_string(),
1629 message: "source views or part groups cannot be carried into one accordion part"
1630 .to_string(),
1631 source: None,
1632 source_location: Some("/score/views".to_string()),
1633 });
1634 }
1635 let mut source_map = Vec::new();
1636 let mut included = std::collections::BTreeSet::new();
1637 let right_copy = right_staff.clone();
1638 for voice in 0..4 {
1639 for address in source_voice_addresses(score, right, 0, voice) {
1640 source_map.push(AccordionSourceMapping {
1641 target: Some(NoteAddr {
1642 part: 0,
1643 staff: 0,
1644 ..address.clone()
1645 }),
1646 source: address,
1647 });
1648 }
1649 }
1650 included.insert((right, 0));
1651 let mut left_copy = score.parts[left_indices[0]].staves[0].clone();
1652 left_copy.clef = Clef::Bass;
1653 for measure in &mut left_copy.measures {
1654 measure.voices = [vec![], vec![], vec![], vec![]];
1655 measure.source_voice_numbers = [None; 4];
1656 }
1657 let mut next_voice = 0;
1658 for &part_index in left_indices {
1659 let staff = &score.parts[part_index].staves[0];
1660 included.insert((part_index, 0));
1661 for voice in 0..4 {
1662 if staff
1663 .measures
1664 .iter()
1665 .all(|measure| measure.voices[voice].is_empty())
1666 {
1667 continue;
1668 }
1669 if next_voice < 4 {
1670 for (measure_index, measure) in staff.measures.iter().enumerate() {
1671 left_copy.measures[measure_index].voices[next_voice] =
1672 measure.voices[voice].clone();
1673 for (note_index, note) in measure.voices[voice].iter().enumerate() {
1674 if note.is_rest {
1675 continue;
1676 }
1677 let address = NoteAddr {
1678 part: part_index,
1679 staff: 0,
1680 measure: measure_index,
1681 voice,
1682 note: note_index,
1683 };
1684 source_map.push(AccordionSourceMapping {
1685 target: Some(NoteAddr {
1686 part: 0,
1687 staff: 1,
1688 measure: measure_index,
1689 voice: next_voice,
1690 note: note_index,
1691 }),
1692 source: address,
1693 });
1694 }
1695 }
1696 next_voice += 1;
1697 } else {
1698 for address in source_voice_addresses(score, part_index, 0, voice) {
1699 diagnostics.push(AccordionReductionDiagnostic {
1700 code: "voice-capacity".to_string(),
1701 message: "left hand has more than four source voices".to_string(),
1702 source: Some(address.clone()),
1703 source_location: None,
1704 });
1705 source_map.push(AccordionSourceMapping {
1706 source: address,
1707 target: None,
1708 });
1709 }
1710 }
1711 }
1712 }
1713 for (part_index, part) in score.parts.iter().enumerate() {
1714 for staff_index in 0..part.staves.len() {
1715 if included.contains(&(part_index, staff_index)) {
1716 continue;
1717 }
1718 for voice in 0..4 {
1719 for address in source_voice_addresses(score, part_index, staff_index, voice) {
1720 diagnostics.push(AccordionReductionDiagnostic {
1721 code: "unselected-source".to_string(),
1722 message: "source note is outside the selected hands".to_string(),
1723 source: Some(address.clone()),
1724 source_location: None,
1725 });
1726 source_map.push(AccordionSourceMapping {
1727 source: address,
1728 target: None,
1729 });
1730 }
1731 }
1732 }
1733 }
1734 source_map.sort_by_key(|mapping| {
1735 (
1736 mapping.source.part,
1737 mapping.source.staff,
1738 mapping.source.measure,
1739 mapping.source.voice,
1740 mapping.source.note,
1741 )
1742 });
1743 if options.loss_policy == AccordionLossPolicy::RequireAll && !diagnostics.is_empty() {
1744 return AccordionReductionResult {
1745 score: None,
1746 resolved_options: options,
1747 diagnostics,
1748 source_map,
1749 transposition_semitones: 0,
1750 contract_version: 1,
1751 };
1752 }
1753 let mut output = score.clone();
1754 output.id = uuid::Uuid::new_v4().to_string();
1755 let mut accordion = Part::new("Accordion", "Acc.");
1756 accordion.midi_program = ACCORDION_PROGRAM;
1757 accordion.staves = vec![right_copy, left_copy];
1758 output.parts = vec![accordion];
1759 output.part_groups.clear();
1760 output.views.clear();
1761 let (output, shift) = if options.octave_fit {
1762 octave_fit(&output)
1763 } else {
1764 (output, 0)
1765 };
1766 if shift != 0 {
1767 diagnostics.push(AccordionReductionDiagnostic {
1768 code: "octave-shift".to_string(),
1769 message: format!("both hands transposed by {shift} semitones"),
1770 source: None,
1771 source_location: None,
1772 });
1773 }
1774 let output_validation = super::validate::validate(&output);
1775 if !output_validation.is_valid() {
1776 return reduction_error(
1777 options,
1778 "invalid-output",
1779 format!(
1780 "reduced score validation failed: {:?}",
1781 output_validation.errors[0]
1782 ),
1783 );
1784 }
1785 AccordionReductionResult {
1786 score: Some(output),
1787 resolved_options: options,
1788 diagnostics,
1789 source_map,
1790 transposition_semitones: shift,
1791 contract_version: 1,
1792 }
1793}
1794
1795#[cfg(test)]
1796mod tests {
1797 use super::*;
1798 use crate::model::{pitch::Step, score::Score, validate::validate};
1799
1800 fn note(step: Step, octave: i8, duration: Duration) -> Note {
1801 Note::new(Pitch::new(step, octave), duration)
1802 }
1803
1804 fn guided_fixture() -> Score {
1805 let mut score = Score::new("melody", 120, 4, 4, 0, 2);
1806 let first = &mut score.parts[0].staves[0].measures[0];
1807 first.number = 0;
1808 first.actual_length = super::super::score::MeasureLength::from_beats(1.0);
1809 first.voices[0] = vec![note(Step::C, 5, Duration::Quarter)];
1810 let second = &mut score.parts[0].staves[0].measures[1];
1811 second.voices[0] = vec![note(Step::D, 5, Duration::Whole)];
1812 second.barline_right = Barline::RepeatEnd;
1813 score
1814 }
1815
1816 fn guided_options(pattern: AccordionPattern) -> GuidedAccordionOptions {
1817 let chord = |root: &str| {
1818 serde_json::from_value(serde_json::json!({"root": root, "kind": "major", "bass": null}))
1819 .unwrap()
1820 };
1821 GuidedAccordionOptions {
1822 pattern,
1823 chords: vec![
1824 PositionedAccordionChord {
1825 measure_index: 0,
1826 offset_beats: 0.0,
1827 chord: chord("C"),
1828 },
1829 PositionedAccordionChord {
1830 measure_index: 1,
1831 offset_beats: 0.0,
1832 chord: chord("G"),
1833 },
1834 ],
1835 }
1836 }
1837
1838 #[test]
1839 fn guided_patterns_preserve_source_and_pickup_repeat_timing() {
1840 let score = guided_fixture();
1841 let before = serde_json::to_value(&score).unwrap();
1842 let original_events: Vec<_> = crate::to_playback_events(&score, &Default::default())
1843 .into_iter()
1844 .map(|event| (event.pitch_midi, event.time_beats, event.duration_beats))
1845 .collect();
1846 let mut bass_patterns = Vec::new();
1847 for pattern in [
1848 AccordionPattern::HeldBassChord,
1849 AccordionPattern::AlternatingBassChord,
1850 AccordionPattern::Arpeggio,
1851 ] {
1852 let result = arrange_with_explicit_chords(&score, guided_options(pattern));
1853 assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics);
1854 let arranged = result.score.expect("valid arrangement");
1855 assert_eq!(arranged.parts.len(), 1);
1856 assert_eq!(arranged.parts[0].staves.len(), 2);
1857 assert_eq!(arranged.parts[0].staves[0].measures[0].number, 0);
1858 assert_eq!(
1859 arranged.parts[0].staves[1].measures[0].actual_length,
1860 score.parts[0].staves[0].measures[0].actual_length
1861 );
1862 assert_eq!(
1863 arranged.parts[0].staves[1].measures[1].barline_right,
1864 Barline::RepeatEnd
1865 );
1866 assert_eq!(
1867 arranged.parts[0].staves[0].measures[0].voices[0][0].id,
1868 score.parts[0].staves[0].measures[0].voices[0][0].id
1869 );
1870 let melody_events: Vec<_> = crate::to_playback_events(&arranged, &Default::default())
1871 .into_iter()
1872 .filter(|event| {
1873 event
1874 .source
1875 .as_ref()
1876 .is_some_and(|address| address.staff == 0)
1877 })
1878 .map(|event| (event.pitch_midi, event.time_beats, event.duration_beats))
1879 .collect();
1880 assert_eq!(melody_events, original_events);
1881 bass_patterns.push(
1882 arranged.parts[0].staves[1].measures[1].voices[0]
1883 .iter()
1884 .map(|note| {
1885 (
1886 note.pitches.iter().map(Pitch::to_midi).collect::<Vec<_>>(),
1887 note.beats(),
1888 )
1889 })
1890 .collect::<Vec<_>>(),
1891 );
1892 }
1893 assert_ne!(bass_patterns[0], bass_patterns[1]);
1894 assert_ne!(bass_patterns[1], bass_patterns[2]);
1895 assert_eq!(serde_json::to_value(&score).unwrap(), before);
1896 }
1897
1898 #[test]
1899 fn guided_arrangement_rejects_uncovered_unsupported_and_out_of_range_input() {
1900 let score = guided_fixture();
1901 let mut options = guided_options(AccordionPattern::Arpeggio);
1902 options.chords.remove(0);
1903 let result = arrange_with_explicit_chords(&score, options);
1904 assert!(result.score.is_none());
1905 assert_eq!(result.diagnostics[0].code, "missing-chords");
1906
1907 let mut options = guided_options(AccordionPattern::AlternatingBassChord);
1908 options.chords[1].offset_beats = 0.5;
1909 let result = arrange_with_explicit_chords(&score, options);
1910 assert!(result.score.is_none());
1911 assert_eq!(result.diagnostics[0].code, "unsupported-rhythm");
1912
1913 let mut high = guided_fixture();
1914 high.parts[0].staves[0].measures[0].voices[0][0].pitches[0] = Pitch::new(Step::C, 9);
1915 let result =
1916 arrange_with_explicit_chords(&high, guided_options(AccordionPattern::HeldBassChord));
1917 assert!(result.score.is_none());
1918 assert_eq!(result.diagnostics[0].code, "out-of-range");
1919 }
1920
1921 #[test]
1922 fn guided_arrangement_carries_explicit_harmony_into_internal_short_bar() {
1923 let mut score = Score::new("short cadence", 120, 4, 4, 0, 2);
1924 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
1925 let cadence = &mut score.parts[0].staves[0].measures[1];
1926 cadence.actual_length = super::super::score::MeasureLength::from_beats(3.0);
1927 let mut dotted = note(Step::D, 5, Duration::Half);
1928 dotted.dot_count = 1;
1929 cadence.voices[0] = vec![dotted];
1930 let cadence_length = cadence.actual_length;
1931 let mut options = guided_options(AccordionPattern::AlternatingBassChord);
1932 options.chords.pop();
1933 let first = arrange_with_explicit_chords(&score, options.clone());
1934 let second = arrange_with_explicit_chords(&score, options);
1935 assert!(first.diagnostics.is_empty(), "{:?}", first.diagnostics);
1936 let arranged = first.score.unwrap();
1937 assert_eq!(
1938 arranged.parts[0].staves[1].measures[1].actual_length,
1939 cadence_length
1940 );
1941 let left = |score: &Score| {
1942 crate::to_playback_events(score, &Default::default())
1943 .into_iter()
1944 .filter(|event| {
1945 event
1946 .source
1947 .as_ref()
1948 .is_some_and(|address| address.staff == 1)
1949 })
1950 .map(|event| (event.pitch_midi, event.time_beats, event.duration_beats))
1951 .collect::<Vec<_>>()
1952 };
1953 assert_eq!(left(&arranged), left(second.score.as_ref().unwrap()));
1954 let mut starts = left(&arranged)
1955 .iter()
1956 .map(|event| event.1)
1957 .collect::<Vec<_>>();
1958 starts.dedup();
1959 assert_eq!(starts, [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1960 }
1961
1962 fn layered_options(pattern: AccordionPattern) -> LayeredAccordionOptions {
1963 let guided = guided_options(pattern);
1964 LayeredAccordionOptions {
1965 chords: guided.chords,
1966 alternative_chords: None,
1967 inversion: None,
1968 accompaniment: Some(pattern),
1969 melodic_voice: AccordionMelodicVoice::Unchanged,
1970 swing: AccordionSwingPolicy::Straight,
1971 bass_mode: AccordionBassMode::FreeBass,
1972 }
1973 }
1974
1975 #[test]
1976 fn extended_accompaniments_are_distinct_and_walking_approaches_next_bass() {
1977 let score = guided_fixture();
1978 let mut signatures = std::collections::BTreeSet::new();
1979 for pattern in [
1980 AccordionPattern::HeldBassChord,
1981 AccordionPattern::BassChordPulse,
1982 AccordionPattern::AlternatingBassChord,
1983 AccordionPattern::Arpeggio,
1984 AccordionPattern::RisingArpeggio,
1985 AccordionPattern::AlbertiBass,
1986 AccordionPattern::OffbeatChords,
1987 AccordionPattern::WalkingBass,
1988 ] {
1989 let result = arrange_with_layers(&score, layered_options(pattern));
1990 assert!(
1991 result.diagnostics.is_empty(),
1992 "{pattern:?}: {:?}",
1993 result.diagnostics
1994 );
1995 let arranged = result.score.unwrap();
1996 let signature = arranged.parts[0].staves[1].measures[1].voices[0]
1997 .iter()
1998 .map(|note| {
1999 (
2000 note.is_rest,
2001 note.pitches.iter().map(Pitch::to_midi).collect::<Vec<_>>(),
2002 note.beats().to_bits(),
2003 )
2004 })
2005 .collect::<Vec<_>>();
2006 assert!(
2007 signatures.insert(format!("{signature:?}")),
2008 "duplicate pattern: {pattern:?}"
2009 );
2010 if pattern == AccordionPattern::WalkingBass {
2011 let pickup = &arranged.parts[0].staves[1].measures[0].voices[0][0];
2012 assert_eq!(
2013 pickup.pitches[0].to_midi(),
2014 42,
2015 "approaches G bass (43) from below"
2016 );
2017 }
2018 }
2019 }
2020
2021 #[test]
2022 fn explicit_seventh_harmony_and_inversion_change_only_generated_notes() {
2023 let score = guided_fixture();
2024 let original = serde_json::to_value(&score).unwrap();
2025 let mut options = layered_options(AccordionPattern::HeldBassChord);
2026 options.chords[0].chord.kind = "major-seventh".to_string();
2027 options.chords[1].chord.kind = "dominant".to_string();
2028 let result = arrange_with_layers(&score, options.clone());
2029 assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics);
2030 let arranged = result.score.unwrap();
2031 assert_eq!(
2032 serde_json::to_value(&arranged.parts[0].staves[0]).unwrap(),
2033 serde_json::to_value(&score.parts[0].staves[0]).unwrap(),
2034 "the authored melody staff must be identical in the JSON contract"
2035 );
2036 assert!(
2037 arranged.parts[0].staves[1].measures[0].voices[0][0]
2038 .pitches
2039 .iter()
2040 .any(|pitch| pitch.to_midi() == 71)
2041 );
2042 assert!(
2043 arranged.parts[0].staves[1].measures[1].voices[0][0]
2044 .pitches
2045 .iter()
2046 .any(|pitch| pitch.to_midi() == 77)
2047 );
2048 assert_eq!(
2049 arranged.parts[0].staves[0].measures[0].voices[0][0].id,
2050 score.parts[0].staves[0].measures[0].voices[0][0].id
2051 );
2052
2053 options.alternative_chords = Some(guided_options(AccordionPattern::HeldBassChord).chords);
2054 options.alternative_chords.as_mut().unwrap()[0].chord.root = "F".to_string();
2055 options.inversion = Some(AccordionInversion::First);
2056 let alternative = arrange_with_layers(&score, options);
2057 assert!(
2058 alternative.diagnostics.is_empty(),
2059 "{:?}",
2060 alternative.diagnostics
2061 );
2062 assert_eq!(
2063 alternative.resolved_chords[0].chord.bass.as_deref(),
2064 Some("A")
2065 );
2066 assert_eq!(
2067 alternative.score.unwrap().parts[0].staves[1].measures[0].voices[0][0].pitches[0]
2068 .to_midi(),
2069 45
2070 );
2071 assert_eq!(serde_json::to_value(&score).unwrap(), original);
2072 }
2073
2074 #[test]
2075 fn supplied_jazz_sevenths_have_exact_pitch_classes_and_symbols() {
2076 let mut score = Score::new("sevenths", 120, 4, 4, 0, 1);
2077 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
2078 for (root, kind, expected) in [
2079 ("C", "major-seventh", [0, 4, 7, 11]),
2080 ("F", "major-seventh", [5, 9, 0, 4]),
2081 ("A", "minor-seventh", [9, 0, 4, 7]),
2082 ("D", "minor-seventh", [2, 5, 9, 0]),
2083 ("E", "minor-seventh", [4, 7, 11, 2]),
2084 ("G", "dominant", [7, 11, 2, 5]),
2085 ("E", "dominant", [4, 8, 11, 2]),
2086 ] {
2087 let mut options = layered_options(AccordionPattern::HeldBassChord);
2088 options.chords.truncate(1);
2089 options.chords[0].chord.root = root.to_string();
2090 options.chords[0].chord.kind = kind.to_string();
2091 let result = arrange_with_layers(&score, options);
2092 assert!(
2093 result.diagnostics.is_empty(),
2094 "{root} {kind}: {:?}",
2095 result.diagnostics
2096 );
2097 let arranged = result.score.unwrap();
2098 let chord = &arranged.parts[0].staves[1].measures[0].voices[0][0];
2099 assert_eq!(
2100 chord
2101 .pitches
2102 .iter()
2103 .skip(1)
2104 .map(|pitch| pitch.to_midi() % 12)
2105 .collect::<Vec<_>>(),
2106 expected.map(|value| value as i16)
2107 );
2108 assert_eq!(
2109 chord
2110 .chord_symbol
2111 .as_ref()
2112 .map(|symbol| (symbol.root.as_str(), symbol.kind.as_str())),
2113 Some((root, kind))
2114 );
2115 }
2116 }
2117
2118 #[test]
2119 fn notated_swing_is_exact_two_to_one_without_measure_drift() {
2120 let score = guided_fixture();
2121 let mut options = layered_options(AccordionPattern::BassChordPulse);
2122 options.swing = AccordionSwingPolicy::NotatedAccompanimentTwoToOne;
2123 let result = arrange_with_layers(&score, options);
2124 assert!(result.notated_swing);
2125 assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics);
2126 let arranged = result.score.unwrap();
2127 let notes = &arranged.parts[0].staves[1].measures[1].voices[0];
2128 assert_eq!(notes.len(), 8);
2129 assert_eq!(
2130 crate::tuplet_group_ranges(notes),
2131 [(0, 1), (2, 3), (4, 5), (6, 7)]
2132 );
2133 for pair in notes.chunks(2) {
2134 assert_eq!(pair.len(), 2);
2135 assert!((pair[0].beats() / pair[1].beats() - 2.0).abs() < 1e-9);
2136 assert_eq!(
2137 pair[0]
2138 .tuplet
2139 .as_ref()
2140 .map(|tuplet| (tuplet.actual_notes, tuplet.normal_notes)),
2141 Some((3, 2))
2142 );
2143 }
2144 assert!((notes.iter().map(Note::beats).sum::<f64>() - 4.0).abs() < 1e-9);
2145 let starts = crate::to_playback_events(&arranged, &Default::default())
2146 .into_iter()
2147 .filter(|event| {
2148 event
2149 .source
2150 .as_ref()
2151 .is_some_and(|address| address.staff == 1 && address.measure == 1)
2152 })
2153 .map(|event| event.time_beats)
2154 .collect::<Vec<_>>();
2155 assert!(
2156 starts
2157 .iter()
2158 .any(|time| (*time - (1.0 + 2.0 / 3.0)).abs() < 1e-9)
2159 );
2160 }
2161
2162 #[test]
2163 fn melodic_voice_layer_is_independent_of_accompaniment() {
2164 let mut score = Score::new("layers", 120, 4, 4, 0, 1);
2165 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::G, 4, Duration::Whole)];
2166 let mut options = layered_options(AccordionPattern::BassChordPulse);
2167 options.chords.truncate(1);
2168 options.melodic_voice = AccordionMelodicVoice::ThirdBelow;
2169 let with_voice = arrange_with_layers(&score, options.clone());
2170 assert!(
2171 with_voice.diagnostics.is_empty(),
2172 "{:?}",
2173 with_voice.diagnostics
2174 );
2175 let arranged = with_voice.score.unwrap();
2176 assert_eq!(
2177 arranged.parts[0].staves[0].measures[0].voices[1][0].pitches[0].to_midi(),
2178 64
2179 );
2180 options.melodic_voice = AccordionMelodicVoice::Unchanged;
2181 let without_voice = arrange_with_layers(&score, options.clone()).score.unwrap();
2182 let bass = |score: &Score| {
2183 score.parts[0].staves[1].measures[0].voices[0]
2184 .iter()
2185 .map(|note| {
2186 (
2187 note.pitches.iter().map(Pitch::to_midi).collect::<Vec<_>>(),
2188 note.beats(),
2189 )
2190 })
2191 .collect::<Vec<_>>()
2192 };
2193 assert_eq!(bass(&arranged), bass(&without_voice));
2194 options.accompaniment = None;
2195 options.melodic_voice = AccordionMelodicVoice::ThirdBelow;
2196 let without_bass = arrange_with_layers(&score, options);
2197 assert!(
2198 without_bass.score.as_ref().unwrap().parts[0].staves[1].measures[0].voices[0]
2199 .is_empty()
2200 );
2201 assert_eq!(
2202 without_bass.score.unwrap().parts[0].staves[0].measures[0].voices[1][0].pitches[0]
2203 .to_midi(),
2204 64
2205 );
2206
2207 score.parts[0].staves[0].measures[0].voices[0][0].pitches[0] = Pitch::new(Step::C, 5);
2208 for (voice, expected) in [
2209 (AccordionMelodicVoice::SixthBelow, 64),
2210 (AccordionMelodicVoice::OctaveBelow, 60),
2211 ] {
2212 let mut options = layered_options(AccordionPattern::HeldBassChord);
2213 options.chords.truncate(1);
2214 options.melodic_voice = voice;
2215 let result = arrange_with_layers(&score, options);
2216 assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics);
2217 assert_eq!(
2218 result.score.unwrap().parts[0].staves[0].measures[0].voices[1][0].pitches[0]
2219 .to_midi(),
2220 expected
2221 );
2222 }
2223 }
2224
2225 #[test]
2226 fn standard_bass_rejects_seventh_realization_without_partial_score() {
2227 let score = guided_fixture();
2228 let mut options = layered_options(AccordionPattern::HeldBassChord);
2229 options.bass_mode = AccordionBassMode::StandardBass;
2230 options.chords[0].chord.kind = "major-seventh".to_string();
2231 let result = arrange_with_layers(&score, options);
2232 assert!(result.score.is_none());
2233 assert_eq!(result.diagnostics[0].code, "unsupported-standard-bass");
2234 }
2235
2236 #[test]
2237 fn walking_bass_keeps_pickups_rests_and_supported_meters_aligned() {
2238 for (numerator, duration, dots) in [
2239 (2, Duration::Quarter, 0),
2240 (3, Duration::Half, 0),
2241 (4, Duration::Half, 1),
2242 ] {
2243 let mut score = Score::new("meter", 120, numerator, 4, 0, 2);
2244 score.parts[0].staves[0].measures[0].actual_length =
2245 super::super::score::MeasureLength::from_beats(1.0);
2246 score.parts[0].staves[0].measures[0].voices[0] =
2247 vec![note(Step::C, 5, Duration::Quarter)];
2248 let mut final_note = note(Step::G, 4, duration);
2249 final_note.dot_count = dots;
2250 score.parts[0].staves[0].measures[1].voices[0] =
2251 vec![Note::rest(Duration::Quarter), final_note];
2252 let result =
2253 arrange_with_layers(&score, layered_options(AccordionPattern::WalkingBass));
2254 assert!(
2255 result.diagnostics.is_empty(),
2256 "{numerator}/4: {:?}",
2257 result.diagnostics
2258 );
2259 let arranged = result.score.unwrap();
2260 assert!(
2261 (arranged.parts[0].staves[1].measures[0].voices[0]
2262 .iter()
2263 .map(Note::beats)
2264 .sum::<f64>()
2265 - 1.0)
2266 .abs()
2267 < 1e-9
2268 );
2269 assert!(
2270 (arranged.parts[0].staves[1].measures[1].voices[0]
2271 .iter()
2272 .map(Note::beats)
2273 .sum::<f64>()
2274 - f64::from(numerator))
2275 .abs()
2276 < 1e-9
2277 );
2278 let melody = crate::to_playback_events(&arranged, &Default::default())
2279 .into_iter()
2280 .filter(|event| {
2281 event
2282 .source
2283 .as_ref()
2284 .is_some_and(|address| address.staff == 0)
2285 })
2286 .map(|event| event.time_beats)
2287 .collect::<Vec<_>>();
2288 assert_eq!(melody, [0.0, 2.0]);
2289 }
2290 }
2291
2292 #[test]
2293 fn layered_score_preserves_accidental_pickup_meter_change_and_short_final_bar() {
2294 let mut score = Score::new("mixed bars", 120, 4, 4, 0, 3);
2295 let first = &mut score.parts[0].staves[0].measures[0];
2296 first.number = 0;
2297 first.actual_length = super::super::score::MeasureLength::from_beats(1.0);
2298 first.voices[0] = vec![Note::new(
2299 Pitch::with_alter(Step::C, 5, 1),
2300 Duration::Quarter,
2301 )];
2302 let middle = &mut score.parts[0].staves[0].measures[1];
2303 middle.time_sig = Some(super::super::notation::TimeSignature {
2304 numerator: 3,
2305 denominator: 4,
2306 });
2307 middle.voices[0] = vec![
2308 Note::rest(Duration::Quarter),
2309 note(Step::G, 4, Duration::Half),
2310 ];
2311 let last = &mut score.parts[0].staves[0].measures[2];
2312 last.time_sig = Some(super::super::notation::TimeSignature {
2313 numerator: 4,
2314 denominator: 4,
2315 });
2316 last.actual_length = super::super::score::MeasureLength::from_beats(2.0);
2317 last.voices[0] = vec![note(Step::F, 4, Duration::Half)];
2318
2319 let input_json = serde_json::to_value(&score).unwrap();
2320 let input_staff = serde_json::to_value(&score.parts[0].staves[0]).unwrap();
2321 let mut options = layered_options(AccordionPattern::WalkingBass);
2322 options.chords.push(PositionedAccordionChord {
2323 measure_index: 2,
2324 offset_beats: 0.0,
2325 chord: serde_json::from_value(
2326 serde_json::json!({"root":"F","kind":"major","bass":null}),
2327 )
2328 .unwrap(),
2329 });
2330 let result = arrange_with_layers(&score, options);
2331 assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics);
2332 let arranged = result.score.unwrap();
2333 assert_eq!(
2334 serde_json::to_value(&arranged.parts[0].staves[0]).unwrap(),
2335 input_staff
2336 );
2337 assert_eq!(serde_json::to_value(&score).unwrap(), input_json);
2338 let left = &arranged.parts[0].staves[1].measures;
2339 assert_eq!(
2340 left.iter()
2341 .map(|bar| bar.voices[0].iter().map(Note::beats).sum::<f64>())
2342 .collect::<Vec<_>>(),
2343 [1.0, 3.0, 2.0]
2344 );
2345 assert_eq!(
2346 left[1].time_sig.as_ref().map(|meter| meter.numerator),
2347 Some(3)
2348 );
2349 assert_eq!(
2350 left[2].actual_length,
2351 super::super::score::MeasureLength::from_beats(2.0)
2352 );
2353 let melody_events = |source: &Score| {
2354 crate::to_playback_events(source, &Default::default())
2355 .into_iter()
2356 .filter(|event| {
2357 event
2358 .source
2359 .as_ref()
2360 .is_some_and(|address| address.staff == 0)
2361 })
2362 .map(|event| (event.pitch_midi, event.time_beats, event.duration_beats))
2363 .collect::<Vec<_>>()
2364 };
2365 assert_eq!(melody_events(&arranged), melody_events(&score));
2366 }
2367
2368 fn reduction_fixture() -> Score {
2369 let mut score = guided_fixture();
2370 let mut bass = score.parts[0].clone();
2371 bass.name = "bass".to_string();
2372 bass.staves[0].clef = Clef::Bass;
2373 bass.staves[0].measures[0].voices[0] = vec![note(Step::C, 3, Duration::Quarter)];
2374 bass.staves[0].measures[1].voices[0] = vec![note(Step::G, 3, Duration::Whole)];
2375 let mut middle = bass.clone();
2376 middle.name = "countermelody".to_string();
2377 middle.staves[0].measures[0].voices[0] = vec![note(Step::E, 4, Duration::Quarter)];
2378 middle.staves[0].measures[1].voices[0] = vec![note(Step::F, 4, Duration::Whole)];
2379 score.parts.push(bass);
2380 score.parts.push(middle);
2381 score
2382 }
2383
2384 fn reduction_options(policy: AccordionLossPolicy) -> AccordionReductionOptions {
2385 AccordionReductionOptions {
2386 right_hand_part_index: Some(0),
2387 left_hand_part_indices: vec![1, 2],
2388 loss_policy: policy,
2389 octave_fit: false,
2390 }
2391 }
2392
2393 #[test]
2394 fn explicit_reduction_preserves_three_parts_and_source_timing() {
2395 let score = reduction_fixture();
2396 let before = serde_json::to_value(&score).unwrap();
2397 let result = arrange_for_accordion_with_options(
2398 &score,
2399 reduction_options(AccordionLossPolicy::RequireAll),
2400 );
2401 assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics);
2402 let reduced = result.score.unwrap();
2403 assert_eq!(reduced.parts[0].staves.len(), 2);
2404 assert_eq!(
2405 reduced.parts[0].staves[1].measures[0].actual_length,
2406 score.parts[1].staves[0].measures[0].actual_length
2407 );
2408 assert_eq!(
2409 reduced.parts[0].staves[1].measures[1].barline_right,
2410 Barline::RepeatEnd
2411 );
2412 assert_eq!(result.source_map.len(), 6);
2413 assert!(
2414 result
2415 .source_map
2416 .iter()
2417 .all(|mapping| mapping.target.is_some())
2418 );
2419 for mapping in &result.source_map {
2420 let source_note = &score.parts[mapping.source.part].staves[mapping.source.staff]
2421 .measures[mapping.source.measure]
2422 .voices[mapping.source.voice][mapping.source.note];
2423 let target = mapping.target.as_ref().unwrap();
2424 let copied = &reduced.parts[target.part].staves[target.staff].measures[target.measure]
2425 .voices[target.voice][target.note];
2426 assert_eq!(copied.id, source_note.id);
2427 assert_eq!(copied.beats(), source_note.beats());
2428 assert_eq!(copied.pitches, source_note.pitches);
2429 }
2430 let original_melody = crate::to_playback_events(&score, &Default::default())
2431 .into_iter()
2432 .filter(|event| event.source.as_ref().is_some_and(|addr| addr.part == 0))
2433 .map(|event| (event.pitch_midi, event.time_beats))
2434 .collect::<Vec<_>>();
2435 let reduced_melody = crate::to_playback_events(&reduced, &Default::default())
2436 .into_iter()
2437 .filter(|event| event.source.as_ref().is_some_and(|addr| addr.staff == 0))
2438 .map(|event| (event.pitch_midi, event.time_beats))
2439 .collect::<Vec<_>>();
2440 assert_eq!(original_melody, reduced_melody);
2441 assert_eq!(serde_json::to_value(&score).unwrap(), before);
2442 }
2443
2444 #[test]
2445 fn lossless_reduction_rejects_voice_overflow_and_permitted_reduction_reports_each_note() {
2446 let mut score = reduction_fixture();
2447 for offset in 0..3 {
2448 let mut extra = score.parts[1].clone();
2449 extra.name = format!("extra {offset}");
2450 extra.staves[0].measures[0].voices[0][0].pitches[0] = Pitch::new(Step::D, 3 + offset);
2451 score.parts.push(extra);
2452 }
2453 let mut options = reduction_options(AccordionLossPolicy::RequireAll);
2454 options.left_hand_part_indices = vec![1, 2, 3, 4, 5];
2455 let rejected = arrange_for_accordion_with_options(&score, options.clone());
2456 assert!(rejected.score.is_none());
2457 assert_eq!(rejected.diagnostics.len(), 2);
2458 assert!(rejected.diagnostics.iter().all(|diagnostic| {
2459 diagnostic.code == "voice-capacity"
2460 && diagnostic
2461 .source
2462 .as_ref()
2463 .is_some_and(|source| source.part == 5)
2464 }));
2465 options.loss_policy = AccordionLossPolicy::PermitReduction;
2466 let reduced = arrange_for_accordion_with_options(&score, options);
2467 assert!(reduced.score.is_some());
2468 assert_eq!(
2469 reduced
2470 .source_map
2471 .iter()
2472 .filter(|mapping| mapping.target.is_none())
2473 .count(),
2474 2
2475 );
2476 assert!(
2477 reduced
2478 .source_map
2479 .iter()
2480 .filter(|mapping| mapping.target.is_none())
2481 .all(|mapping| mapping.source.part == 5)
2482 );
2483 }
2484
2485 #[test]
2486 fn reduction_reports_unselected_notes_and_optional_octave_shift() {
2487 let mut score = reduction_fixture();
2488 let mut options = reduction_options(AccordionLossPolicy::RequireAll);
2489 options.left_hand_part_indices = vec![1];
2490 let rejected = arrange_for_accordion_with_options(&score, options.clone());
2491 assert!(rejected.score.is_none());
2492 assert_eq!(rejected.diagnostics.len(), 2);
2493 assert!(rejected.diagnostics.iter().all(|diagnostic| {
2494 diagnostic.code == "unselected-source"
2495 && diagnostic
2496 .source
2497 .as_ref()
2498 .is_some_and(|source| source.part == 2)
2499 }));
2500
2501 options.loss_policy = AccordionLossPolicy::PermitReduction;
2502 let reduced = arrange_for_accordion_with_options(&score, options.clone());
2503 assert!(reduced.score.is_some());
2504 assert_eq!(
2505 reduced
2506 .source_map
2507 .iter()
2508 .filter(|mapping| mapping.target.is_none())
2509 .count(),
2510 2
2511 );
2512
2513 for part in &mut score.parts {
2514 for measure in &mut part.staves[0].measures {
2515 for note in &mut measure.voices[0] {
2516 if !note.is_rest {
2517 note.pitches[0] = Pitch::new(Step::C, 7);
2518 }
2519 }
2520 }
2521 }
2522 options.left_hand_part_indices = vec![1, 2];
2523 options.loss_policy = AccordionLossPolicy::RequireAll;
2524 options.octave_fit = true;
2525 let fitted = arrange_for_accordion_with_options(&score, options);
2526 assert!(fitted.score.is_some());
2527 assert_ne!(fitted.transposition_semitones, 0);
2528 assert!(
2529 fitted
2530 .diagnostics
2531 .iter()
2532 .any(|diagnostic| diagnostic.code == "octave-shift")
2533 );
2534 }
2535
2536 #[test]
2537 fn reduction_reports_measure_annotations_that_cannot_merge() {
2538 let mut score = reduction_fixture();
2539 score.parts[2].staves[0].measures[1].tempo_text = Some("rubato".to_string());
2540 let options = reduction_options(AccordionLossPolicy::RequireAll);
2541 let rejected = arrange_for_accordion_with_options(&score, options.clone());
2542 assert!(rejected.score.is_none());
2543 assert!(rejected.diagnostics.iter().any(|diagnostic| diagnostic.code
2544 == "unsupported-notation"
2545 && diagnostic.source_location.as_deref() == Some("/score/part/3/staff/1/measure/2")));
2546 let mut permitted = options;
2547 permitted.loss_policy = AccordionLossPolicy::PermitReduction;
2548 let result = arrange_for_accordion_with_options(&score, permitted);
2549 assert!(result.score.is_some());
2550 assert!(
2551 result
2552 .diagnostics
2553 .iter()
2554 .any(|diagnostic| diagnostic.code == "unsupported-notation")
2555 );
2556 }
2557
2558 #[test]
2559 fn analyze_ranks_by_mean_pitch_descending() {
2560 let mut score = Score::new("T", 120, 4, 4, 0, 1);
2561 score.parts[0].name = "Low".to_string();
2562 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
2563
2564 let mut high = score.parts[0].clone();
2565 high.name = "High".to_string();
2566 high.staves[0].measures[0].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
2567 score.parts.push(high);
2568
2569 let analysis = analyze_for_accordion(&score);
2570 assert_eq!(analysis.candidates.len(), 2);
2571 assert_eq!(analysis.candidates[0].name, "High");
2572 assert_eq!(analysis.candidates[1].name, "Low");
2573 }
2574
2575 #[test]
2576 fn analyze_excludes_percussion_channel() {
2577 let mut score = Score::new("T", 120, 4, 4, 0, 1);
2578 score.parts[0].midi_channel = 9;
2579 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 4, Duration::Whole)];
2580 assert!(analyze_for_accordion(&score).candidates.is_empty());
2581 }
2582
2583 #[test]
2584 fn analyze_excludes_silent_part() {
2585 let score = Score::new("T", 120, 4, 4, 0, 1);
2588 assert!(analyze_for_accordion(&score).candidates.is_empty());
2589 }
2590
2591 #[test]
2592 fn arrange_no_candidates_errors() {
2593 let score = Score::new("T", 120, 4, 4, 0, 1);
2594 assert!(arrange_for_accordion(&score, None).is_err());
2595 }
2596
2597 #[test]
2598 fn arrange_rejects_out_of_range_part_index() {
2599 let mut score = Score::new("T", 120, 4, 4, 0, 1);
2600 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 4, Duration::Whole)];
2601 let result = arrange_for_accordion(&score, Some(99));
2602 assert!(matches!(result, Err(Error::PartNotFound(99))));
2603 }
2604
2605 #[test]
2606 fn arrange_single_part_splits_chord_and_sets_accordion_program() {
2607 let mut score = Score::new("T", 120, 4, 4, 0, 1);
2608 let mut chord = note(Step::C, 5, Duration::Whole);
2609 chord.pitches.push(Pitch::new(Step::C, 3));
2610 score.parts[0].staves[0].measures[0].voices[0] = vec![chord];
2611
2612 let result = arrange_for_accordion(&score, None).unwrap();
2613 assert_eq!(result.score.parts.len(), 1);
2614 assert_eq!(result.score.parts[0].midi_program, ACCORDION_PROGRAM);
2615 assert_eq!(result.score.parts[0].staves.len(), 2);
2616 assert_eq!(result.score.parts[0].staves[0].clef, Clef::Treble);
2617 assert_eq!(result.score.parts[0].staves[1].clef, Clef::Bass);
2618 }
2619
2620 #[test]
2621 fn arrange_two_parts_puts_higher_mean_pitch_on_treble() {
2622 let mut score = Score::new("T", 120, 4, 4, 0, 2);
2623 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
2624 score.parts[0].staves[0].measures[1].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
2625
2626 let mut melody = score.parts[0].clone();
2627 melody.name = "Melody".to_string();
2628 melody.staves[0].measures[0].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
2629 melody.staves[0].measures[1].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
2630 score.parts.push(melody);
2631
2632 let result = arrange_for_accordion(&score, None).unwrap();
2633 assert_eq!(result.score.parts[0].midi_program, ACCORDION_PROGRAM);
2634 let treble_note = &result.score.parts[0].staves[0].measures[0].voices[0][0];
2635 assert!(!treble_note.is_rest);
2636 assert_eq!(treble_note.pitches[0].octave, 5);
2637 }
2638
2639 #[test]
2640 fn arrange_right_hand_override_picks_requested_part() {
2641 let mut score = Score::new("T", 120, 4, 4, 0, 1);
2642 score.parts[0].name = "Low".to_string();
2643 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
2644
2645 let mut high = score.parts[0].clone();
2646 high.name = "High".to_string();
2647 high.staves[0].measures[0].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
2648 score.parts.push(high);
2649
2650 let result = arrange_for_accordion(&score, Some(0)).unwrap();
2652 let treble_note = &result.score.parts[0].staves[0].measures[0].voices[0][0];
2653 assert!(!treble_note.is_rest);
2654 assert_eq!(treble_note.pitches[0].octave, 3);
2655 }
2656
2657 #[test]
2658 fn arranged_measures_pass_beat_count_validation() {
2659 let mut score = Score::new("T", 120, 4, 4, 0, 2);
2663 score.parts[0].staves[0].measures[0].voices[0] = vec![
2664 note(Step::C, 3, Duration::Half),
2665 note(Step::E, 3, Duration::Half),
2666 ];
2667 score.parts[0].staves[0].measures[1].voices[0] = vec![note(Step::G, 3, Duration::Whole)];
2668
2669 let mut melody = score.parts[0].clone();
2670 melody.staves[0].measures[0].voices[0] = vec![
2671 note(Step::C, 5, Duration::Quarter),
2672 note(Step::D, 5, Duration::Quarter),
2673 note(Step::E, 5, Duration::Quarter),
2674 note(Step::F, 5, Duration::Quarter),
2675 ];
2676 melody.staves[0].measures[1].voices[0] = vec![note(Step::G, 5, Duration::Whole)];
2677 score.parts.push(melody);
2678
2679 let result = arrange_for_accordion(&score, None).unwrap();
2680 let report = validate(&result.score);
2681 assert!(
2682 report.errors.is_empty(),
2683 "expected no beat-count errors, got {:?}",
2684 report.errors
2685 );
2686 }
2687}