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.
176 lines
5.4 KiB
Rust
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(),
|
|
}
|
|
}
|
|
}
|