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