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
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:
1 parent
abd37fd4fe
commit
f1366280ff
37 files changed
+3763
-2549
No files matched your search
@@ -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 ")?;
|
||||
|
||||
Reference in new issue
Block a user