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

+12 -13
View File
@@ -1,15 +1,15 @@
//! Shared application state and bounded control messages. The audio callback never locks a session.
use crossbeam_queue::ArrayQueue;
use rivet_core::{
Audition, Candidate, Error, EventBatch, MessageKind, MidiMessage, Performance, PerformanceClip,
PerformancePlayer, Result, Session, SECTION,
Audition, Error, EventBatch, MessageKind, MidiMessage, Performance, PerformanceClip,
PerformancePlayer, Result, Session,
};
use std::{
fs::File,
io::{Read, Write},
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering},
Arc, Mutex, RwLock,
},
};
@@ -34,17 +34,17 @@ pub struct Shared {
pub queued: AtomicBool,
pub playing: AtomicBool,
pub playhead: AtomicU64,
/// Bar length in ticks; queued auditions switch on the next bar line.
pub bar: AtomicU32,
pub commands: ArrayQueue<PlayRequest>,
pub busy: AtomicBool,
results: Mutex<Vec<Completion>>,
}
pub enum Output {
Imported(rivet_midi::MidiFile),
Generated(Vec<Candidate>),
Exported(PathBuf),
Saved(PathBuf),
Loaded(Box<Session>),
LibraryLoaded(Vec<rivet_core::Gesture>),
Canceled,
}
pub struct Completion {
@@ -65,6 +65,7 @@ impl Default for Shared {
queued: AtomicBool::new(false),
playing: AtomicBool::new(false),
playhead: AtomicU64::new(0),
bar: AtomicU32::new(rivet_core::BAR),
commands: ArrayQueue::new(2),
busy: AtomicBool::new(false),
results: Mutex::new(Vec::new()),
@@ -87,6 +88,7 @@ impl Shared {
self.midi
.store(s.audition == Audition::Midi, Ordering::Release);
self.looping.store(s.looping, Ordering::Release);
self.bar.store(s.settings.meter.bar(), Ordering::Release);
self.revision.fetch_add(1, Ordering::AcqRel);
}
pub fn replace(&self, mut session: Session) -> Result<()> {
@@ -107,10 +109,7 @@ impl Shared {
"Session changed while this operation was running. Try again.".into(),
));
}
if current.source != session.source
|| current.settings != session.settings
|| current.harmony != session.harmony
|| current.candidates != session.candidates
if current.settings.meter != session.settings.meter
|| current.performance != session.performance
|| current.audition != session.audition
{
@@ -130,8 +129,9 @@ impl Shared {
};
let settings = state.performance.clone();
let compensate = state.audition == Audition::Midi;
let meter = state.settings.meter;
drop(state);
Performance::compile(phrase, &settings, tempo, compensate)?.clip()
Performance::compile(phrase, &settings, tempo, compensate, meter)?.clip()
}
pub fn preview(&self, phrase: &rivet_core::Phrase) -> Result<()> {
let epoch = self.epoch.load(Ordering::Acquire);
@@ -279,9 +279,8 @@ impl AudioEngine {
if request.quantized && self.player.is_playing() {
let now = self.player.absolute_tick();
let lead = request.clip.lead_in as f64;
let boundary = ((now + lead) / SECTION as f64)
.floor()
.mul_add(SECTION as f64, SECTION as f64);
let bar = shared.bar.load(Ordering::Acquire).max(1) as f64;
let boundary = ((now + lead) / bar).floor().mul_add(bar, bar);
self.pending = Some((boundary - lead, request.clip));
} else {
self.player.play(request.clip);