use rivet_core::*; fn rhythm(p: &Phrase) -> Vec<(u32, u32)> { p.notes.iter().map(|n| (n.start, n.duration)).collect() } fn options(motif: &Phrase, role: Role, s: &Settings) -> Vec { realize(motif, role, 2, 2 * BAR, false, 0, s, &Harmony::default()).unwrap() } #[test] fn each_role_offers_distinct_reproducible_options_led_by_its_best_fit() { let motif = demo_motif(); let s = Settings::default(); for (role, first) in [ (Role::Statement, Strategy::Quote), (Role::Answer, Strategy::CallResponse), (Role::Departure, Strategy::LaterPeak), (Role::Return, Strategy::HarmonicReturn), ] { let a = options(&motif, role, &s); assert_eq!(a, options(&motif, role, &s), "{role:?} is deterministic"); assert_eq!(a.len(), 3, "{role:?}"); assert_eq!(a[0].strategy, first, "{role:?}"); for pair in a.windows(2) { assert_ne!(pair[0].phrase, pair[1].phrase); } for c in &a { c.phrase.validate(MAX_PLAYBACK_NOTES).unwrap(); assert_eq!(c.phrase.length, 2 * BAR); } } let quote = &options(&motif, Role::Statement, &s)[0]; assert_eq!( quote.phrase.notes, motif.notes, "a statement quotes the motif" ); let rest = realize(&motif, Role::Rest, 1, 0, false, 0, &s, &Harmony::default()).unwrap(); assert_eq!(rest.len(), 1); assert!(rest[0].phrase.notes.is_empty()); } #[test] fn options_change_rhythm_and_density_not_only_pitch() { let motif = demo_motif(); let s = Settings::default(); let mut signatures = std::collections::BTreeSet::new(); for seed in 0..12 { for role in [Role::Answer, Role::Departure] { for c in options(&motif, role, &Settings { seed, ..s.clone() }) { signatures.insert(rhythm(&c.phrase)); } } } assert!( signatures.len() > 3, "seeds should explore rhythmic decisions" ); assert!(options(&motif, Role::Answer, &s) .iter() .any(|c| c.phrase.notes.len() != motif.notes.len())); } #[test] fn fixed_groove_and_separate_note_locks_take_priority() { let mut motif = demo_motif(); motif.notes[1].pitch_locked = true; motif.notes[1].pitch = 99; motif.notes[3].rhythm_locked = true; for role in [Role::Answer, Role::Departure, Role::Return] { for c in realize( &motif, role, 4, 0, false, 0, &Settings::default(), &Harmony::default(), ) .unwrap() { let locked: Vec<_> = c.phrase.notes.iter().filter(|n| n.pitch_locked).collect(); assert_eq!(locked.len(), 2, "{role:?} {:?}", c.strategy); assert!(locked.iter().all(|n| n.pitch == 99)); for rep in 0..2 { assert!(c.phrase.notes.iter().any(|n| n.rhythm_locked && n.start == motif.notes[3].start + rep * motif.length && n.duration == motif.notes[3].duration)); } } } let motif = demo_motif(); let s = Settings { preserve_rhythm: true, ..Settings::default() }; for c in options(&motif, Role::Departure, &s) { assert_eq!(rhythm(&c.phrase), rhythm(&motif)); } } #[test] fn fully_locked_motif_returns_one_honest_option() { let mut motif = demo_motif(); for n in &mut motif.notes { n.locked = true; } let all = options(&motif, Role::Answer, &Settings::default()); assert_eq!(all.len(), 1); assert_eq!(all[0].phrase.notes, motif.notes); } #[test] fn harmony_drives_strong_notes_and_the_final_ending() { let harmony = Harmony { length: BAR, spans: vec![HarmonySpan { start: 0, duration: BAR, tones: ChordQuality::Major.mask(0), bass: 48, }], }; for ending in [Ending::Open, Ending::Resolve] { let s = Settings { preserve_contour: false, ending, ..Settings::default() }; for c in realize( &demo_motif(), Role::Return, 2, 6 * BAR, true, 0, &s, &harmony, ) .unwrap() { if c.strategy == Strategy::Quote { continue; } let last = c.phrase.notes.last().unwrap(); let chord = ChordQuality::Major.mask(0) & (1 << (last.pitch % 12)) != 0; assert_eq!(chord, ending == Ending::Resolve, "{:?}", c.strategy); for n in c .phrase .notes .iter() .filter(|n| n.start.is_multiple_of(PPQ) && n.start != last.start) { assert_ne!( harmony.at(n.start).unwrap().tones & (1 << (n.pitch % 12)), 0 ); } } } } #[test] fn imported_harmony_preserves_voicings_and_gaps_without_guessing_roots() { let notes: Vec<_> = [64, 67, 72] .into_iter() .map(|pitch| Note { start: 0, duration: BAR, pitch, velocity: 90, ..Default::default() }) .collect(); let h = Harmony::from_midi(¬es, 2 * BAR).unwrap(); assert_eq!(h.spans.len(), 1); assert_eq!(h.spans[0].bass, 64); assert_eq!(h.spans[0].tones, ChordQuality::Major.mask(0)); assert!(h.at(BAR + 1).is_none()); assert_eq!(h.at(2 * BAR).unwrap().bass, 64); } #[test] fn every_meter_key_and_block_length_realizes_valid_options() { let motif = demo_motif(); for meter in Meter::ALL { for root in [0, 5, 11] { for scale in Scale::ALL { let s = Settings { root, scale, meter, low: 60, high: 72, ..Settings::default() }; for bars in [1, 3, 8] { for role in Role::ALL { for c in realize(&motif, role, bars, 0, true, 0, &s, &Harmony::default()) .unwrap() { c.phrase.check(MAX_PLAYBACK_NOTES, true).unwrap(); assert_eq!(c.phrase.length, bars as u32 * meter.bar()); } } } } } } }