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