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.
662 lines
23 KiB
Rust
662 lines
23 KiB
Rust
//! The storyboard: a bin of motifs, and a timeline of blocks that develop them.
|
||
//! Rivet joins neighbouring blocks at their seams.
|
||
use crate::{
|
||
err, legacy, realize, Audition, Candidate, Error, Harmony, Note, PerformanceSettings, Phrase,
|
||
Result, Settings, MAX_PLAYBACK_NOTES, MAX_SOURCE_NOTES, MAX_STATE_BYTES, STATE_VERSION,
|
||
};
|
||
use serde::{Deserialize, Deserializer, Serialize};
|
||
|
||
pub const MAX_MOTIFS: usize = 16;
|
||
pub const MAX_BLOCKS: usize = 32;
|
||
/// Longest storyboard, in bars.
|
||
pub const MAX_BARS: u32 = 32;
|
||
/// Longest motif and longest block, in bars.
|
||
pub const MAX_MOTIF_BARS: u32 = 8;
|
||
|
||
/// A short idea the producer brought in. Locks live on its notes.
|
||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||
pub struct Motif {
|
||
pub id: u64,
|
||
pub name: String,
|
||
pub phrase: Phrase,
|
||
}
|
||
|
||
/// What a block does in the story.
|
||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||
pub enum Role {
|
||
/// Present the idea so it can be recognized later.
|
||
#[default]
|
||
Statement,
|
||
/// Reply to what came before.
|
||
Answer,
|
||
/// Move away: new register, new rhythm.
|
||
Departure,
|
||
/// Bring the idea back.
|
||
Return,
|
||
/// Silence.
|
||
Rest,
|
||
}
|
||
impl Role {
|
||
pub const ALL: [Self; 5] = [
|
||
Self::Statement,
|
||
Self::Answer,
|
||
Self::Departure,
|
||
Self::Return,
|
||
Self::Rest,
|
||
];
|
||
pub fn name(self) -> &'static str {
|
||
match self {
|
||
Self::Statement => "Statement",
|
||
Self::Answer => "Answer",
|
||
Self::Departure => "Departure",
|
||
Self::Return => "Return",
|
||
Self::Rest => "Rest",
|
||
}
|
||
}
|
||
pub fn hint(self) -> &'static str {
|
||
match self {
|
||
Self::Statement => "Present the motif so it can be recognized later.",
|
||
Self::Answer => "Reply to what came before: shorter call, longer arrival.",
|
||
Self::Departure => "Move away: higher register, displaced rhythm, fragments.",
|
||
Self::Return => "Bring the motif back and land it.",
|
||
Self::Rest => "Silence. Space makes the next entry count.",
|
||
}
|
||
}
|
||
}
|
||
|
||
/// How a block hands over to the next one.
|
||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||
pub enum Join {
|
||
/// No change at the seam.
|
||
#[default]
|
||
Cut,
|
||
/// Leave half a beat of air before the next block.
|
||
Breath,
|
||
/// Two short notes in the last beat climb or fall into the next block's first note.
|
||
Pickup,
|
||
/// The last note moves one scale step next to the next block's first note.
|
||
Approach,
|
||
}
|
||
impl Join {
|
||
pub const ALL: [Self; 4] = [Self::Cut, Self::Breath, Self::Pickup, Self::Approach];
|
||
pub fn name(self) -> &'static str {
|
||
match self {
|
||
Self::Cut => "Cut",
|
||
Self::Breath => "Breath",
|
||
Self::Pickup => "Pickup",
|
||
Self::Approach => "Approach",
|
||
}
|
||
}
|
||
pub fn hint(self) -> &'static str {
|
||
match self {
|
||
Self::Cut => "Blocks meet as written.",
|
||
Self::Breath => "The last note is shortened to leave air before the next block.",
|
||
Self::Pickup => "Two short notes lead into the next block's first note.",
|
||
Self::Approach => "The last note steps next to the next block's first note.",
|
||
}
|
||
}
|
||
/// A musical default for a transition between two roles.
|
||
pub fn between(from: Role, to: Role) -> Self {
|
||
match (from, to) {
|
||
(Role::Rest, _) | (_, Role::Rest) => Self::Cut,
|
||
(_, Role::Departure) => Self::Pickup,
|
||
(_, Role::Return) => Self::Approach,
|
||
_ => Self::Breath,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// One section of the storyboard.
|
||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||
pub struct Block {
|
||
pub id: u64,
|
||
/// Motif this block develops. Ignored for rests.
|
||
pub motif: u64,
|
||
pub role: Role,
|
||
pub bars: u8,
|
||
/// Hand-over into the next block.
|
||
pub join: Join,
|
||
/// Increment for another set of options.
|
||
#[serde(default)]
|
||
pub take: u64,
|
||
/// Up to three realized options; the first fits the role best.
|
||
#[serde(default)]
|
||
pub options: Vec<Candidate>,
|
||
#[serde(default)]
|
||
pub choice: usize,
|
||
/// Kept blocks are not regenerated when other inputs change.
|
||
#[serde(default)]
|
||
pub kept: bool,
|
||
}
|
||
impl Block {
|
||
pub fn phrase(&self) -> Option<&Phrase> {
|
||
self.options.get(self.choice).map(|c| &c.phrase)
|
||
}
|
||
}
|
||
|
||
/// Starting shapes for a storyboard built from one motif.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum Template {
|
||
Arc,
|
||
QuestionAnswer,
|
||
Breathe,
|
||
Build,
|
||
}
|
||
impl Template {
|
||
pub const ALL: [Self; 4] = [Self::Arc, Self::QuestionAnswer, Self::Breathe, Self::Build];
|
||
pub fn name(self) -> &'static str {
|
||
match self {
|
||
Self::Arc => "Arc: statement, answer, departure, return",
|
||
Self::QuestionAnswer => "Question and answer, twice",
|
||
Self::Breathe => "Breathe: ideas separated by rests",
|
||
Self::Build => "Build: two departures before the return",
|
||
}
|
||
}
|
||
fn roles(self) -> &'static [Role] {
|
||
use Role::*;
|
||
match self {
|
||
Self::Arc => &[Statement, Answer, Departure, Return],
|
||
Self::QuestionAnswer => &[Statement, Answer, Statement, Answer],
|
||
Self::Breathe => &[Statement, Rest, Answer, Rest, Return],
|
||
Self::Build => &[Statement, Answer, Departure, Departure, Answer, Return],
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||
pub struct Session {
|
||
pub version: u32,
|
||
pub settings: Settings,
|
||
pub motifs: Vec<Motif>,
|
||
pub blocks: Vec<Block>,
|
||
pub tempo: f64,
|
||
pub follow_host: bool,
|
||
pub harmony: Harmony,
|
||
pub audition: Audition,
|
||
pub looping: bool,
|
||
pub performance: PerformanceSettings,
|
||
pub next_id: u64,
|
||
}
|
||
impl Default for Session {
|
||
fn default() -> Self {
|
||
Self {
|
||
version: STATE_VERSION,
|
||
settings: Settings::default(),
|
||
motifs: Vec::new(),
|
||
blocks: Vec::new(),
|
||
tempo: 140.0,
|
||
follow_host: false,
|
||
harmony: Harmony::default(),
|
||
audition: Audition::Internal,
|
||
looping: true,
|
||
performance: PerformanceSettings::default(),
|
||
next_id: 1,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Current-format fields, read leniently so added fields default.
|
||
#[derive(Deserialize)]
|
||
#[serde(default)]
|
||
struct Current {
|
||
version: u32,
|
||
settings: Settings,
|
||
motifs: Vec<Motif>,
|
||
blocks: Vec<Block>,
|
||
tempo: f64,
|
||
follow_host: bool,
|
||
harmony: Harmony,
|
||
audition: Audition,
|
||
looping: bool,
|
||
performance: PerformanceSettings,
|
||
next_id: u64,
|
||
}
|
||
impl Default for Current {
|
||
fn default() -> Self {
|
||
let s = Session::default();
|
||
Self {
|
||
version: s.version,
|
||
settings: s.settings,
|
||
motifs: s.motifs,
|
||
blocks: s.blocks,
|
||
tempo: s.tempo,
|
||
follow_host: s.follow_host,
|
||
harmony: s.harmony,
|
||
audition: s.audition,
|
||
looping: s.looping,
|
||
performance: s.performance,
|
||
next_id: s.next_id,
|
||
}
|
||
}
|
||
}
|
||
/// Sessions from version 1–3 (single motif, fixed two-bar sections) become a
|
||
/// storyboard: the motif goes into the bin and the old phrase length becomes blocks.
|
||
impl<'de> Deserialize<'de> for Session {
|
||
fn deserialize<D: Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
|
||
use serde::de::Error as _;
|
||
let value = serde_json::Value::deserialize(d)?;
|
||
let version = value
|
||
.get("version")
|
||
.and_then(serde_json::Value::as_u64)
|
||
.unwrap_or(1);
|
||
if version < STATE_VERSION as u64 {
|
||
let old: legacy::Session = serde_json::from_value(value).map_err(D::Error::custom)?;
|
||
return Ok(old.upgrade());
|
||
}
|
||
let c: Current = serde_json::from_value(value).map_err(D::Error::custom)?;
|
||
Ok(Self {
|
||
version: c.version,
|
||
settings: c.settings,
|
||
motifs: c.motifs,
|
||
blocks: c.blocks,
|
||
tempo: c.tempo,
|
||
follow_host: c.follow_host,
|
||
harmony: c.harmony,
|
||
audition: c.audition,
|
||
looping: c.looping,
|
||
performance: c.performance,
|
||
next_id: c.next_id,
|
||
})
|
||
}
|
||
}
|
||
|
||
impl Session {
|
||
pub fn migrate(&mut self) -> Result<()> {
|
||
if self.version != STATE_VERSION {
|
||
return Err(err("Unsupported session version."));
|
||
}
|
||
Ok(())
|
||
}
|
||
fn id(&mut self) -> u64 {
|
||
let id = self.next_id;
|
||
self.next_id += 1;
|
||
id
|
||
}
|
||
pub fn motif(&self, id: u64) -> Option<&Motif> {
|
||
self.motifs.iter().find(|m| m.id == id)
|
||
}
|
||
pub fn motif_mut(&mut self, id: u64) -> Option<&mut Motif> {
|
||
self.motifs.iter_mut().find(|m| m.id == id)
|
||
}
|
||
pub fn total_bars(&self) -> u32 {
|
||
self.blocks.iter().map(|b| b.bars as u32).sum()
|
||
}
|
||
/// Start of block `index` in ticks.
|
||
pub fn block_start(&self, index: usize) -> u32 {
|
||
self.blocks[..index.min(self.blocks.len())]
|
||
.iter()
|
||
.map(|b| b.bars as u32 * self.settings.meter.bar())
|
||
.sum()
|
||
}
|
||
fn last_sounding(&self) -> Option<usize> {
|
||
self.blocks.iter().rposition(|b| b.role != Role::Rest)
|
||
}
|
||
fn check_motif(&self, phrase: &Phrase) -> Result<()> {
|
||
phrase.validate(MAX_SOURCE_NOTES)?;
|
||
if phrase.length > MAX_MOTIF_BARS * self.settings.meter.bar() {
|
||
return Err(Error(format!(
|
||
"A motif can be at most {MAX_MOTIF_BARS} bars of {}.",
|
||
self.settings.meter.name()
|
||
)));
|
||
}
|
||
Ok(())
|
||
}
|
||
/// Adds a motif to the bin. Its length is rounded up to whole bars.
|
||
pub fn add_motif(&mut self, name: &str, mut phrase: Phrase) -> Result<u64> {
|
||
if self.motifs.len() >= MAX_MOTIFS {
|
||
return Err(Error(format!(
|
||
"The bin holds {MAX_MOTIFS} motifs. Remove one first."
|
||
)));
|
||
}
|
||
let bar = self.settings.meter.bar();
|
||
phrase.length = self.settings.meter.bars_for(phrase.length) * bar;
|
||
self.check_motif(&phrase)?;
|
||
let id = self.id();
|
||
let name = name.trim();
|
||
self.motifs.push(Motif {
|
||
id,
|
||
name: if name.is_empty() {
|
||
format!("Motif {}", self.motifs.len() + 1)
|
||
} else {
|
||
name.chars().take(60).collect()
|
||
},
|
||
phrase,
|
||
});
|
||
Ok(id)
|
||
}
|
||
/// Removes a motif and every block that develops it.
|
||
pub fn remove_motif(&mut self, id: u64) {
|
||
self.motifs.retain(|m| m.id != id);
|
||
self.blocks
|
||
.retain(|b| b.role == Role::Rest || b.motif != id);
|
||
self.realize_all();
|
||
}
|
||
/// Motif length in whole bars of the current meter.
|
||
pub fn motif_bars(&self, id: u64) -> u8 {
|
||
self.motif(id).map_or(2, |m| {
|
||
self.settings
|
||
.meter
|
||
.bars_for(m.phrase.length)
|
||
.min(MAX_MOTIF_BARS) as u8
|
||
})
|
||
}
|
||
/// Inserts a block at `index` and realizes the storyboard.
|
||
pub fn insert_block(&mut self, index: usize, motif: u64, role: Role, bars: u8) -> Result<u64> {
|
||
if self.blocks.len() >= MAX_BLOCKS {
|
||
return Err(Error(format!("A storyboard holds {MAX_BLOCKS} blocks.")));
|
||
}
|
||
if !(1..=MAX_MOTIF_BARS as u8).contains(&bars) {
|
||
return Err(err("A block is 1–8 bars long."));
|
||
}
|
||
if self.total_bars() + bars as u32 > MAX_BARS {
|
||
return Err(Error(format!(
|
||
"The storyboard is limited to {MAX_BARS} bars."
|
||
)));
|
||
}
|
||
if role != Role::Rest && self.motif(motif).is_none() {
|
||
return Err(err("Choose a motif from the bin first."));
|
||
}
|
||
let index = index.min(self.blocks.len());
|
||
let id = self.id();
|
||
let join = self
|
||
.blocks
|
||
.get(index)
|
||
.map_or(Join::Cut, |next| Join::between(role, next.role));
|
||
if let Some(previous) = index.checked_sub(1).and_then(|i| self.blocks.get_mut(i)) {
|
||
previous.join = Join::between(previous.role, role);
|
||
}
|
||
self.blocks.insert(
|
||
index,
|
||
Block {
|
||
id,
|
||
motif,
|
||
role,
|
||
bars,
|
||
join,
|
||
take: 0,
|
||
options: Vec::new(),
|
||
choice: 0,
|
||
kept: false,
|
||
},
|
||
);
|
||
self.realize_all();
|
||
Ok(id)
|
||
}
|
||
pub fn remove_block(&mut self, index: usize) {
|
||
if index < self.blocks.len() {
|
||
self.blocks.remove(index);
|
||
self.realize_all();
|
||
}
|
||
}
|
||
pub fn move_block(&mut self, from: usize, to: usize) {
|
||
if from < self.blocks.len() && to < self.blocks.len() && from != to {
|
||
let block = self.blocks.remove(from);
|
||
self.blocks.insert(to, block);
|
||
self.realize_all();
|
||
}
|
||
}
|
||
pub fn duplicate_block(&mut self, index: usize) -> Result<()> {
|
||
let Some(block) = self.blocks.get(index).cloned() else {
|
||
return Ok(());
|
||
};
|
||
if self.blocks.len() >= MAX_BLOCKS || self.total_bars() + block.bars as u32 > MAX_BARS {
|
||
return Err(Error(format!(
|
||
"The storyboard is limited to {MAX_BLOCKS} blocks and {MAX_BARS} bars."
|
||
)));
|
||
}
|
||
let id = self.id();
|
||
self.blocks.insert(index + 1, Block { id, ..block });
|
||
self.realize_all();
|
||
Ok(())
|
||
}
|
||
/// Replaces the timeline with a template built from one motif.
|
||
pub fn apply_template(&mut self, template: Template, motif: u64) -> Result<()> {
|
||
if self.motif(motif).is_none() {
|
||
return Err(err("Choose a motif from the bin first."));
|
||
}
|
||
let size = self.motif_bars(motif);
|
||
let roles = template.roles();
|
||
let mut blocks = Vec::new();
|
||
let mut total = 0;
|
||
for (i, &role) in roles.iter().enumerate() {
|
||
let bars = if role == Role::Rest { 1 } else { size };
|
||
if total + bars as u32 > MAX_BARS {
|
||
break;
|
||
}
|
||
total += bars as u32;
|
||
let join = roles
|
||
.get(i + 1)
|
||
.map_or(Join::Cut, |&next| Join::between(role, next));
|
||
let id = self.id();
|
||
blocks.push(Block {
|
||
id,
|
||
motif,
|
||
role,
|
||
bars,
|
||
join,
|
||
take: 0,
|
||
options: Vec::new(),
|
||
choice: 0,
|
||
kept: false,
|
||
});
|
||
}
|
||
self.blocks = blocks;
|
||
self.realize_all();
|
||
Ok(())
|
||
}
|
||
/// New options for one block; it is no longer kept.
|
||
pub fn another_take(&mut self, index: usize) {
|
||
if let Some(block) = self.blocks.get_mut(index) {
|
||
block.take = block.take.wrapping_add(1);
|
||
block.kept = false;
|
||
block.choice = 0;
|
||
self.realize(index);
|
||
}
|
||
}
|
||
fn realize(&mut self, index: usize) {
|
||
let offset = self.block_start(index);
|
||
let last = self.last_sounding() == Some(index);
|
||
let block = &self.blocks[index];
|
||
let motif = self
|
||
.motif(block.motif)
|
||
.map(|m| m.phrase.clone())
|
||
.unwrap_or_default();
|
||
let options = if block.role == Role::Rest || !motif.notes.is_empty() {
|
||
realize(
|
||
&motif,
|
||
block.role,
|
||
block.bars,
|
||
offset,
|
||
last,
|
||
block.take,
|
||
&self.settings,
|
||
&self.harmony,
|
||
)
|
||
.unwrap_or_default()
|
||
} else {
|
||
Vec::new()
|
||
};
|
||
let block = &mut self.blocks[index];
|
||
block.choice = block.choice.min(options.len().saturating_sub(1));
|
||
block.options = options;
|
||
}
|
||
/// Realizes every block that is not kept. Kept blocks are realized only when
|
||
/// their stored options no longer fit (for example after a meter change).
|
||
pub fn realize_all(&mut self) {
|
||
let bar = self.settings.meter.bar();
|
||
for index in 0..self.blocks.len() {
|
||
let block = &self.blocks[index];
|
||
let fits = block
|
||
.phrase()
|
||
.is_some_and(|p| p.length == block.bars as u32 * bar);
|
||
if !block.kept || !fits {
|
||
self.realize(index);
|
||
}
|
||
}
|
||
}
|
||
/// The whole storyboard as one phrase, with joins applied at every seam.
|
||
pub fn compose(&self) -> Result<Phrase> {
|
||
if self.blocks.is_empty() {
|
||
return Err(err("Add blocks to the storyboard first."));
|
||
}
|
||
let mut notes = Vec::new();
|
||
let mut offset = 0;
|
||
for (i, block) in self.blocks.iter().enumerate() {
|
||
let length = block.bars as u32 * self.settings.meter.bar();
|
||
let mut local = block.phrase().map(|p| p.notes.clone()).unwrap_or_default();
|
||
let next = self
|
||
.blocks
|
||
.get(i + 1)
|
||
.and_then(Block::phrase)
|
||
.and_then(|p| p.notes.first().copied());
|
||
if i + 1 < self.blocks.len() {
|
||
self.join(&mut local, length, block.join, next);
|
||
}
|
||
notes.extend(local.into_iter().map(|n| Note {
|
||
start: n.start + offset,
|
||
..n
|
||
}));
|
||
offset += length;
|
||
}
|
||
if notes.is_empty() {
|
||
return Err(err("The storyboard is silent. Add a block with notes."));
|
||
}
|
||
let phrase = Phrase {
|
||
notes,
|
||
length: offset,
|
||
};
|
||
phrase.validate(MAX_PLAYBACK_NOTES)?;
|
||
Ok(phrase)
|
||
}
|
||
/// Applies a hand-over to the end of one block. Never touches locked attributes
|
||
/// and never moves notes across the block boundary.
|
||
fn join(&self, notes: &mut Vec<Note>, length: u32, join: Join, next: Option<Note>) {
|
||
let beat = self.settings.meter.beat();
|
||
let Some(last) = notes.last().copied() else {
|
||
return;
|
||
};
|
||
match join {
|
||
Join::Cut => {}
|
||
Join::Breath => {
|
||
let limit = length - beat / 2;
|
||
let n = notes.last_mut().unwrap();
|
||
if !n.keeps_rhythm() && n.start < limit && n.end() > limit {
|
||
n.duration = limit - n.start;
|
||
}
|
||
}
|
||
Join::Approach => {
|
||
let Some(target) = next else { return };
|
||
let n = notes.last_mut().unwrap();
|
||
if n.keeps_pitch() {
|
||
return;
|
||
}
|
||
let below = self.settings.degree(target.pitch, -1);
|
||
let above = self.settings.degree(target.pitch, 1);
|
||
n.pitch = if n.pitch > target.pitch && above != target.pitch {
|
||
above
|
||
} else if below != target.pitch {
|
||
below
|
||
} else {
|
||
above
|
||
};
|
||
}
|
||
Join::Pickup => {
|
||
let Some(target) = next else { return };
|
||
let region = length - beat;
|
||
if notes.iter().any(|n| n.end() > region && n.is_anchor()) {
|
||
return;
|
||
}
|
||
notes.retain(|n| n.start < region);
|
||
if let Some(n) = notes.last_mut() {
|
||
if n.end() > region {
|
||
n.duration = region - n.start;
|
||
}
|
||
}
|
||
let reference = notes.last().unwrap_or(&last).pitch;
|
||
let direction: i16 = if reference > target.pitch { 1 } else { -1 };
|
||
let half = beat / 2;
|
||
for (k, steps) in [(0, 2 * direction), (1, direction)] {
|
||
notes.push(Note {
|
||
start: region + k * half,
|
||
duration: half,
|
||
pitch: self.settings.degree(target.pitch, steps),
|
||
velocity: (target.velocity as i16 - 18 + 8 * k as i16).clamp(1, 127) as u8,
|
||
..Note::default()
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
pub fn validate(&self) -> Result<()> {
|
||
if self.version != STATE_VERSION {
|
||
return Err(err("Unsupported session version."));
|
||
}
|
||
self.settings.validate()?;
|
||
self.performance.validate()?;
|
||
self.harmony.validate()?;
|
||
if !self.tempo.is_finite() || !(30.0..=300.0).contains(&self.tempo) {
|
||
return Err(err("Tempo must be between 30 and 300 BPM."));
|
||
}
|
||
if self.motifs.len() > MAX_MOTIFS || self.blocks.len() > MAX_BLOCKS {
|
||
return Err(err("Too many motifs or blocks."));
|
||
}
|
||
let mut ids = std::collections::BTreeSet::new();
|
||
for m in &self.motifs {
|
||
if m.id >= self.next_id || !ids.insert(m.id) || m.name.chars().count() > 60 {
|
||
return Err(err("Invalid motif."));
|
||
}
|
||
self.check_motif(&m.phrase)?;
|
||
}
|
||
let bar = self.settings.meter.bar();
|
||
for b in &self.blocks {
|
||
if b.id >= self.next_id
|
||
|| !ids.insert(b.id)
|
||
|| !(1..=MAX_MOTIF_BARS as u8).contains(&b.bars)
|
||
|| (b.role != Role::Rest && self.motif(b.motif).is_none())
|
||
|| b.options.len() > 3
|
||
|| (!b.options.is_empty() && b.choice >= b.options.len())
|
||
{
|
||
return Err(err("Invalid storyboard block."));
|
||
}
|
||
for option in &b.options {
|
||
option.phrase.check(MAX_PLAYBACK_NOTES, true)?;
|
||
if option.phrase.length != b.bars as u32 * bar {
|
||
return Err(err("A block option does not match its length."));
|
||
}
|
||
}
|
||
}
|
||
if self.total_bars() > MAX_BARS {
|
||
return Err(Error(format!(
|
||
"The storyboard is limited to {MAX_BARS} bars."
|
||
)));
|
||
}
|
||
Ok(())
|
||
}
|
||
pub fn from_json(bytes: &[u8]) -> Result<Self> {
|
||
if bytes.len() > MAX_STATE_BYTES {
|
||
return Err(err("Session exceeds the 16 MiB limit."));
|
||
}
|
||
let mut session: Self = serde_json::from_slice(bytes).map_err(|e| Error(e.to_string()))?;
|
||
session.migrate()?;
|
||
session.validate()?;
|
||
Ok(session)
|
||
}
|
||
pub fn to_json(&self) -> Result<Vec<u8>> {
|
||
self.validate()?;
|
||
let bytes = serde_json::to_vec(self).map_err(|e| Error(e.to_string()))?;
|
||
if bytes.len() > MAX_STATE_BYTES {
|
||
return Err(err("Session exceeds the 16 MiB limit."));
|
||
}
|
||
Ok(bytes)
|
||
}
|
||
/// The demo motif and an arc storyboard built from it.
|
||
pub fn demo() -> Self {
|
||
let mut s = Self::default();
|
||
let id = s
|
||
.add_motif("Demo motif", crate::demo_motif())
|
||
.expect("valid demo");
|
||
s.apply_template(Template::Arc, id).expect("valid demo");
|
||
s
|
||
}
|
||
}
|