Files
nvrl f1366280ff
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
feat(rivet): storyboard composition with flexible lengths and meters
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.
2026-11-06 02:15:18 +01:00

159 lines
4.8 KiB
Rust

use rivet_core::*;
fn held() -> Phrase {
Phrase {
length: PPQ,
notes: vec![
Note {
start: 0,
duration: PPQ / 2,
pitch: 60,
velocity: 90,
..Default::default()
},
Note {
start: PPQ / 2,
duration: PPQ / 2,
pitch: 60,
velocity: 70,
..Default::default()
},
],
}
}
#[test]
fn role_handoffs_preserve_event_identity_and_respect_locks() {
let mut source = demo_motif();
source.notes[1].pitch_locked = true;
source.notes[0].locked = true;
for mode in RoleMode::ALL {
let mut s = PerformanceSettings {
mode,
..Default::default()
};
s.voices[0].transpose = -12;
s.voices[1].transpose = 12;
s.voices[0].velocity = 50;
s.voices[1].velocity = 50;
let p = Performance::compile(&source, &s, 140., false, Meter::default()).unwrap();
let on: Vec<_> = p
.events
.iter()
.filter(|e| e.message.kind == MessageKind::On && !e.next_cycle)
.collect();
assert_eq!(on.len(), source.notes.len());
for (i, n) in source.notes.iter().enumerate() {
let role = mode.role(i, n, Meter::default());
let actual = on.iter().find(|e| e.tick == n.start as i32).unwrap();
assert_eq!(actual.message.channel, s.voices[role].channel);
assert_eq!(
actual.message.key,
if n.keeps_pitch() {
n.pitch
} else {
(n.pitch as i16 + s.voices[role].transpose as i16) as u8
}
);
}
assert_eq!(on[0].message.value, 90);
}
}
#[test]
fn controller_interpolation_and_validation_are_explicit() {
let mut curve = ControllerCurve::default();
assert_eq!(curve.value_at(500), 64);
curve.shape(2);
assert_eq!(curve.value_at(500), 127);
assert_eq!(curve.value_at(1000), 0);
curve.cc = 64;
assert!(curve.validate().is_err());
let mut s = PerformanceSettings::default();
s.voices[0].controller.enabled = true;
s.voices[0].controller.cc = 20;
let p = Performance::compile(&held(), &s, 120., false, Meter::default()).unwrap();
assert!(p
.events
.iter()
.any(|e| e.message.kind == MessageKind::Control
&& e.message.key == 20
&& e.message.value == 127));
s.voices[0].controller.points[1].position = 0;
assert!(s.validate().is_err());
}
#[test]
fn attack_compensation_warms_up_once_and_wraps_without_cutting_the_next_note() {
let mut s = PerformanceSettings::default();
s.voices[0].attack_ms = 100;
let performance = Performance::compile(&held(), &s, 120., true, Meter::default()).unwrap();
assert_eq!(performance.lead_in, 192);
assert!(performance
.events
.iter()
.any(|e| e.tick == -192 && e.message.kind == MessageKind::On && !e.next_cycle));
let mut player = PerformancePlayer::default();
player.set_sample_rate(8000.).unwrap();
player.set_looping(true);
player.play(performance.clip().unwrap());
let mut ons = Vec::new();
for frame in 0..4802 {
let (_, events) = player.next_frame(120.);
for e in events.events[..events.len].iter().flatten() {
if e.kind == MessageKind::On {
ons.push(frame);
}
}
}
assert_eq!(ons.len(), 3);
assert_eq!(ons[0], 0);
assert!((ons[2] as i32 - 4000).abs() <= 1);
assert!(player
.current_messages()
.events
.iter()
.flatten()
.any(|e| e.kind == MessageKind::On && e.key == 60));
player.set_looping(false);
player.play(performance.clip().unwrap());
let mut count = 0;
while player.is_playing() {
let (_, events) = player.next_frame(120.);
count += events
.events
.iter()
.flatten()
.filter(|e| e.kind == MessageKind::On)
.count();
}
assert_eq!(count, 2);
}
#[test]
fn public_clip_validation_rejects_bad_roles_and_dense_events() {
let mut p = Performance::compile(
&held(),
&PerformanceSettings::default(),
120.,
false,
Meter::default(),
)
.unwrap();
p.events[0].role = 9;
assert!(p.clip().is_err());
let event = TimedMessage {
tick: 0,
role: 0,
message: MidiMessage {
kind: MessageKind::On,
channel: 0,
key: 60,
value: 90,
rest: 0,
},
next_cycle: false,
};
let p = Performance {
length: PPQ,
lead_in: 0,
events: vec![event; 17],
};
assert!(p.clip().is_err());
}