Check and Windows preview / check (patina) (push) Successful in 3m59s
Check and Windows preview / check (rivet) (push) Successful in 2m2s
Check and Windows preview / previews (push) Successful in 1m8s
Independent plugin release / plan (push) Successful in 7s
Check and Windows preview / windows (push) Successful in 2m41s
Independent plugin release / build (push) Successful in 3m0s
Independent plugin release / publish (push) Successful in 9s
Rivet 0.4.0 tells a motif as a story. A timeline of blocks with roles (statement, answer, departure, return, rest) replaces tasks, pins and branches. Rivet writes each block from a motif and joins the seams with a cut, breath, pickup or approach. - Motifs of 1-8 bars in a bin of 16; storyboards up to 32 bars. - Meters 2/4, 3/4, 4/4, 5/4, 6/8, 7/8 and 12/8 for import, writing, audition and export. Phrygian, harmonic minor and Mixolydian scales. - Per-block options, Another take and Keep; templates for new boards. - Editor rebuilt: motif bin, timeline, large piano roll, inspector, tabs for generation, harmony and sound roles, 32-step undo/redo. - Sessions from 0.1-0.3 open as a storyboard from their source motif.
204 lines
6.4 KiB
Rust
204 lines
6.4 KiB
Rust
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<Candidate> {
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|