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