Files
bass-foundry/plugins/rivet/core/src/harmony.rs
T
nvrl f1366280ff
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
feat(rivet): storyboard composition with flexible lengths and meters
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.
2026-11-06 02:15:18 +01:00

176 lines
5.4 KiB
Rust

use crate::{err, Meter, Note, Result, Scale, MAX_LENGTH, MAX_PLAYBACK_NOTES};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ChordQuality {
Major,
Minor,
Major7,
Minor7,
Sus2,
Sus4,
}
impl ChordQuality {
pub const ALL: [Self; 6] = [
Self::Major,
Self::Minor,
Self::Major7,
Self::Minor7,
Self::Sus2,
Self::Sus4,
];
pub fn name(self) -> &'static str {
match self {
Self::Major => "Major",
Self::Minor => "Minor",
Self::Major7 => "Major 7",
Self::Minor7 => "Minor 7",
Self::Sus2 => "Sus 2",
Self::Sus4 => "Sus 4",
}
}
pub fn mask(self, root: u8) -> u16 {
let intervals: &[u8] = match self {
Self::Major => &[0, 4, 7],
Self::Minor => &[0, 3, 7],
Self::Major7 => &[0, 4, 7, 11],
Self::Minor7 => &[0, 3, 7, 10],
Self::Sus2 => &[0, 2, 7],
Self::Sus4 => &[0, 5, 7],
};
intervals
.iter()
.fold(0, |mask, i| mask | 1 << ((root + i) % 12))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HarmonySpan {
pub start: u32,
pub duration: u32,
pub tones: u16,
pub bass: u8,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct Harmony {
pub length: u32,
pub spans: Vec<HarmonySpan>,
}
impl Harmony {
pub fn validate(&self) -> Result<()> {
if self.spans.is_empty() {
return if self.length == 0 {
Ok(())
} else {
Err(err("Empty harmony must have zero length."))
};
}
if self.length == 0 || self.length > MAX_LENGTH || self.spans.len() > 256 {
return Err(err("Harmony must fit 32 bars and at most 256 changes."));
}
let mut end = 0;
for span in &self.spans {
if span.duration == 0
|| span.start < end
|| span.start.saturating_add(span.duration) > self.length
|| span.tones == 0
|| span.tones > 0xfff
|| span.bass > 127
{
return Err(err("Invalid or overlapping harmonic regions."));
}
end = span.start + span.duration;
}
Ok(())
}
pub fn at(&self, tick: u32) -> Option<&HarmonySpan> {
if self.length == 0 {
return None;
}
let tick = tick % self.length;
self.spans
.iter()
.find(|s| tick >= s.start && tick < s.start + s.duration)
}
pub fn from_midi(notes: &[Note], length: u32) -> Result<Self> {
if notes.is_empty()
|| notes.len() > MAX_PLAYBACK_NOTES
|| length == 0
|| length > MAX_LENGTH
{
return Err(err("Import a harmony track of up to 32 bars."));
}
let mut edges = std::collections::BTreeSet::new();
for n in notes {
if n.pitch > 127 || n.duration == 0 || n.end() > length {
return Err(err("Invalid harmony note."));
}
edges.insert(n.start);
edges.insert(n.end());
}
let edges: Vec<_> = edges.into_iter().collect();
let mut spans: Vec<HarmonySpan> = Vec::new();
for pair in edges.windows(2) {
let active: Vec<_> = notes
.iter()
.filter(|n| n.start <= pair[0] && n.end() > pair[0])
.collect();
if active.is_empty() {
continue;
}
let tones = active.iter().fold(0, |mask, n| mask | 1 << (n.pitch % 12));
let bass = active.iter().map(|n| n.pitch).min().unwrap();
if let Some(last) = spans.last_mut() {
if last.start + last.duration == pair[0] && last.tones == tones && last.bass == bass
{
last.duration += pair[1] - pair[0];
continue;
}
}
spans.push(HarmonySpan {
start: pair[0],
duration: pair[1] - pair[0],
tones,
bass,
});
}
let context = Self { length, spans };
context.validate()?;
Ok(context)
}
pub fn progression(root: u8, scale: Scale, meter: Meter) -> Self {
let minor = scale.minor();
let bar = meter.bar();
let chords = if minor {
[
(0, ChordQuality::Minor),
(8, ChordQuality::Major),
(3, ChordQuality::Major),
(10, ChordQuality::Major),
]
} else {
[
(0, ChordQuality::Major),
(9, ChordQuality::Minor),
(5, ChordQuality::Major),
(7, ChordQuality::Major),
]
};
Self {
length: 4 * bar,
spans: chords
.into_iter()
.enumerate()
.map(|(i, (step, quality))| {
let root = (root + step) % 12;
HarmonySpan {
start: i as u32 * bar,
duration: bar,
tones: quality.mask(root),
bass: 48 + root,
}
})
.collect(),
}
}
}