use acorde_core::{
AccordionBassMode, AccordionMelodicVoice, AccordionPattern, AccordionPresetSelection,
AccordionSwingPolicy, Duration, LayeredAccordionOptions, Note, Pitch, PositionedAccordionChord,
Score, Step, arrange_accordion_preset, arrange_with_layers, to_playback_events,
};
fn input() -> (Score, AccordionPresetSelection) {
let mut score = Score::new("2/4", 60, 2, 4, 0, 1);
score.parts[0].staves[0].measures[0].voices[0] =
vec![Note::new(Pitch::new(Step::C, 5), Duration::Half)];
let chord =
serde_json::from_value(serde_json::json!({"root":"C", "kind":"major", "bass":null}))
.unwrap();
let selection = AccordionPresetSelection {
chords: vec![PositionedAccordionChord {
measure_index: 0,
offset_beats: 0.0,
chord,
}],
alternative_chords: None,
inversion: None,
bass_mode: AccordionBassMode::StandardBass,
};
(score, selection)
}
#[test]
fn full_bar_preset_patterns_have_expected_roles_and_playback() {
for (id, durations) in [
("fine-bass-chord", vec![0.5, 0.5, 0.5, 0.5]),
("habanera", vec![0.75, 0.25, 0.5, 0.5]),
] {
for bass_mode in [AccordionBassMode::StandardBass, AccordionBassMode::FreeBass] {
let (score, mut selection) = input();
selection.bass_mode = bass_mode;
let before = serde_json::to_value(&score).unwrap();
let result = arrange_accordion_preset(&score, selection, id);
assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics);
let output = result.score.unwrap();
assert_eq!(before, serde_json::to_value(&score).unwrap());
assert_eq!(
output.parts[0].staves[0].measures[0].voices[0][0].id,
score.parts[0].staves[0].measures[0].voices[0][0].id
);
let notes = &output.parts[0].staves[1].measures[0].voices[0];
assert_eq!(
notes.iter().map(|n| n.beats()).collect::<Vec<_>>(),
durations
);
assert_eq!(
notes
.iter()
.map(|n| n.pitches.iter().map(Pitch::to_midi).collect::<Vec<_>>())
.collect::<Vec<_>>(),
vec![vec![36], vec![60, 64, 67], vec![43], vec![60, 64, 67]]
);
let events = to_playback_events(&output, &Default::default());
let left = events
.iter()
.filter(|e| e.source.as_ref().is_some_and(|a| a.staff == 1))
.collect::<Vec<_>>();
assert_eq!(left.len(), 8);
assert_eq!(left[0].time_beats, 0.0);
assert!(
left.iter()
.any(|e| e.pitch_midi == 43 && e.time_beats == 1.0)
);
}
}
}
#[test]
fn pickup_and_chord_change_keep_phase_and_reject_unrepresentable_cases() {
for pattern in [AccordionPattern::FineBassChord, AccordionPattern::Habanera] {
let (mut score, selection) = input();
score.parts[0].staves[0].measures[0].actual_length =
acorde_core::MeasureLength::from_beats(1.0);
score.parts[0].staves[0].measures[0].voices[0][0].duration = Duration::Quarter;
let options = LayeredAccordionOptions {
chords: selection.chords.clone(),
alternative_chords: None,
inversion: None,
accompaniment: Some(pattern),
melodic_voice: AccordionMelodicVoice::Unchanged,
swing: AccordionSwingPolicy::Straight,
bass_mode: AccordionBassMode::StandardBass,
};
let result = arrange_with_layers(&score, options.clone());
let score = result.score.expect("pickup supported");
let notes = &score.parts[0].staves[1].measures[0].voices[0];
assert_eq!(
notes.iter().map(|n| n.beats()).collect::<Vec<_>>(),
vec![0.5, 0.5]
);
assert_eq!(notes[0].pitches[0].to_midi(), 43);
let (full, _) = input();
let mut repeated = options.clone();
let mut same = repeated.chords[0].clone();
same.offset_beats = 0.25;
repeated.chords.push(same);
let a = arrange_with_layers(&full, options.clone()).score.unwrap();
let b = arrange_with_layers(&full, repeated).score.unwrap();
let signature = |s: &Score| {
s.parts[0].staves[1].measures[0].voices[0]
.iter()
.map(|n| {
(
n.beats(),
n.pitches.iter().map(Pitch::to_midi).collect::<Vec<_>>(),
)
})
.collect::<Vec<_>>()
};
assert_eq!(signature(&a), signature(&b));
let mut changed = options.clone();
let mut chord = changed.chords[0].clone();
chord.offset_beats = 0.25;
chord.chord =
serde_json::from_value(serde_json::json!({"root":"G", "kind":"major", "bass":null}))
.unwrap();
changed.chords.push(chord);
let output = arrange_with_layers(&full, changed.clone()).score.unwrap();
let notes = &output.parts[0].staves[1].measures[0].voices[0];
assert_eq!(notes[0].beats(), 0.25);
assert_eq!(
notes[1].beats(),
if pattern == AccordionPattern::Habanera {
0.5
} else {
0.25
}
);
assert_eq!(notes[1].pitches[0].to_midi(), 43);
changed.chords[1].offset_beats = 0.125;
assert_eq!(
arrange_with_layers(&full, changed).diagnostics[0].code,
"unsupported-rhythm"
);
let wrong_meter = Score::new("3/4", 60, 3, 4, 0, 1);
assert_eq!(
arrange_with_layers(&wrong_meter, options).diagnostics[0].code,
"unsupported-meter"
);
}
}
#[test]
fn presets_and_layers_can_be_replayed_and_shuffled() {
use acorde_core::{
AccordionLayerShuffleOptions, AccordionShuffleOptions, arrange_accordion_layers,
arrange_from_resolved_layers, arrange_from_resolved_preset, shuffle_accordion_layers,
shuffle_accordion_preset,
};
let (score, selection) = input();
let events = |s: &Score| {
to_playback_events(s, &Default::default())
.into_iter()
.map(|e| (e.pitch_midi, e.time_beats, e.duration_beats))
.collect::<Vec<_>>()
};
for id in ["fine-bass-chord", "habanera"] {
let first = arrange_accordion_preset(&score, selection.clone(), id);
let replay = arrange_from_resolved_preset(&score, first.resolved_config.unwrap());
assert_eq!(
events(&first.score.unwrap()),
events(&replay.score.unwrap())
);
let options = LayeredAccordionOptions {
chords: selection.chords.clone(),
alternative_chords: None,
inversion: None,
accompaniment: Some(if id == "habanera" {
AccordionPattern::Habanera
} else {
AccordionPattern::FineBassChord
}),
melodic_voice: AccordionMelodicVoice::OctaveBelow,
swing: AccordionSwingPolicy::Straight,
bass_mode: AccordionBassMode::StandardBass,
};
let layer = arrange_accordion_layers(&score, options);
let replay = arrange_from_resolved_layers(&score, layer.resolved_config.unwrap());
assert_eq!(
events(&layer.score.unwrap()),
events(&replay.score.unwrap())
);
}
let preset_shuffle = AccordionShuffleOptions {
selection: selection.clone(),
alternative_selections: vec![],
allowed_preset_ids: vec!["fine-bass-chord".into(), "habanera".into()],
seed: "42".into(),
previous_config: None,
};
let a = shuffle_accordion_preset(&score, preset_shuffle.clone());
let a_again = shuffle_accordion_preset(&score, preset_shuffle);
assert!(a.score.is_some(), "{:?}", a.diagnostics);
assert_eq!(a.resolved_config, a_again.resolved_config);
assert_eq!(events(&a.score.unwrap()), events(&a_again.score.unwrap()));
let layer_shuffle = AccordionLayerShuffleOptions {
selection,
alternative_selections: vec![],
allowed_accompaniments: vec![
Some(AccordionPattern::FineBassChord),
Some(AccordionPattern::Habanera),
],
allowed_melodic_voices: vec![AccordionMelodicVoice::Unchanged],
allowed_swing_policies: vec![AccordionSwingPolicy::Straight],
seed: "42".into(),
previous_config: None,
};
let b = shuffle_accordion_layers(&score, layer_shuffle.clone());
let b_again = shuffle_accordion_layers(&score, layer_shuffle);
assert!(b.score.is_some(), "{:?}", b.diagnostics);
assert_eq!(b.resolved_config, b_again.resolved_config);
assert_eq!(events(&b.score.unwrap()), events(&b_again.score.unwrap()));
}
#[test]
fn short_final_bar_repeats_and_ties_keep_original_melody() {
let (mut score, selection) = input();
let original = score.parts[0].staves[0].measures[0].clone();
let mut second = original.clone();
second.number = 2;
second.voices[0][0] = Note::new(Pitch::new(Step::C, 5), Duration::Half);
second.voices[0][0].tie_end = true;
let mut final_measure = original.clone();
final_measure.number = 3;
final_measure.actual_length = acorde_core::MeasureLength::from_beats(1.0);
final_measure.voices[0][0] = Note::new(Pitch::new(Step::D, 5), Duration::Quarter);
score.parts[0].staves[0].measures[0].voices[0][0].tie_start = true;
score.parts[0].staves[0].measures[0].barline_left = acorde_core::Barline::RepeatStart;
second.barline_right = acorde_core::Barline::RepeatEnd;
score.parts[0].staves[0]
.measures
.extend([second, final_measure]);
let source_before = serde_json::to_value(&score).unwrap();
for id in ["fine-bass-chord", "habanera"] {
let result = arrange_accordion_preset(&score, selection.clone(), id);
assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics);
let output = result.score.unwrap();
assert_eq!(serde_json::to_value(&score).unwrap(), source_before);
assert_eq!(
serde_json::to_value(&output.parts[0].staves[0].measures).unwrap(),
serde_json::to_value(&score.parts[0].staves[0].measures).unwrap()
);
let melody_events = |score: &Score| {
to_playback_events(score, &Default::default())
.into_iter()
.filter(|event| event.source.as_ref().is_some_and(|addr| addr.staff == 0))
.map(|event| (event.pitch_midi, event.time_beats, event.duration_beats))
.collect::<Vec<_>>()
};
assert_eq!(melody_events(&output), melody_events(&score));
let left = &output.parts[0].staves[1].measures;
assert_eq!(
left[2].actual_length,
score.parts[0].staves[0].measures[2].actual_length
);
assert_eq!(left[2].voices[0].iter().map(Note::beats).sum::<f64>(), 1.0);
assert_eq!(left[1].barline_right, acorde_core::Barline::RepeatEnd);
}
}
#[test]
fn tuplets_in_melody_are_preserved() {
let (mut score, selection) = input();
let mut notes = Vec::new();
for (index, pitch) in [Step::C, Step::D, Step::E].into_iter().enumerate() {
let mut note = Note::new(Pitch::new(pitch, 5), Duration::Eighth);
note.tuplet = Some(acorde_core::TupletInfo {
actual_notes: 3,
normal_notes: 2,
});
note.tuplet_boundary = match index {
0 => Some(acorde_core::TupletBoundary::Start),
2 => Some(acorde_core::TupletBoundary::Stop),
_ => None,
};
notes.push(note);
}
notes.push(Note::new(Pitch::new(Step::G, 5), Duration::Quarter));
score.parts[0].staves[0].measures[0].voices[0] = notes;
let before = serde_json::to_value(&score.parts[0].staves[0]).unwrap();
for id in ["fine-bass-chord", "habanera"] {
let result = arrange_accordion_preset(&score, selection.clone(), id);
assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics);
assert_eq!(
serde_json::to_value(&result.score.unwrap().parts[0].staves[0]).unwrap(),
before
);
}
}