Check and Windows preview / check (patina) (push) Successful in 2m20s
Check and Windows preview / check (rivet) (push) Successful in 1m38s
Check and Windows preview / previews (push) Successful in 1m6s
Check and Windows preview / windows (push) Successful in 1m50s
Independent plugin release / publish (push) Successful in 8s
Independent plugin release / plan (push) Successful in 7s
Independent plugin release / build (push) Successful in 2m21s
229 lines
7.0 KiB
Rust
229 lines
7.0 KiB
Rust
use rivet_core::*;
|
|
|
|
fn rhythm(p: &Phrase) -> Vec<(u32, u32)> {
|
|
p.notes.iter().map(|n| (n.start, n.duration)).collect()
|
|
}
|
|
#[test]
|
|
fn alternatives_change_density_and_rhythm_instead_of_only_pitch() {
|
|
let source = demo_motif();
|
|
let s = Settings::default();
|
|
let candidates = generate(&source, &s).unwrap();
|
|
assert_eq!(candidates.len(), 3);
|
|
assert!(candidates
|
|
.iter()
|
|
.any(|c| c.phrase.notes.len() != source.notes.len() * 4));
|
|
assert!(candidates
|
|
.windows(2)
|
|
.any(|pair| rhythm(&pair[0].phrase) != rhythm(&pair[1].phrase)));
|
|
for c in &candidates {
|
|
assert_eq!(
|
|
&c.phrase.notes[..source.notes.len()],
|
|
source.notes.as_slice()
|
|
);
|
|
}
|
|
assert_eq!(candidates, generate(&source, &s).unwrap());
|
|
let mut signatures = std::collections::BTreeSet::new();
|
|
for seed in 0..12 {
|
|
for c in generate(&source, &Settings { seed, ..s.clone() }).unwrap() {
|
|
signatures.insert(rhythm(&c.phrase));
|
|
}
|
|
}
|
|
assert!(
|
|
signatures.len() > 3,
|
|
"seeds should explore multiple rhythmic decisions"
|
|
);
|
|
}
|
|
#[test]
|
|
fn fixed_groove_and_separate_note_locks_take_priority() {
|
|
let mut source = demo_motif();
|
|
source.notes[1].pitch_locked = true;
|
|
source.notes[1].pitch = 99;
|
|
source.notes[3].rhythm_locked = true;
|
|
for c in generate(&source, &Settings::default()).unwrap() {
|
|
let pitch: Vec<_> = c.phrase.notes.iter().filter(|n| n.pitch_locked).collect();
|
|
assert_eq!(pitch.len(), 4);
|
|
assert!(pitch.iter().all(|n| n.pitch == 99));
|
|
for rep in 0..4 {
|
|
assert!(c.phrase.notes.iter().any(|n| n.rhythm_locked
|
|
&& n.start == source.notes[3].start + rep * source.length
|
|
&& n.duration == source.notes[3].duration));
|
|
}
|
|
}
|
|
let source = demo_motif();
|
|
let repeated: Vec<_> = (0..4)
|
|
.flat_map(|rep| {
|
|
source
|
|
.notes
|
|
.iter()
|
|
.map(move |n| (n.start + rep * SECTION, n.duration))
|
|
})
|
|
.collect();
|
|
for c in generate(
|
|
&source,
|
|
&Settings {
|
|
preserve_rhythm: true,
|
|
..Settings::default()
|
|
},
|
|
)
|
|
.unwrap()
|
|
{
|
|
assert_eq!(rhythm(&c.phrase), repeated);
|
|
}
|
|
}
|
|
#[test]
|
|
fn impossible_diversity_returns_one_honest_proposal() {
|
|
let mut source = demo_motif();
|
|
for n in &mut source.notes {
|
|
n.locked = true;
|
|
}
|
|
let candidates = generate(&source, &Settings::default()).unwrap();
|
|
assert_eq!(candidates.len(), 1);
|
|
assert_eq!(candidates[0].phrase.notes.len(), source.notes.len() * 4);
|
|
}
|
|
#[test]
|
|
fn harmony_drives_strong_notes_and_distinct_open_or_closed_arrivals() {
|
|
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 generate_with_harmony(&demo_motif(), &s, &harmony).unwrap() {
|
|
let last = c.phrase.notes.last().unwrap();
|
|
let chord = ChordQuality::Major.mask(0) & (1 << (last.pitch % 12)) != 0;
|
|
assert_eq!(chord, ending == Ending::Resolve);
|
|
for n in c.phrase.notes.iter().filter(|n| {
|
|
n.start >= SECTION && 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![
|
|
Note {
|
|
start: 0,
|
|
duration: BAR,
|
|
pitch: 64,
|
|
velocity: 90,
|
|
..Default::default()
|
|
},
|
|
Note {
|
|
start: 0,
|
|
duration: BAR,
|
|
pitch: 67,
|
|
velocity: 90,
|
|
..Default::default()
|
|
},
|
|
Note {
|
|
start: 0,
|
|
duration: BAR,
|
|
pitch: 72,
|
|
velocity: 90,
|
|
..Default::default()
|
|
},
|
|
];
|
|
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 pins_combine_candidates_and_survive_regeneration_exactly() {
|
|
let mut s = Session {
|
|
source: Some(demo_motif()),
|
|
..Default::default()
|
|
};
|
|
s.regenerate().unwrap();
|
|
s.pin_selected(1).unwrap();
|
|
let saved = s.pins[0].clone();
|
|
s.selected = 1;
|
|
let expected: Vec<_> = saved
|
|
.phrase
|
|
.notes
|
|
.iter()
|
|
.map(|n| Note {
|
|
start: n.start + SECTION,
|
|
..*n
|
|
})
|
|
.collect();
|
|
let combined = s.composed_phrase().unwrap();
|
|
assert_eq!(
|
|
combined
|
|
.notes
|
|
.iter()
|
|
.filter(|n| n.start >= SECTION && n.start < 2 * SECTION)
|
|
.copied()
|
|
.collect::<Vec<_>>(),
|
|
expected
|
|
);
|
|
s.settings.seed += 7;
|
|
s.regenerate().unwrap();
|
|
assert_eq!(s.pins[0], saved);
|
|
s.accept_composed().unwrap();
|
|
assert_eq!(s.accepted, Some(s.composed_phrase().unwrap()));
|
|
assert_eq!(Session::from_json(&s.to_json().unwrap()).unwrap(), s);
|
|
}
|
|
#[test]
|
|
fn version_one_sessions_migrate_without_losing_existing_notes() {
|
|
let s = Session {
|
|
source: Some(demo_motif()),
|
|
..Default::default()
|
|
};
|
|
let mut value = serde_json::to_value(&s).unwrap();
|
|
value["version"] = 1.into();
|
|
let object = value.as_object_mut().unwrap();
|
|
for key in ["harmony", "pins", "audition", "looping"] {
|
|
object.remove(key);
|
|
}
|
|
let settings = object.get_mut("settings").unwrap().as_object_mut().unwrap();
|
|
for key in ["preserve_contour", "task", "ending"] {
|
|
settings.remove(key);
|
|
}
|
|
for n in object.get_mut("source").unwrap()["notes"]
|
|
.as_array_mut()
|
|
.unwrap()
|
|
{
|
|
n.as_object_mut().unwrap().remove("pitch_locked");
|
|
n.as_object_mut().unwrap().remove("rhythm_locked");
|
|
}
|
|
let migrated = Session::from_json(&serde_json::to_vec(&value).unwrap()).unwrap();
|
|
assert_eq!(migrated.version, STATE_VERSION);
|
|
assert_eq!(migrated.source, s.source);
|
|
assert!(!migrated.looping);
|
|
assert!(migrated.harmony.spans.is_empty());
|
|
}
|
|
#[test]
|
|
fn invalid_pins_and_unsafe_legacy_seams_are_rejected() {
|
|
let mut s = Session {
|
|
source: Some(demo_motif()),
|
|
..Default::default()
|
|
};
|
|
s.regenerate().unwrap();
|
|
s.candidates[0].phrase.notes = vec![Note {
|
|
start: SECTION - 100,
|
|
duration: 200,
|
|
pitch: 60,
|
|
velocity: 80,
|
|
..Default::default()
|
|
}];
|
|
assert!(s.pin_selected(1).is_err());
|
|
assert!(s.pin_selected(9).is_err());
|
|
}
|