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| (sum + p.to_midi() as i64, count + 1));
66 if count == 0 { None } else { Some(sum as f64 / count as f64) }
67}
68
69pub fn analyze_for_accordion(score: &Score) -> AccordionAnalysis {
72 let mut candidates: Vec<PartCandidate> = score
73 .parts
74 .iter()
75 .enumerate()
76 .filter(|(_, p)| !is_percussion_part(p))
77 .filter_map(|(i, p)| mean_pitch(p).map(|mp| PartCandidate { part_index: i, name: p.name.clone(), mean_pitch: mp }))
78 .collect();
79 candidates.sort_by(|a, b| b.mean_pitch.partial_cmp(&a.mean_pitch).unwrap_or(std::cmp::Ordering::Equal));
80 let ambiguous = candidates.len() >= 2 && (candidates[0].mean_pitch - candidates[1].mean_pitch).abs() < 3.0;
81 AccordionAnalysis { candidates, ambiguous }
82}
83
84struct SourceEvent {
85 onset_ticks: i64,
86 beats: f64,
87 pitches: Vec<Pitch>,
88}
89
90fn events_from_parts(score: &Score, part_indices: &[usize], measure_idx: usize) -> Vec<SourceEvent> {
91 let mut events = Vec::new();
92 for &pi in part_indices {
93 let part = &score.parts[pi];
94 for staff in &part.staves {
95 let Some(measure) = staff.measures.get(measure_idx) else { continue };
96 if measure.multi_rest_count.is_some() { continue }
97 for voice in &measure.voices {
98 let mut onset = 0.0f64;
99 for note in voice {
100 let b = note.beats();
101 if !note.is_rest && !note.is_grace && !note.pitches.is_empty() {
102 let pitches = note
103 .pitches
104 .iter()
105 .map(|p| {
106 let midi = (p.to_midi() + staff.transpose_semitones as i16).clamp(0, 127) as u8;
107 Pitch::from_midi(midi, false)
108 })
109 .collect();
110 events.push(SourceEvent { onset_ticks: (onset * GRID).round() as i64, beats: b, pitches });
111 }
112 onset += b;
113 }
114 }
115 }
116 }
117 events
118}
119
120fn events_from_pitch_split(score: &Score, part_index: usize, measure_idx: usize, high: bool) -> Vec<SourceEvent> {
124 let part = &score.parts[part_index];
125 let mut events = Vec::new();
126 for staff in &part.staves {
127 let Some(measure) = staff.measures.get(measure_idx) else { continue };
128 if measure.multi_rest_count.is_some() { continue }
129 for voice in &measure.voices {
130 let mut onset = 0.0f64;
131 for note in voice {
132 let b = note.beats();
133 if !note.is_rest && !note.is_grace {
134 let pitches: Vec<Pitch> = note
135 .pitches
136 .iter()
137 .filter_map(|p| {
138 let midi = (p.to_midi() + staff.transpose_semitones as i16).clamp(0, 127) as u8;
139 let keep = if high { midi >= SPLIT_MIDI } else { midi < SPLIT_MIDI };
140 if keep { Some(Pitch::from_midi(midi, false)) } else { None }
141 })
142 .collect();
143 if !pitches.is_empty() {
144 events.push(SourceEvent { onset_ticks: (onset * GRID).round() as i64, beats: b, pitches });
145 }
146 }
147 onset += b;
148 }
149 }
150 }
151 events
152}
153
154fn fill_rests(notes: &mut Vec<Note>, mut remaining: f64) {
155 while remaining > 1.0 / GRID {
156 let dur = Duration::whole_filling_beats(remaining);
157 let filled = dur.beats(0);
158 notes.push(Note::rest(dur));
159 remaining -= filled;
160 }
161}
162
163fn assemble_staff(clef: Clef, template_per_measure: Vec<Option<Measure>>, events_per_measure: Vec<Vec<SourceEvent>>, default_ts: super::notation::TimeSignature) -> Staff {
167 let mut current_ts = default_ts;
168 let mut measures = Vec::with_capacity(template_per_measure.len());
169
170 for (mi, (template, events)) in template_per_measure.into_iter().zip(events_per_measure).enumerate() {
171 if let Some(ts) = template.as_ref().and_then(|m| m.time_sig.as_ref()) {
172 current_ts = ts.clone();
173 }
174 let total_beats = current_ts.total_beats();
175
176 let mut buckets: BTreeMap<i64, Vec<SourceEvent>> = BTreeMap::new();
177 for ev in events {
178 buckets.entry(ev.onset_ticks).or_default().push(ev);
179 }
180 let keys: Vec<i64> = buckets.keys().copied().collect();
181
182 let mut voice0: Vec<Note> = Vec::new();
183 let mut cursor = 0.0f64;
184 for (idx, &key) in keys.iter().enumerate() {
185 let onset = key as f64 / GRID;
186 if onset < cursor - 1.0 / GRID { continue } if onset > cursor {
188 fill_rests(&mut voice0, onset - cursor);
189 cursor = onset;
190 }
191
192 let next_onset = keys.get(idx + 1).map(|&k| k as f64 / GRID).unwrap_or(total_beats);
193 let group = &buckets[&key];
194 let shortest = group.iter().map(|e| e.beats).fold(f64::MAX, f64::min);
195 let cap = (next_onset - onset).max(1.0 / GRID);
196 let sounding = shortest.min(cap).min((total_beats - onset).max(1.0 / GRID));
197
198 let mut pitches: Vec<Pitch> = Vec::new();
199 let mut seen_midi: Vec<u8> = Vec::new();
200 for ev in group {
201 for p in &ev.pitches {
202 let midi = p.to_midi().clamp(0, 127) as u8;
203 if !seen_midi.contains(&midi) {
204 seen_midi.push(midi);
205 pitches.push(p.clone());
206 }
207 }
208 }
209 if pitches.is_empty() { continue }
210
211 let dur = Duration::whole_filling_beats(sounding);
212 let emitted = dur.beats(0);
213 let mut note = Note::new(pitches[0].clone(), dur);
214 note.pitches = pitches;
215 voice0.push(note);
216 cursor += emitted;
217 }
218 if total_beats - cursor > 1.0 / GRID {
219 fill_rests(&mut voice0, total_beats - cursor);
220 }
221 if voice0.is_empty() {
222 fill_rests(&mut voice0, total_beats);
223 }
224
225 let mut measure = Measure::empty(current_ts.numerator, current_ts.denominator);
226 measure.number = mi as u32 + 1;
227 measure.time_sig = template.as_ref().and_then(|m| m.time_sig.clone());
228 measure.key_sig = template.as_ref().and_then(|m| m.key_sig.clone());
229 measure.tempo = template.as_ref().and_then(|m| m.tempo);
230 measure.barline_left = template.as_ref().map(|m| m.barline_left.clone()).unwrap_or(Barline::Normal);
231 measure.barline_right = template.as_ref().map(|m| m.barline_right.clone()).unwrap_or(Barline::Normal);
232 measure.voices[0] = voice0;
233 measures.push(measure);
234 }
235
236 Staff { clef, measures, transpose_semitones: 0 }
237}
238
239fn template_measure_for_parts(score: &Score, part_indices: &[usize], mi: usize) -> Option<Measure> {
240 part_indices.iter().filter_map(|&pi| score.parts[pi].staves.first()).find_map(|s| s.measures.get(mi).cloned())
241}
242
243fn octave_fit(score: &Score) -> (Score, i8) {
248 let (lo, hi) = instrument_range(ACCORDION_PROGRAM);
249 let target_mid = (lo as f64 + hi as f64) / 2.0;
250 let Some(mp) = score.parts.first().and_then(mean_pitch) else { return (score.clone(), 0) };
251
252 let shift = [-24i8, -12, 0, 12, 24]
253 .into_iter()
254 .min_by(|&a, &b| {
255 let da = (mp + a as f64 - target_mid).abs();
256 let db = (mp + b as f64 - target_mid).abs();
257 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
258 })
259 .unwrap_or(0);
260
261 if shift == 0 { (score.clone(), 0) } else { (super::score::transpose(score, shift), shift) }
262}
263
264pub fn arrange_for_accordion(score: &Score, right_hand_part_index: Option<usize>) -> Result<ArrangeResult, Error> {
272 let analysis = analyze_for_accordion(score);
273 if analysis.candidates.is_empty() {
274 return Err(Error::InvalidCommand("no pitched, non-percussion part to arrange".to_string()));
275 }
276
277 let treble_index = match right_hand_part_index {
278 Some(i) => {
279 if i >= score.parts.len() || is_percussion_part(&score.parts[i]) {
280 return Err(Error::PartNotFound(i));
281 }
282 i
283 }
284 None => analysis.candidates[0].part_index,
285 };
286 let bass_indices: Vec<usize> = analysis.candidates.iter().map(|c| c.part_index).filter(|&i| i != treble_index).collect();
287
288 let mut notes = Vec::new();
289 let measure_count = analysis
290 .candidates
291 .iter()
292 .map(|c| score.parts[c.part_index].staves.iter().map(|s| s.measures.len()).max().unwrap_or(0))
293 .max()
294 .unwrap_or(0);
295 let default_ts = score.settings.time_signature.clone();
296
297 let (treble_staff, bass_staff) = if !bass_indices.is_empty() {
298 let treble_template: Vec<Option<Measure>> = (0..measure_count).map(|mi| template_measure_for_parts(score, &[treble_index], mi)).collect();
299 let treble_events: Vec<Vec<SourceEvent>> = (0..measure_count).map(|mi| events_from_parts(score, &[treble_index], mi)).collect();
300 let bass_template: Vec<Option<Measure>> = (0..measure_count).map(|mi| template_measure_for_parts(score, &bass_indices, mi)).collect();
301 let bass_events: Vec<Vec<SourceEvent>> = (0..measure_count).map(|mi| events_from_parts(score, &bass_indices, mi)).collect();
302 notes.push(format!(
303 "右手(高音部): {} / 左手(低音部): {}パートを統合",
304 analysis.candidates.iter().find(|c| c.part_index == treble_index).map(|c| c.name.as_str()).unwrap_or(""),
305 bass_indices.len()
306 ));
307 (
308 assemble_staff(Clef::Treble, treble_template, treble_events, default_ts.clone()),
309 assemble_staff(Clef::Bass, bass_template, bass_events, default_ts),
310 )
311 } else {
312 let template: Vec<Option<Measure>> = (0..measure_count).map(|mi| template_measure_for_parts(score, &[treble_index], mi)).collect();
313 let treble_events: Vec<Vec<SourceEvent>> = (0..measure_count).map(|mi| events_from_pitch_split(score, treble_index, mi, true)).collect();
314 let bass_events: Vec<Vec<SourceEvent>> = (0..measure_count).map(|mi| events_from_pitch_split(score, treble_index, mi, false)).collect();
315 notes.push("単一パートのため中央ハ(MIDI 60)を基準に上下2段へ分割".to_string());
316 (
317 assemble_staff(Clef::Treble, template.clone(), treble_events, default_ts.clone()),
318 assemble_staff(Clef::Bass, template, bass_events, default_ts),
319 )
320 };
321
322 let mut accordion_part = Part::new("Accordion", "Acc.");
323 accordion_part.midi_program = ACCORDION_PROGRAM;
324 accordion_part.staves = vec![treble_staff, bass_staff];
325
326 let mut merged = Score {
327 id: uuid::Uuid::new_v4().to_string(),
328 schema_version: 1,
329 metadata: score.metadata.clone(),
330 settings: score.settings.clone(),
331 parts: vec![accordion_part],
332 part_groups: Vec::new(),
333 };
334
335 let (fitted, shift) = octave_fit(&merged);
336 merged = fitted;
337 if shift != 0 {
338 notes.push(format!("アコーディオンの実用音域に合わせて{}オクターブ移調", shift / 12));
339 }
340
341 super::score::respell_score_to_key(&mut merged);
342
343 if analysis.ambiguous && right_hand_part_index.is_none() {
344 notes.push("上位2パートの平均音高が僅差のため、右手パートの選択が曖昧です".to_string());
345 }
346
347 Ok(ArrangeResult { score: merged, notes })
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::model::{pitch::Step, score::Score, validate::validate};
354
355 fn note(step: Step, octave: i8, duration: Duration) -> Note {
356 Note::new(Pitch::new(step, octave), duration)
357 }
358
359 #[test]
360 fn analyze_ranks_by_mean_pitch_descending() {
361 let mut score = Score::new("T", 120, 4, 4, 0, 1);
362 score.parts[0].name = "Low".to_string();
363 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
364
365 let mut high = score.parts[0].clone();
366 high.name = "High".to_string();
367 high.staves[0].measures[0].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
368 score.parts.push(high);
369
370 let analysis = analyze_for_accordion(&score);
371 assert_eq!(analysis.candidates.len(), 2);
372 assert_eq!(analysis.candidates[0].name, "High");
373 assert_eq!(analysis.candidates[1].name, "Low");
374 }
375
376 #[test]
377 fn analyze_excludes_percussion_channel() {
378 let mut score = Score::new("T", 120, 4, 4, 0, 1);
379 score.parts[0].midi_channel = 9;
380 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 4, Duration::Whole)];
381 assert!(analyze_for_accordion(&score).candidates.is_empty());
382 }
383
384 #[test]
385 fn analyze_excludes_silent_part() {
386 let score = Score::new("T", 120, 4, 4, 0, 1);
389 assert!(analyze_for_accordion(&score).candidates.is_empty());
390 }
391
392 #[test]
393 fn arrange_no_candidates_errors() {
394 let score = Score::new("T", 120, 4, 4, 0, 1);
395 assert!(arrange_for_accordion(&score, None).is_err());
396 }
397
398 #[test]
399 fn arrange_rejects_out_of_range_part_index() {
400 let mut score = Score::new("T", 120, 4, 4, 0, 1);
401 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 4, Duration::Whole)];
402 let result = arrange_for_accordion(&score, Some(99));
403 assert!(matches!(result, Err(Error::PartNotFound(99))));
404 }
405
406 #[test]
407 fn arrange_single_part_splits_chord_and_sets_accordion_program() {
408 let mut score = Score::new("T", 120, 4, 4, 0, 1);
409 let mut chord = note(Step::C, 5, Duration::Whole);
410 chord.pitches.push(Pitch::new(Step::C, 3));
411 score.parts[0].staves[0].measures[0].voices[0] = vec![chord];
412
413 let result = arrange_for_accordion(&score, None).unwrap();
414 assert_eq!(result.score.parts.len(), 1);
415 assert_eq!(result.score.parts[0].midi_program, ACCORDION_PROGRAM);
416 assert_eq!(result.score.parts[0].staves.len(), 2);
417 assert_eq!(result.score.parts[0].staves[0].clef, Clef::Treble);
418 assert_eq!(result.score.parts[0].staves[1].clef, Clef::Bass);
419 }
420
421 #[test]
422 fn arrange_two_parts_puts_higher_mean_pitch_on_treble() {
423 let mut score = Score::new("T", 120, 4, 4, 0, 2);
424 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
425 score.parts[0].staves[0].measures[1].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
426
427 let mut melody = score.parts[0].clone();
428 melody.name = "Melody".to_string();
429 melody.staves[0].measures[0].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
430 melody.staves[0].measures[1].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
431 score.parts.push(melody);
432
433 let result = arrange_for_accordion(&score, None).unwrap();
434 assert_eq!(result.score.parts[0].midi_program, ACCORDION_PROGRAM);
435 let treble_note = &result.score.parts[0].staves[0].measures[0].voices[0][0];
436 assert!(!treble_note.is_rest);
437 assert_eq!(treble_note.pitches[0].octave, 5);
438 }
439
440 #[test]
441 fn arrange_right_hand_override_picks_requested_part() {
442 let mut score = Score::new("T", 120, 4, 4, 0, 1);
443 score.parts[0].name = "Low".to_string();
444 score.parts[0].staves[0].measures[0].voices[0] = vec![note(Step::C, 3, Duration::Whole)];
445
446 let mut high = score.parts[0].clone();
447 high.name = "High".to_string();
448 high.staves[0].measures[0].voices[0] = vec![note(Step::C, 5, Duration::Whole)];
449 score.parts.push(high);
450
451 let result = arrange_for_accordion(&score, Some(0)).unwrap();
453 let treble_note = &result.score.parts[0].staves[0].measures[0].voices[0][0];
454 assert!(!treble_note.is_rest);
455 assert_eq!(treble_note.pitches[0].octave, 3);
456 }
457
458 #[test]
459 fn arranged_measures_pass_beat_count_validation() {
460 let mut score = Score::new("T", 120, 4, 4, 0, 2);
464 score.parts[0].staves[0].measures[0].voices[0] =
465 vec![note(Step::C, 3, Duration::Half), note(Step::E, 3, Duration::Half)];
466 score.parts[0].staves[0].measures[1].voices[0] = vec![note(Step::G, 3, Duration::Whole)];
467
468 let mut melody = score.parts[0].clone();
469 melody.staves[0].measures[0].voices[0] = vec![
470 note(Step::C, 5, Duration::Quarter), note(Step::D, 5, Duration::Quarter),
471 note(Step::E, 5, Duration::Quarter), note(Step::F, 5, Duration::Quarter),
472 ];
473 melody.staves[0].measures[1].voices[0] = vec![note(Step::G, 5, Duration::Whole)];
474 score.parts.push(melody);
475
476 let result = arrange_for_accordion(&score, None).unwrap();
477 let report = validate(&result.score);
478 assert!(report.errors.is_empty(), "expected no beat-count errors, got {:?}", report.errors);
479 }
480}