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

+38 -35
View File
@@ -1,5 +1,5 @@
//! Headless end-to-end development harness; uses exactly the plugin's composition and voice.
use rivet_core::{demo_motif, Harmony, Performance, PerformancePlayer, Session};
use rivet_core::{Harmony, Performance, PerformancePlayer, Session, Template};
use std::{env, fs, io::Write, path::PathBuf};
fn main() {
@@ -11,13 +11,13 @@ fn main() {
fn run() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = env::args().skip(1).collect();
if args.is_empty() || args[0] == "--help" {
println!("Rivet preview\n rivet-preview demo <output-directory>\n rivet-preview develop <input.mid> <track-index> <output-directory> [harmony.mid harmony-track-index]\n rivet-preview session <session.json> <output-directory>\nWrites source.mid, up to three distinct candidates, session.json, and selected.wav. Track indices start at 0.");
println!("Rivet preview\n rivet-preview demo <output-directory>\n rivet-preview develop <input.mid> <track-index> <output-directory> [harmony.mid harmony-track-index]\n rivet-preview session <session.json> <output-directory>\nBuilds an arc storyboard from the motif (or uses the saved one) and writes motif.mid, storyboard.mid, session.json, and storyboard.wav. Track indices start at 0.");
return Ok(());
}
let mut session = Session::default();
let output = match args[0].as_str() {
"demo" if args.len() == 2 => {
session.source = Some(demo_motif());
session = Session::demo();
PathBuf::from(&args[1])
}
"session" if args.len() == 3 => {
@@ -25,62 +25,65 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
PathBuf::from(&args[2])
}
"develop" if args.len() == 4 || args.len() == 6 => {
let bytes = fs::read(&args[1])?;
let file = rivet_midi::read(&bytes)?;
let file = rivet_midi::read(&fs::read(&args[1])?)?;
let index: usize = args[2].parse()?;
let track = file.tracks.get(index).ok_or("Track index out of range")?;
track.phrase.validate_source()?;
for w in &track.warnings {
eprintln!("{w}");
}
session.source = Some(track.phrase.clone());
session.settings.meter = file.meter.unwrap_or_default();
if args.len() == 6 {
let file = rivet_midi::read(&fs::read(&args[4])?)?;
let index: usize = args[5].parse()?;
let track = file
.tracks
.get(index)
.ok_or("Harmony track index out of range")?;
session.harmony = Harmony::from_midi(&track.phrase.notes, track.phrase.length)?;
}
let id = session.add_motif(&track.name, track.phrase.clone())?;
session.apply_template(Template::Arc, id)?;
PathBuf::from(&args[3])
}
_ => return Err("Use --help for usage.".into()),
};
if args.len() == 6 {
let file = rivet_midi::read(&fs::read(&args[4])?)?;
let index: usize = args[5].parse()?;
let track = file
.tracks
.get(index)
.ok_or("Harmony track index out of range")?;
session.harmony = Harmony::from_midi(&track.phrase.notes, track.phrase.length)?;
}
if args[0] != "session" {
session.regenerate()?;
session.accept();
}
let selected = session.composed_phrase()?;
let meter = session.settings.meter;
let selected = session.compose()?;
fs::create_dir_all(&output)?;
if let Some(source) = &session.source {
if let Some(motif) = session.motifs.first() {
fs::write(
output.join("source.mid"),
rivet_midi::write(source, session.tempo)?,
output.join("motif.mid"),
rivet_midi::write(&motif.phrase, session.tempo, meter)?,
)?;
}
for (i, c) in session.candidates.iter().enumerate() {
fs::write(
output.join(format!("candidate-{}.mid", i + 1)),
rivet_midi::write_performance(&c.phrase, &session.performance, session.tempo)?,
)?;
println!("{}: {}", c.strategy.name(), c.explanation);
for (i, block) in session.blocks.iter().enumerate() {
let option = block.options.get(block.choice);
println!(
"{:>2}. {:<9} {} bars {:<22} → {}",
i + 1,
block.role.name(),
block.bars,
option.map_or("(empty)", |c| c.strategy.name()),
block.join.name()
);
}
fs::write(output.join("session.json"), session.to_json()?)?;
fs::write(
output.join("selected.mid"),
rivet_midi::write_performance(&selected, &session.performance, session.tempo)?,
output.join("storyboard.mid"),
rivet_midi::write_performance(&selected, &session.performance, session.tempo, meter)?,
)?;
let mut player = PerformancePlayer::default();
player.set_sample_rate(44100.)?;
player
.play(Performance::compile(&selected, &session.performance, session.tempo, false)?.clip()?);
player.play(
Performance::compile(&selected, &session.performance, session.tempo, false, meter)?
.clip()?,
);
let mut samples = Vec::new();
while player.is_playing() {
samples.push((player.next_frame(session.tempo).0 * i16::MAX as f32) as i16);
}
let len = (samples.len() * 2) as u32;
let mut wav = fs::File::create(output.join("selected.wav"))?;
let mut wav = fs::File::create(output.join("storyboard.wav"))?;
wav.write_all(b"RIFF")?;
wav.write_all(&(36 + len).to_le_bytes())?;
wav.write_all(b"WAVEfmt ")?;