feat(rivet): storyboard composition with flexible lengths and meters
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.
This commit is contained in:
nvrl committed 2026-11-06 02:15:18 +01:00
1 parent abd37fd4fe
commit f1366280ff
37 files changed
+3763 -2549

No files matched your search

+9 -6
View File
@@ -1,5 +1,5 @@
//! Explicit sound roles and controller gestures, compiled away from the audio thread.
use crate::{err, Note, Phrase, Result, BAR, MAX_PLAYBACK_NOTES, PPQ, SECTION};
use crate::{err, Meter, Note, Phrase, Result, MAX_LENGTH, MAX_PLAYBACK_NOTES, PPQ};
use serde::{Deserialize, Serialize};
use std::f64::consts::TAU;
@@ -28,12 +28,14 @@ impl RoleMode {
Self::Interlock => "Strong beats and replies",
}
}
pub fn role(self, index: usize, n: &Note) -> usize {
/// Which of the two voices plays note `index`. Question/answer alternates bars;
/// strong beats follow the meter's pulse.
pub fn role(self, index: usize, n: &Note, meter: Meter) -> usize {
match self {
Self::Solo => 0,
Self::Alternating => index % 2,
Self::CallResponse => usize::from(n.start % SECTION >= BAR),
Self::Interlock => usize::from(!n.start.is_multiple_of(PPQ)),
Self::CallResponse => ((n.start / meter.bar()) % 2) as usize,
Self::Interlock => usize::from(!meter.strong(n.start)),
}
}
}
@@ -224,6 +226,7 @@ impl Performance {
settings: &PerformanceSettings,
bpm: f64,
compensate: bool,
meter: Meter,
) -> Result<Self> {
phrase.validate(MAX_PLAYBACK_NOTES)?;
settings.validate()?;
@@ -252,7 +255,7 @@ impl Performance {
.notes
.iter()
.enumerate()
.filter(|(i, n)| settings.mode.role(*i, n) == role)
.filter(|(i, n)| settings.mode.role(*i, n, meter) == role)
.map(|(_, n)| n)
.collect();
if routed.is_empty() && !profile.controller.enabled {
@@ -378,7 +381,7 @@ impl Performance {
pub fn clip(&self) -> Result<PerformanceClip> {
if self.events.len() > MAX_PERFORMANCE_EVENTS
|| self.length == 0
|| self.length > 8 * BAR
|| self.length > MAX_LENGTH
|| self.lead_in >= self.length
{
return Err(err("Invalid performance clip."));