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 }
318}
319
320fn template_measure_for_parts(score: &Score, part_indices: &[usize], mi: usize) -> Option<Measure> {
321 part_indices
322 .iter()
323 .filter_map(|&pi| score.parts[pi].staves.first())
324 .find_map(|s| s.measures.get(mi).cloned())
325}
326
327fn octave_fit(score: &Score) -> (Score, i8) {
332 let (lo, hi) = instrument_range(ACCORDION_PROGRAM);
333 let target_mid = (lo as f64 + hi as f64) / 2.0;
334 let Some(mp) = score.parts.first().and_then(mean_pitch) else {
335 return (score.clone(), 0);
336 };
337
338 let shift = [-24i8, -12, 0, 12, 24]
339 .into_iter()
340 .min_by(|&a, &b| {
341 let da = (mp + a as f64 - target_mid).abs();
342 let db = (mp + b as f64 - target_mid).abs();
343 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
344 })
345 .unwrap_or(0);
346
347 if shift == 0 {
348 (score.clone(), 0)
349 } else {
350 (super::score::transpose(score, shift), shift)
351 }
352}
353
354pub fn arrange_for_accordion(
362 score: &Score,
363 right_hand_part_index: Option<usize>,
364) -> Result<ArrangeResult, Error> {
365 let analysis = analyze_for_accordion(score);
366 if analysis.candidates.is_empty() {
367 return Err(Error::InvalidCommand(
368 "no pitched, non-percussion part to arrange".to_string(),
369 ));
370 }
371
372 let treble_index = match right_hand_part_index {
373 Some(i) => {
374 if i >= score.parts.len() || is_percussion_part(&score.parts[i]) {
375 return Err(Error::PartNotFound(i));
376 }
377 i
378 }
379 None => analysis.candidates[0].part_index,
380 };
381 let bass_indices: Vec<usize> = analysis
382 .candidates
383 .iter()
384 .map(|c| c.part_index)
385 .filter(|&i| i != treble_index)
386 .collect();
387
388 let mut notes = Vec::new();
389 let measure_count = analysis
390 .candidates
391 .iter()
392 .map(|c| {
393 score.parts[c.part_index]
394 .staves
395 .iter()
396 .map(|s| s.measures.len())
397 .max()
398 .unwrap_or(0)
399 })
400 .max()
401 .unwrap_or(0);
402 let default_ts = score.settings.time_signature.clone();
403
404 let (treble_staff, bass_staff) = if !bass_indices.is_empty() {
405 let treble_template: Vec<Option<Measure>> = (0..measure_count)
406 .map(|mi| template_measure_for_parts(score, &[treble_index], mi))
407 .collect();
408 let treble_events: Vec<Vec<SourceEvent>> = (0..measure_count)
409 .map(|mi| events_from_parts(score, &[treble_index], mi))
410 .collect();
411 let bass_template: Vec<Option<Measure>> = (0..measure_count)
412 .map(|mi| template_measure_for_parts(score, &bass_indices, mi))
413 .collect();
414 let bass_events: Vec<Vec<SourceEvent>> = (0..measure_count)
415 .map(|mi| events_from_parts(score, &bass_indices, mi))
416 .collect();
417 notes.push(format!(
418 "右手(高音部): {} / 左手(低音部): {}パートを統合",
419 analysis
420 .candidates
421 .iter()
422 .find(|c| c.part_index == treble_index)
423 .map(|c| c.name.as_str())
424 .unwrap_or(""),
425 bass_indices.len()
426 ));
427 (
428 assemble_staff(
429 Clef::Treble,
430 treble_template,
431 treble_events,
432 default_ts.clone(),
433 ),
434 assemble_staff(Clef::Bass, bass_template, bass_events, default_ts),
435 )
436 } else {
437 let template: Vec<Option<Measure>> = (0..measure_count)
438 .map(|mi| template_measure_for_parts(score, &[treble_index], mi))
439 .collect();
440 let treble_events: Vec<Vec<SourceEvent>> = (0..measure_count)
441 .map(|mi| events_from_pitch_split(score, treble_index, mi, true))
442 .collect();
443 let bass_events: Vec<Vec<SourceEvent>> = (0..measure_count)
444 .map(|mi| events_from_pitch_split(score, treble_index, mi, false))
445 .collect();
446 notes.push("単一パートのため中央ハ(MIDI 60)を基準に上下2段へ分割".to_string());
447 (
448 assemble_staff(
449 Clef::Treble,
450 template.clone(),
451 treble_events,
452 default_ts.clone(),
453 ),
454 assemble_staff(Clef::Bass, template, bass_events, default_ts),
455 )
456 };
457
458 let mut accordion_part = Part::new("Accordion", "Acc.");
459 accordion_part.midi_program = ACCORDION_PROGRAM;
460 accordion_part.staves = vec![treble_staff, bass_staff];
461
462 let mut merged = Score {
463 id: uuid::Uuid::new_v4().to_string(),
464 schema_version: 1,
465 metadata: score.metadata.clone(),
466 settings: score.settings.clone(),
467 parts: vec![accordion_part],
468 part_groups: Vec::new(),
469 texts: score.texts.clone(),
470 };
471
472 let (fitted, shift) = octave_fit(&merged);
473 merged = fitted;
474 if shift != 0 {
475 notes.push(format!(
476 "アコーディオンの実用音域に合わせて{}オクターブ移調",
477 shift / 12
478 ));
479 }
480
481 super::score::respell_score_to_key(&mut merged);
482
483 if analysis.ambiguous && right_hand_part_index.is_none() {
484 notes.push("上位2パートの平均音高が僅差のため、右手パートの選択が曖昧です".to_string());
485 }
486
487 Ok(ArrangeResult {
488 score: merged,
489 notes,
490 })
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496 use crate::model::{pitch::Step, score::Score, validate::validate};
497
498 fn note(step: Step, octave: i8, duration: Duration) -> Note {
499 Note::new(Pitch::new(step, octave), duration)
500 }
501
502 #[test]
503 fn analyze_ranks_by_mean_pitch_descending() {
504 let mut score = Score::new("T", 120, 4, 4, 0, 1);
505 score.parts[0].name = "Low".to_string();
506 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
507
508 let mut high = score.parts[0].clone();
509 high.name = "High".to_string();
510 high.staves[0].measures[0].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
511 score.parts.push(high);
512
513 let analysis = analyze_for_accordion(&score);
514 assert_eq!(analysis.candidates.len(), 2);
515 assert_eq!(analysis.candidates[0].name, "High");
516 assert_eq!(analysis.candidates[1].name, "Low");
517 }
518
519 #[test]
520 fn analyze_excludes_percussion_channel() {
521 let mut score = Score::new("T", 120, 4, 4, 0, 1);
522 score.parts[0].midi_channel = 9;
523 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 4, Duration::Whole)];
524 assert!(analyze_for_accordion(&score).candidates.is_empty());
525 }
526
527 #[test]
528 fn analyze_excludes_silent_part() {
529 let score = Score::new("T", 120, 4, 4, 0, 1);
532 assert!(analyze_for_accordion(&score).candidates.is_empty());
533 }
534
535 #[test]
536 fn arrange_no_candidates_errors() {
537 let score = Score::new("T", 120, 4, 4, 0, 1);
538 assert!(arrange_for_accordion(&score, None).is_err());
539 }
540
541 #[test]
542 fn arrange_rejects_out_of_range_part_index() {
543 let mut score = Score::new("T", 120, 4, 4, 0, 1);
544 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 4, Duration::Whole)];
545 let result = arrange_for_accordion(&score, Some(99));
546 assert!(matches!(result, Err(Error::PartNotFound(99))));
547 }
548
549 #[test]
550 fn arrange_single_part_splits_chord_and_sets_accordion_program() {
551 let mut score = Score::new("T", 120, 4, 4, 0, 1);
552 let mut chord = note(Step::C, 5, Duration::Whole);
553 chord.pitches.push(Pitch::new(Step::C, 3));
554 score.parts[0].staves[0].measures[0].voices[0] = vec![chord];
555
556 let result = arrange_for_accordion(&score, None).unwrap();
557 assert_eq!(result.score.parts.len(), 1);
558 assert_eq!(result.score.parts[0].midi_program, ACCORDION_PROGRAM);
559 assert_eq!(result.score.parts[0].staves.len(), 2);
560 assert_eq!(result.score.parts[0].staves[0].clef, Clef::Treble);
561 assert_eq!(result.score.parts[0].staves[1].clef, Clef::Bass);
562 }
563
564 #[test]
565 fn arrange_two_parts_puts_higher_mean_pitch_on_treble() {
566 let mut score = Score::new("T", 120, 4, 4, 0, 2);
567 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
568 score.parts[0].staves[0].measures[1].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
569
570 let mut melody = score.parts[0].clone();
571 melody.name = "Melody".to_string();
572 melody.staves[0].measures[0].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
573 melody.staves[0].measures[1].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
574 score.parts.push(melody);
575
576 let result = arrange_for_accordion(&score, None).unwrap();
577 assert_eq!(result.score.parts[0].midi_program, ACCORDION_PROGRAM);
578 let treble_note = &result.score.parts[0].staves[0].measures[0].voices[0][0];
579 assert!(!treble_note.is_rest);
580 assert_eq!(treble_note.pitches[0].octave, 5);
581 }
582
583 #[test]
584 fn arrange_right_hand_override_picks_requested_part() {
585 let mut score = Score::new("T", 120, 4, 4, 0, 1);
586 score.parts[0].name = "Low".to_string();
587 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
588
589 let mut high = score.parts[0].clone();
590 high.name = "High".to_string();
591 high.staves[0].measures[0].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
592 score.parts.push(high);
593
594 let result = arrange_for_accordion(&score, Some(0)).unwrap();
596 let treble_note = &result.score.parts[0].staves[0].measures[0].voices[0][0];
597 assert!(!treble_note.is_rest);
598 assert_eq!(treble_note.pitches[0].octave, 3);
599 }
600
601 #[test]
602 fn arranged_measures_pass_beat_count_validation() {
603 let mut score = Score::new("T", 120, 4, 4, 0, 2);
607 score.parts[0].staves[0].measures[0].voices[0] = vec![
608 note(Step::C, 3, Duration::Half),
609 note(Step::E, 3, Duration::Half),
610 ];
611 score.parts[0].staves[0].measures[1].voices[0] = vec![note(Step::G, 3, Duration::Whole)];
612
613 let mut melody = score.parts[0].clone();
614 melody.staves[0].measures[0].voices[0] = vec![
615 note(Step::C, 5, Duration::Quarter),
616 note(Step::D, 5, Duration::Quarter),
617 note(Step::E, 5, Duration::Quarter),
618 note(Step::F, 5, Duration::Quarter),
619 ];
620 melody.staves[0].measures[1].voices[0] = vec![note(Step::G, 5, Duration::Whole)];
621 score.parts.push(melody);
622
623 let result = arrange_for_accordion(&score, None).unwrap();
624 let report = validate(&result.score);
625 assert!(
626 report.errors.is_empty(),
627 "expected no beat-count errors, got {:?}",
628 report.errors
629 );
630 }
631}