Files
bass-foundry/plugins/rivet/core/tests/musical.rs
T
nvrl 15853e61ac
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
restructure
2026-11-05 23:58:00 +01:00

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(&notes, 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());
}