1use serde::Serialize;
14use std::collections::BTreeMap;
15
16use super::duration::Duration;
17use super::gm::instrument_range;
18use super::notation::{Barline, Clef};
19use super::pitch::Pitch;
20use super::score::{Measure, Note, Part, Score, Staff};
21use crate::Error;
22
23const ACCORDION_PROGRAM: u8 = 21;
24const GRID: f64 = 64.0;
27const SPLIT_MIDI: u8 = 60;
30
31#[derive(Debug, Clone, Serialize)]
32pub struct PartCandidate {
33 pub part_index: usize,
34 pub name: String,
35 pub mean_pitch: f64,
36}
37
38#[derive(Debug, Clone, Serialize)]
39pub struct AccordionAnalysis {
40 pub candidates: Vec<PartCandidate>,
41 pub ambiguous: bool,
44}
45
46#[derive(Debug, Clone, Serialize)]
47pub struct ArrangeResult {
48 pub score: Score,
49 pub notes: Vec<String>,
50}
51
52fn is_percussion_part(part: &Part) -> bool {
53 part.midi_channel == 9 || part.staves.iter().any(|s| s.clef == Clef::Percussion)
54}
55
56fn mean_pitch(part: &Part) -> Option<f64> {
57 let (sum, count) = part
58 .staves
59 .iter()
60 .flat_map(|s| s.measures.iter())
61 .flat_map(|m| m.voices.iter())
62 .flat_map(|v| v.iter())
63 .filter(|n| !n.is_rest && !n.is_grace)
64 .flat_map(|n| n.pitches.iter())
65 .fold((0i64, 0i64), |(sum, count), p| {
66 (sum + p.to_midi() as i64, count + 1)
67 });
68 if count == 0 {
69 None
70 } else {
71 Some(sum as f64 / count as f64)
72 }
73}
74
75pub fn analyze_for_accordion(score: &Score) -> AccordionAnalysis {
78 let mut candidates: Vec<PartCandidate> = score
79 .parts
80 .iter()
81 .enumerate()
82 .filter(|(_, p)| !is_percussion_part(p))
83 .filter_map(|(i, p)| {
84 mean_pitch(p).map(|mp| PartCandidate {
85 part_index: i,
86 name: p.name.clone(),
87 mean_pitch: mp,
88 })
89 })
90 .collect();
91 candidates.sort_by(|a, b| {
92 b.mean_pitch
93 .partial_cmp(&a.mean_pitch)
94 .unwrap_or(std::cmp::Ordering::Equal)
95 });
96 let ambiguous =
97 candidates.len() >= 2 && (candidates[0].mean_pitch - candidates[1].mean_pitch).abs() < 3.0;
98 AccordionAnalysis {
99 candidates,
100 ambiguous,
101 }
102}
103
104struct SourceEvent {
105 onset_ticks: i64,
106 beats: f64,
107 pitches: Vec<Pitch>,
108}
109
110fn events_from_parts(
111 score: &Score,
112 part_indices: &[usize],
113 measure_idx: usize,
114) -> Vec<SourceEvent> {
115 let mut events = Vec::new();
116 for &pi in part_indices {
117 let part = &score.parts[pi];
118 for staff in &part.staves {
119 let Some(measure) = staff.measures.get(measure_idx) else {
120 continue;
121 };
122 if measure.multi_rest_count.is_some() {
123 continue;
124 }
125 for voice in &measure.voices {
126 let mut onset = 0.0f64;
127 for note in voice {
128 let b = note.beats();
129 if !note.is_rest && !note.is_grace && !note.pitches.is_empty() {
130 let pitches = note
131 .pitches
132 .iter()
133 .map(|p| {
134 let midi = (p.to_midi() + staff.transpose_semitones as i16)
135 .clamp(0, 127) as u8;
136 Pitch::from_midi(midi, false)
137 })
138 .collect();
139 events.push(SourceEvent {
140 onset_ticks: (onset * GRID).round() as i64,
141 beats: b,
142 pitches,
143 });
144 }
145 onset += b;
146 }
147 }
148 }
149 }
150 events
151}
152
153fn events_from_pitch_split(
157 score: &Score,
158 part_index: usize,
159 measure_idx: usize,
160 high: bool,
161) -> Vec<SourceEvent> {
162 let part = &score.parts[part_index];
163 let mut events = Vec::new();
164 for staff in &part.staves {
165 let Some(measure) = staff.measures.get(measure_idx) else {
166 continue;
167 };
168 if measure.multi_rest_count.is_some() {
169 continue;
170 }
171 for voice in &measure.voices {
172 let mut onset = 0.0f64;
173 for note in voice {
174 let b = note.beats();
175 if !note.is_rest && !note.is_grace {
176 let pitches: Vec<Pitch> = note
177 .pitches
178 .iter()
179 .filter_map(|p| {
180 let midi = (p.to_midi() + staff.transpose_semitones as i16)
181 .clamp(0, 127) as u8;
182 let keep = if high {
183 midi >= SPLIT_MIDI
184 } else {
185 midi < SPLIT_MIDI
186 };
187 if keep {
188 Some(Pitch::from_midi(midi, false))
189 } else {
190 None
191 }
192 })
193 .collect();
194 if !pitches.is_empty() {
195 events.push(SourceEvent {
196 onset_ticks: (onset * GRID).round() as i64,
197 beats: b,
198 pitches,
199 });
200 }
201 }
202 onset += b;
203 }
204 }
205 }
206 events
207}
208
209fn fill_rests(notes: &mut Vec<Note>, mut remaining: f64) {
210 while remaining > 1.0 / GRID {
211 let dur = Duration::whole_filling_beats(remaining);
212 let filled = dur.beats(0);
213 notes.push(Note::rest(dur));
214 remaining -= filled;
215 }
216}
217
218fn assemble_staff(
222 clef: Clef,
223 template_per_measure: Vec<Option<Measure>>,
224 events_per_measure: Vec<Vec<SourceEvent>>,
225 default_ts: super::notation::TimeSignature,
226) -> Staff {
227 let mut current_ts = default_ts;
228 let mut measures = Vec::with_capacity(template_per_measure.len());
229
230 for (mi, (template, events)) in template_per_measure
231 .into_iter()
232 .zip(events_per_measure)
233 .enumerate()
234 {
235 if let Some(ts) = template.as_ref().and_then(|m| m.time_sig.as_ref()) {
236 current_ts = ts.clone();
237 }
238 let total_beats = current_ts.total_beats();
239
240 let mut buckets: BTreeMap<i64, Vec<SourceEvent>> = BTreeMap::new();
241 for ev in events {
242 buckets.entry(ev.onset_ticks).or_default().push(ev);
243 }
244 let keys: Vec<i64> = buckets.keys().copied().collect();
245
246 let mut voice0: Vec<Note> = Vec::new();
247 let mut cursor = 0.0f64;
248 for (idx, &key) in keys.iter().enumerate() {
249 let onset = key as f64 / GRID;
250 if onset < cursor - 1.0 / GRID {
251 continue;
252 } if onset > cursor {
254 fill_rests(&mut voice0, onset - cursor);
255 cursor = onset;
256 }
257
258 let next_onset = keys
259 .get(idx + 1)
260 .map(|&k| k as f64 / GRID)
261 .unwrap_or(total_beats);
262 let group = &buckets[&key];
263 let shortest = group.iter().map(|e| e.beats).fold(f64::MAX, f64::min);
264 let cap = (next_onset - onset).max(1.0 / GRID);
265 let sounding = shortest.min(cap).min((total_beats - onset).max(1.0 / GRID));
266
267 let mut pitches: Vec<Pitch> = Vec::new();
268 let mut seen_midi: Vec<u8> = Vec::new();
269 for ev in group {
270 for p in &ev.pitches {
271 let midi = p.to_midi().clamp(0, 127) as u8;
272 if !seen_midi.contains(&midi) {
273 seen_midi.push(midi);
274 pitches.push(p.clone());
275 }
276 }
277 }
278 if pitches.is_empty() {
279 continue;
280 }
281
282 let dur = Duration::whole_filling_beats(sounding);
283 let emitted = dur.beats(0);
284 let mut note = Note::new(pitches[0].clone(), dur);
285 note.pitches = pitches;
286 voice0.push(note);
287 cursor += emitted;
288 }
289 if total_beats - cursor > 1.0 / GRID {
290 fill_rests(&mut voice0, total_beats - cursor);
291 }
292 if voice0.is_empty() {
293 fill_rests(&mut voice0, total_beats);
294 }
295
296 let mut measure = Measure::empty(current_ts.numerator, current_ts.denominator);
297 measure.number = mi as u32 + 1;
298 measure.time_sig = template.as_ref().and_then(|m| m.time_sig.clone());
299 measure.key_sig = template.as_ref().and_then(|m| m.key_sig.clone());
300 measure.tempo = template.as_ref().and_then(|m| m.tempo);
301 measure.barline_left = template
302 .as_ref()
303 .map(|m| m.barline_left.clone())
304 .unwrap_or(Barline::Normal);
305 measure.barline_right = template
306 .as_ref()
307 .map(|m| m.barline_right.clone())
308 .unwrap_or(Barline::Normal);
309 measure.voices[0] = voice0;
310 measures.push(measure);
311 }
312
313 Staff {
314 clef,
315 measures,
316 transpose_semitones: 0,
317 tablature: None,
318 }
319}
320
321fn template_measure_for_parts(score: &Score, part_indices: &[usize], mi: usize) -> Option<Measure> {
322 part_indices
323 .iter()
324 .filter_map(|&pi| score.parts[pi].staves.first())
325 .find_map(|s| s.measures.get(mi).cloned())
326}
327
328fn octave_fit(score: &Score) -> (Score, i8) {
333 let (lo, hi) = instrument_range(ACCORDION_PROGRAM);
334 let target_mid = (lo as f64 + hi as f64) / 2.0;
335 let Some(mp) = score.parts.first().and_then(mean_pitch) else {
336 return (score.clone(), 0);
337 };
338
339 let shift = [-24i8, -12, 0, 12, 24]
340 .into_iter()
341 .min_by(|&a, &b| {
342 let da = (mp + a as f64 - target_mid).abs();
343 let db = (mp + b as f64 - target_mid).abs();
344 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
345 })
346 .unwrap_or(0);
347
348 if shift == 0 {
349 (score.clone(), 0)
350 } else {
351 (super::score::transpose(score, shift), shift)
352 }
353}
354
355pub fn arrange_for_accordion(
363 score: &Score,
364 right_hand_part_index: Option<usize>,
365) -> Result<ArrangeResult, Error> {
366 let analysis = analyze_for_accordion(score);
367 if analysis.candidates.is_empty() {
368 return Err(Error::InvalidCommand(
369 "no pitched, non-percussion part to arrange".to_string(),
370 ));
371 }
372
373 let treble_index = match right_hand_part_index {
374 Some(i) => {
375 if i >= score.parts.len() || is_percussion_part(&score.parts[i]) {
376 return Err(Error::PartNotFound(i));
377 }
378 i
379 }
380 None => analysis.candidates[0].part_index,
381 };
382 let bass_indices: Vec<usize> = analysis
383 .candidates
384 .iter()
385 .map(|c| c.part_index)
386 .filter(|&i| i != treble_index)
387 .collect();
388
389 let mut notes = Vec::new();
390 let measure_count = analysis
391 .candidates
392 .iter()
393 .map(|c| {
394 score.parts[c.part_index]
395 .staves
396 .iter()
397 .map(|s| s.measures.len())
398 .max()
399 .unwrap_or(0)
400 })
401 .max()
402 .unwrap_or(0);
403 let default_ts = score.settings.time_signature.clone();
404
405 let (treble_staff, bass_staff) = if !bass_indices.is_empty() {
406 let treble_template: Vec<Option<Measure>> = (0..measure_count)
407 .map(|mi| template_measure_for_parts(score, &[treble_index], mi))
408 .collect();
409 let treble_events: Vec<Vec<SourceEvent>> = (0..measure_count)
410 .map(|mi| events_from_parts(score, &[treble_index], mi))
411 .collect();
412 let bass_template: Vec<Option<Measure>> = (0..measure_count)
413 .map(|mi| template_measure_for_parts(score, &bass_indices, mi))
414 .collect();
415 let bass_events: Vec<Vec<SourceEvent>> = (0..measure_count)
416 .map(|mi| events_from_parts(score, &bass_indices, mi))
417 .collect();
418 notes.push(format!(
419 "右手(高音部): {} / 左手(低音部): {}パートを統合",
420 analysis
421 .candidates
422 .iter()
423 .find(|c| c.part_index == treble_index)
424 .map(|c| c.name.as_str())
425 .unwrap_or(""),
426 bass_indices.len()
427 ));
428 (
429 assemble_staff(
430 Clef::Treble,
431 treble_template,
432 treble_events,
433 default_ts.clone(),
434 ),
435 assemble_staff(Clef::Bass, bass_template, bass_events, default_ts),
436 )
437 } else {
438 let template: Vec<Option<Measure>> = (0..measure_count)
439 .map(|mi| template_measure_for_parts(score, &[treble_index], mi))
440 .collect();
441 let treble_events: Vec<Vec<SourceEvent>> = (0..measure_count)
442 .map(|mi| events_from_pitch_split(score, treble_index, mi, true))
443 .collect();
444 let bass_events: Vec<Vec<SourceEvent>> = (0..measure_count)
445 .map(|mi| events_from_pitch_split(score, treble_index, mi, false))
446 .collect();
447 notes.push("単一パートのため中央ハ(MIDI 60)を基準に上下2段へ分割".to_string());
448 (
449 assemble_staff(
450 Clef::Treble,
451 template.clone(),
452 treble_events,
453 default_ts.clone(),
454 ),
455 assemble_staff(Clef::Bass, template, bass_events, default_ts),
456 )
457 };
458
459 let mut accordion_part = Part::new("Accordion", "Acc.");
460 accordion_part.midi_program = ACCORDION_PROGRAM;
461 accordion_part.staves = vec![treble_staff, bass_staff];
462
463 let mut merged = Score {
464 id: uuid::Uuid::new_v4().to_string(),
465 schema_version: 1,
466 metadata: score.metadata.clone(),
467 settings: score.settings.clone(),
468 parts: vec![accordion_part],
469 part_groups: Vec::new(),
470 texts: score.texts.clone(),
471 chord_definitions: score.chord_definitions.clone(),
472 };
473
474 let (fitted, shift) = octave_fit(&merged);
475 merged = fitted;
476 if shift != 0 {
477 notes.push(format!(
478 "アコーディオンの実用音域に合わせて{}オクターブ移調",
479 shift / 12
480 ));
481 }
482
483 super::score::respell_score_to_key(&mut merged);
484
485 if analysis.ambiguous && right_hand_part_index.is_none() {
486 notes.push("上位2パートの平均音高が僅差のため、右手パートの選択が曖昧です".to_string());
487 }
488
489 Ok(ArrangeResult {
490 score: merged,
491 notes,
492 })
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use crate::model::{pitch::Step, score::Score, validate::validate};
499
500 fn note(step: Step, octave: i8, duration: Duration) -> Note {
501 Note::new(Pitch::new(step, octave), duration)
502 }
503
504 #[test]
505 fn analyze_ranks_by_mean_pitch_descending() {
506 let mut score = Score::new("T", 120, 4, 4, 0, 1);
507 score.parts[0].name = "Low".to_string();
508 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
509
510 let mut high = score.parts[0].clone();
511 high.name = "High".to_string();
512 high.staves[0].measures[0].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
513 score.parts.push(high);
514
515 let analysis = analyze_for_accordion(&score);
516 assert_eq!(analysis.candidates.len(), 2);
517 assert_eq!(analysis.candidates[0].name, "High");
518 assert_eq!(analysis.candidates[1].name, "Low");
519 }
520
521 #[test]
522 fn analyze_excludes_percussion_channel() {
523 let mut score = Score::new("T", 120, 4, 4, 0, 1);
524 score.parts[0].midi_channel = 9;
525 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 4, Duration::Whole)];
526 assert!(analyze_for_accordion(&score).candidates.is_empty());
527 }
528
529 #[test]
530 fn analyze_excludes_silent_part() {
531 let score = Score::new("T", 120, 4, 4, 0, 1);
534 assert!(analyze_for_accordion(&score).candidates.is_empty());
535 }
536
537 #[test]
538 fn arrange_no_candidates_errors() {
539 let score = Score::new("T", 120, 4, 4, 0, 1);
540 assert!(arrange_for_accordion(&score, None).is_err());
541 }
542
543 #[test]
544 fn arrange_rejects_out_of_range_part_index() {
545 let mut score = Score::new("T", 120, 4, 4, 0, 1);
546 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 4, Duration::Whole)];
547 let result = arrange_for_accordion(&score, Some(99));
548 assert!(matches!(result, Err(Error::PartNotFound(99))));
549 }
550
551 #[test]
552 fn arrange_single_part_splits_chord_and_sets_accordion_program() {
553 let mut score = Score::new("T", 120, 4, 4, 0, 1);
554 let mut chord = note(Step::C, 5, Duration::Whole);
555 chord.pitches.push(Pitch::new(Step::C, 3));
556 score.parts[0].staves[0].measures[0].voices[0] = vec![chord];
557
558 let result = arrange_for_accordion(&score, None).unwrap();
559 assert_eq!(result.score.parts.len(), 1);
560 assert_eq!(result.score.parts[0].midi_program, ACCORDION_PROGRAM);
561 assert_eq!(result.score.parts[0].staves.len(), 2);
562 assert_eq!(result.score.parts[0].staves[0].clef, Clef::Treble);
563 assert_eq!(result.score.parts[0].staves[1].clef, Clef::Bass);
564 }
565
566 #[test]
567 fn arrange_two_parts_puts_higher_mean_pitch_on_treble() {
568 let mut score = Score::new("T", 120, 4, 4, 0, 2);
569 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
570 score.parts[0].staves[0].measures[1].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
571
572 let mut melody = score.parts[0].clone();
573 melody.name = "Melody".to_string();
574 melody.staves[0].measures[0].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
575 melody.staves[0].measures[1].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
576 score.parts.push(melody);
577
578 let result = arrange_for_accordion(&score, None).unwrap();
579 assert_eq!(result.score.parts[0].midi_program, ACCORDION_PROGRAM);
580 let treble_note = &result.score.parts[0].staves[0].measures[0].voices[0][0];
581 assert!(!treble_note.is_rest);
582 assert_eq!(treble_note.pitches[0].octave, 5);
583 }
584
585 #[test]
586 fn arrange_right_hand_override_picks_requested_part() {
587 let mut score = Score::new("T", 120, 4, 4, 0, 1);
588 score.parts[0].name = "Low".to_string();
589 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
590
591 let mut high = score.parts[0].clone();
592 high.name = "High".to_string();
593 high.staves[0].measures[0].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
594 score.parts.push(high);
595
596 let result = arrange_for_accordion(&score, Some(0)).unwrap();
598 let treble_note = &result.score.parts[0].staves[0].measures[0].voices[0][0];
599 assert!(!treble_note.is_rest);
600 assert_eq!(treble_note.pitches[0].octave, 3);
601 }
602
603 #[test]
604 fn arranged_measures_pass_beat_count_validation() {
605 let mut score = Score::new("T", 120, 4, 4, 0, 2);
609 score.parts[0].staves[0].measures[0].voices[0] = vec![
610 note(Step::C, 3, Duration::Half),
611 note(Step::E, 3, Duration::Half),
612 ];
613 score.parts[0].staves[0].measures[1].voices[0] = vec![note(Step::G, 3, Duration::Whole)];
614
615 let mut melody = score.parts[0].clone();
616 melody.staves[0].measures[0].voices[0] = vec![
617 note(Step::C, 5, Duration::Quarter),
618 note(Step::D, 5, Duration::Quarter),
619 note(Step::E, 5, Duration::Quarter),
620 note(Step::F, 5, Duration::Quarter),
621 ];
622 melody.staves[0].measures[1].voices[0] = vec![note(Step::G, 5, Duration::Whole)];
623 score.parts.push(melody);
624
625 let result = arrange_for_accordion(&score, None).unwrap();
626 let report = validate(&result.score);
627 assert!(
628 report.errors.is_empty(),
629 "expected no beat-count errors, got {:?}",
630 report.errors
631 );
632 }
633}