mod compile;
mod phrase;
pub use phrase::Phrase;
use serde::{Deserialize, Serialize};
use crate::catalog::{Voice, VoiceParams};
use crate::dsl::{Adsr, ENGINE_VERSION, Node, SeqNote, SeqWave, Value};
#[non_exhaustive]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SongTrack {
pub name: String,
pub wave: SeqWave,
pub env: Adsr,
#[serde(default = "unit_gain")]
pub gain: f32,
#[serde(default)]
pub pan: f32,
#[serde(default)]
pub sf2: String,
#[serde(default)]
pub sf2_preset: u32,
#[serde(default)]
pub sf2_bank: u32,
#[serde(default)]
pub notes: Vec<SeqNote>,
#[serde(default)]
pub voice: VoiceParams,
#[serde(default)]
pub reverb: f32,
#[serde(default)]
pub swing: Option<f32>,
#[serde(default)]
pub humanize: Option<f32>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Pattern {
pub name: String,
pub bars: u32,
pub notes: Vec<SeqNote>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Placement {
pub track: String,
pub pattern: String,
pub bar: u32,
}
#[non_exhaustive]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Song {
pub name: String,
pub bpm: f32,
#[serde(default = "default_steps_per_beat")]
pub steps_per_beat: u32,
#[serde(default = "default_beats_per_bar")]
pub beats_per_bar: u32,
#[serde(default)]
pub swing: f32,
#[serde(default)]
pub humanize: f32,
pub tracks: Vec<SongTrack>,
pub patterns: Vec<Pattern>,
pub arrangement: Vec<Placement>,
#[serde(default)]
pub master: Vec<Node>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub engine: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<u32>,
}
pub fn note(step: u32, len: u32, pitch: &str) -> SeqNote {
note_vel(step, len, pitch, 1.0)
}
pub fn note_vel(step: u32, len: u32, pitch: &str, gain: f32) -> SeqNote {
SeqNote {
step,
len,
pitch: Value::Note(pitch.to_string()),
gain,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SongError {
Empty,
UnknownTrack(String),
UnknownPattern(String),
Compile(String),
}
impl std::fmt::Display for SongError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SongError::Empty => f.write_str("song has no tracks"),
SongError::UnknownTrack(t) => {
write!(f, "arrangement references unknown track '{t}'")
}
SongError::UnknownPattern(p) => {
write!(f, "arrangement references unknown pattern '{p}'")
}
SongError::Compile(e) => f.write_str(e),
}
}
}
impl std::error::Error for SongError {}
impl From<SongError> for String {
fn from(e: SongError) -> String {
e.to_string()
}
}
impl Song {
pub fn new(name: impl Into<String>, bpm: f32) -> Self {
Song {
name: name.into(),
bpm,
steps_per_beat: default_steps_per_beat(),
beats_per_bar: default_beats_per_bar(),
swing: 0.0,
humanize: 0.0,
tracks: Vec::new(),
patterns: Vec::new(),
arrangement: Vec::new(),
master: Vec::new(),
engine: Some(ENGINE_VERSION),
version: Some(crate::dsl::SCHEMA_VERSION),
}
}
pub fn add_track(&mut self, name: impl Into<String>, wave: SeqWave, env: Adsr) -> &mut Self {
let name = self.unique_name(&slugify(&name.into()));
self.tracks.push(SongTrack {
name,
wave,
env,
gain: 1.0,
pan: 0.0,
sf2: String::new(),
sf2_preset: 0,
sf2_bank: 0,
notes: Vec::new(),
voice: VoiceParams::default(),
reverb: 0.0,
swing: None,
humanize: None,
});
self
}
pub fn add(mut self, instrument: Voice, write: impl FnOnce(&mut Phrase)) -> Self {
let mut phrase = Phrase::new(self.steps_per_beat);
write(&mut phrase);
let name = self.unique_name(&slugify(&instrument.name));
self.tracks.push(SongTrack {
name,
wave: instrument.wave,
env: instrument.env,
gain: instrument.gain,
pan: instrument.pan,
sf2: String::new(),
sf2_preset: 0,
sf2_bank: 0,
notes: phrase.notes,
voice: instrument.voice,
reverb: instrument.reverb,
swing: instrument.swing,
humanize: instrument.humanize,
});
self
}
fn unique_name(&self, base: &str) -> String {
if !self.tracks.iter().any(|t| t.name == base) {
return base.to_string();
}
(2..)
.map(|i| format!("{base}_{i}"))
.find(|n| !self.tracks.iter().any(|t| &t.name == n))
.expect("an unused suffix always exists")
}
pub fn add_pattern(
&mut self,
name: impl Into<String>,
bars: u32,
notes: Vec<SeqNote>,
) -> &mut Self {
self.patterns.push(Pattern {
name: name.into(),
bars: bars.max(1),
notes,
});
self
}
pub fn arrange(&mut self, track: impl Into<String>, pattern: impl Into<String>, bar: u32) {
self.arrangement.push(Placement {
track: track.into(),
pattern: pattern.into(),
bar,
});
}
pub fn arrange_repeat(&mut self, track: &str, pattern: &str, start_bar: u32, times: u32) {
let stride = self
.patterns
.iter()
.find(|p| p.name == pattern)
.map(|p| p.bars)
.unwrap_or(1);
for i in 0..times {
self.arrange(track, pattern, start_bar + i * stride);
}
}
pub fn with_master(mut self, master: Vec<Node>) -> Self {
self.master = master;
self
}
}
fn slugify(name: &str) -> String {
let mut s = String::with_capacity(name.len());
let mut pending_us = false;
for c in name.chars() {
if c.is_ascii_alphanumeric() {
if pending_us && !s.is_empty() {
s.push('_');
}
s.push(c.to_ascii_lowercase());
pending_us = false;
} else {
pending_us = true;
}
}
if s.is_empty() {
s.push_str("track");
}
s
}
fn unit_gain() -> f32 {
1.0
}
fn default_steps_per_beat() -> u32 {
4
}
fn default_beats_per_bar() -> u32 {
4
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render;
fn amp() -> Adsr {
Adsr {
a: 0.005,
d: 0.1,
s: 0.8,
r: 0.2,
punch: 0.0,
}
}
fn peak(s: &[f32]) -> f32 {
s.iter().fold(0.0f32, |m, &x| m.max(x.abs()))
}
#[test]
fn compiles_and_renders_a_two_track_song() {
let mut song = Song::new("demo", 120.0);
song.add_track("bass", SeqWave::Bass, amp());
song.add_track("drums", SeqWave::Kit, amp());
song.add_pattern("bassline", 1, vec![note(0, 4, "C2"), note(8, 4, "G2")]);
song.add_pattern(
"beat",
1,
vec![note(0, 2, "midi:36"), note(8, 2, "midi:38")],
);
song.arrange_repeat("bass", "bassline", 0, 2);
song.arrange_repeat("drums", "beat", 0, 2);
assert_eq!(song.length_bars(), 2);
let doc = song.to_doc().unwrap();
assert!(matches!(&doc.root, Node::Tracks { tracks, .. } if tracks.len() == 2));
let out = render::render(&doc);
assert!(peak(&out) > 0.0, "the song makes sound");
assert_eq!(render::render(&song.to_doc().unwrap()), out);
}
#[test]
fn pattern_places_at_the_right_bar() {
let mut song = Song::new("s", 120.0);
song.add_track("lead", SeqWave::Square, amp());
song.add_pattern("p", 1, vec![note(0, 1, "C4")]);
song.arrange("lead", "p", 0);
song.arrange("lead", "p", 2); let doc = song.to_doc().unwrap();
let Node::Tracks { tracks, .. } = &doc.root else {
panic!("tracks root");
};
let Node::Seq { notes, .. } = &tracks[0].node else {
panic!("seq track");
};
assert_eq!(
notes.iter().map(|n| n.step).collect::<Vec<_>>(),
vec![0, 32]
);
}
#[test]
fn rejects_unknown_references() {
let mut a = Song::new("s", 120.0);
a.add_track("t", SeqWave::Sine, amp());
a.add_pattern("p", 1, vec![note(0, 1, "C4")]);
a.arrange("nope", "p", 0);
assert_eq!(
a.to_doc().unwrap_err(),
SongError::UnknownTrack("nope".into())
);
let mut b = Song::new("s", 120.0);
b.add_track("t", SeqWave::Sine, amp());
b.arrange("t", "ghost", 0);
assert_eq!(
b.to_doc().unwrap_err(),
SongError::UnknownPattern("ghost".into())
);
}
#[test]
fn round_trips_through_serde() {
let mut song = Song::new("s", 128.0);
song.add_track("bass", SeqWave::Bass, amp());
song.add_pattern("r", 1, vec![note(0, 4, "C2")]);
song.arrange("bass", "r", 0);
let json = serde_json::to_string(&song).unwrap();
let back: Song = serde_json::from_str(&json).unwrap();
assert!(back.to_doc().is_ok(), "a saved song reloads and compiles");
}
#[test]
fn fluent_add_places_notes_on_the_beat_grid() {
use crate::catalog::{Drums, GrandPiano};
let song = Song::new("demo", 120.0)
.add(GrandPiano::grand(), |t| {
t.at(0.0).note("C4", 1.0).at(1.0).note("E4", 1.0);
})
.add(Drums::acoustic(), |t| {
t.at(0.0).kick().at(0.5).hat();
});
let doc = song.to_doc().unwrap();
let Node::Tracks { tracks, .. } = &doc.root else {
panic!("tracks root");
};
assert_eq!(tracks.len(), 2);
let Node::Seq { notes, .. } = &tracks[0].node else {
panic!("seq");
};
assert_eq!(notes.iter().map(|n| n.step).collect::<Vec<_>>(), vec![0, 4]);
let Node::Seq { notes: drums, .. } = &tracks[1].node else {
panic!("seq");
};
assert_eq!(drums.iter().map(|n| n.step).collect::<Vec<_>>(), vec![0, 2]);
}
#[test]
fn fluent_song_renders_deterministically() {
use crate::catalog::{Bass, GrandPiano};
let build = || {
Song::new("tune", 100.0)
.add(GrandPiano::grand(), |t| {
t.play("C4", 1.0).play("E4", 1.0).play("G4", 1.0);
})
.add(Bass::finger(), |t| {
t.at(0.0).note("C2", 3.0);
})
.to_doc()
.unwrap()
};
let a = render::render(&build());
assert!(peak(&a) > 0.0, "the fluent song makes sound");
assert_eq!(render::render(&build()), a, "byte-identical every render");
}
#[test]
fn guitar_voice_param_reaches_the_seq() {
use crate::catalog::Guitar;
let doc = Song::new("g", 120.0)
.add(Guitar::steel(), |t| {
t.at(0.0).note("E3", 2.0);
})
.to_doc()
.unwrap();
let Node::Tracks { tracks, .. } = &doc.root else {
panic!("tracks");
};
let Node::Seq { pluck, .. } = &tracks[0].node else {
panic!("seq");
};
assert!(
(pluck.pluck_decay - 0.965).abs() < 1e-6,
"steel pluck_decay set"
);
}
#[test]
fn duplicate_instrument_names_are_disambiguated() {
use crate::catalog::GrandPiano;
let song = Song::new("two pianos", 120.0)
.add(GrandPiano::grand(), |t| {
t.at(0.0).note("C4", 1.0);
})
.add(GrandPiano::grand(), |t| {
t.at(0.0).note("E4", 1.0);
});
assert_eq!(song.tracks[0].name, "grand_piano");
assert_eq!(song.tracks[1].name, "grand_piano_2");
}
#[test]
fn per_track_reverb_wraps_and_is_dry_by_default() {
use crate::catalog::GrandPiano;
let dry = Song::new("s", 100.0)
.add(GrandPiano::grand(), |t| {
t.at(0.0).note("C4", 1.0);
})
.to_doc()
.unwrap();
let Node::Tracks { tracks, .. } = &dry.root else {
panic!("tracks")
};
assert!(
matches!(&tracks[0].node, Node::Seq { .. }),
"dry = bare seq"
);
let wet = Song::new("s", 100.0)
.add(GrandPiano::grand().reverb(0.5), |t| {
t.at(0.0).note("C4", 1.0);
})
.to_doc()
.unwrap();
let Node::Tracks { tracks, .. } = &wet.root else {
panic!("tracks")
};
let Node::Chain { stages } = &tracks[0].node else {
panic!("reverb wraps the seq in a chain")
};
assert!(matches!(stages[0], Node::Seq { .. }));
assert!(matches!(stages[1], Node::Reverb { .. }));
assert!(
render::render(&wet).iter().any(|&x| x != 0.0),
"wet song sounds"
);
}
#[test]
fn per_track_swing_overrides_the_song_swing() {
use crate::catalog::Bass;
let doc = Song::new("s", 120.0) .add(Bass::finger().swing(0.6), |t| {
t.at(0.0).note("C2", 1.0).at(1.0).note("G1", 1.0);
})
.to_doc()
.unwrap();
let Node::Tracks { tracks, .. } = &doc.root else {
panic!("tracks")
};
let Node::Seq { swing, .. } = &tracks[0].node else {
panic!("seq")
};
assert!(
(*swing - 0.6).abs() < 1e-6,
"track swing overrides the song's"
);
}
#[test]
fn catalog_names_become_valid_layer_id_slugs() {
use crate::catalog::{Drums, Guitar, Strings};
let doc = Song::new("s", 100.0)
.add(Strings::warm(), |t| {
t.at(0.0).chord(&["C4", "E4"], 4.0);
})
.add(Guitar::steel(), |t| {
t.at(0.0).note("E3", 4.0);
})
.add(Drums::tr808(), |t| {
t.at(0.0).kick();
})
.to_doc()
.unwrap();
assert!(
doc.validate().is_ok(),
"catalog song validates: {:?}",
doc.validate()
);
let Node::Tracks { tracks, .. } = &doc.root else {
panic!("tracks");
};
for t in tracks {
let id = t.id.as_deref().unwrap();
assert!(
id.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'),
"layer id '{id}' is a slug"
);
}
}
#[test]
fn song_pins_engine_and_version_at_creation() {
let mut song = Song::new("pinned", 120.0);
song.add_track("bass", SeqWave::Bass, amp());
song.tracks[0].notes.push(note(0, 4, "C2"));
assert_eq!(song.engine, Some(ENGINE_VERSION));
let doc = song.to_doc().unwrap();
assert_eq!(doc.engine, Some(ENGINE_VERSION));
assert_eq!(doc.version, Some(crate::dsl::SCHEMA_VERSION));
song.engine = Some(3);
assert_eq!(song.to_doc().unwrap().engine, Some(3));
let mut v = serde_json::to_value(&song).unwrap();
v.as_object_mut().unwrap().remove("engine");
v.as_object_mut().unwrap().remove("version");
let legacy: Song = serde_json::from_value(v).unwrap();
let doc = legacy.to_doc().unwrap();
assert_eq!(doc.engine, Some(ENGINE_VERSION));
assert_eq!(doc.version, None);
}
#[test]
fn length_bars_counts_direct_track_notes() {
let mut song = Song::new("fluent", 120.0); song.add_track("bass", SeqWave::Bass, amp());
assert_eq!(song.length_bars(), 0);
song.tracks[0].notes.push(note(17, 4, "C2")); assert_eq!(song.length_bars(), 2);
}
#[test]
fn slugifies_names() {
assert_eq!(slugify("Mellow Piano"), "mellow_piano");
assert_eq!(slugify("808 drums"), "808_drums");
assert_eq!(slugify("steel guitar"), "steel_guitar");
assert_eq!(slugify(" !! "), "track");
}
#[test]
fn pathological_step_values_saturate_instead_of_wrapping() {
let mut song = Song::new("s", 120.0);
song.add_track("t", SeqWave::Square, amp());
song.tracks[0].notes.push(note(u32::MAX, 4, "C4"));
let doc = song.to_doc().unwrap(); assert!(doc.duration.is_finite());
let mut song = Song::new("s", 120.0);
song.add_track("t", SeqWave::Square, amp());
song.add_pattern("p", 1, vec![note(0, 1, "C4")]);
song.arrange("t", "p", u32::MAX);
let doc = song.to_doc().unwrap();
assert!(doc.duration.is_finite());
}
#[test]
fn add_track_slugifies_and_dedups_names() {
let mut song = Song::new("s", 120.0);
song.add_track("My Bass", SeqWave::Bass, amp());
song.add_track("My Bass", SeqWave::Bass, amp());
assert_eq!(song.tracks[0].name, "my_bass");
assert_eq!(song.tracks[1].name, "my_bass_2");
}
#[test]
fn degenerate_bpm_keeps_duration_and_placement_consistent() {
let mut song = Song::new("s", 0.5);
song.add_track("t", SeqWave::Sine, amp());
song.tracks[0].notes.push(note(0, 2, "C4"));
song.tracks[0].notes.push(note(16, 2, "C4")); let doc = song.to_doc().unwrap();
doc.validate().unwrap();
let Node::Tracks { tracks, .. } = &doc.root else {
panic!("tracks root");
};
let Node::Seq { bpm, .. } = &tracks[0].node else {
panic!("a reverb-less track compiles to a bare seq");
};
assert_eq!(*bpm, 1.0, "the seq plays at the clamped bpm");
let expected = 18.0 * (60.0 / 4.0) + 2.0; assert!(
(doc.duration - expected).abs() < 1e-3,
"duration matches the clamped bpm: {}",
doc.duration
);
}
}