use serde::{Deserialize, Serialize};
use crate::catalog::{Voice, VoiceParams};
use crate::dsl::{Adsr, ENGINE_VERSION, Node, SeqNote, SeqWave, SoundDoc, Track, 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,
}
}
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 {
self.tracks.push(SongTrack {
name: name.into(),
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
}
pub fn length_bars(&self) -> u32 {
let steps_per_bar = (self.steps_per_beat * self.beats_per_bar).max(1);
let from_patterns = self
.arrangement
.iter()
.map(|pl| {
let bars = self
.patterns
.iter()
.find(|p| p.name == pl.pattern)
.map(|p| p.bars)
.unwrap_or(0);
pl.bar + bars
})
.max()
.unwrap_or(0);
let from_notes = self
.tracks
.iter()
.flat_map(|t| t.notes.iter())
.map(|n| (n.step + n.len).div_ceil(steps_per_bar))
.max()
.unwrap_or(0);
from_patterns.max(from_notes)
}
pub fn to_doc(&self) -> Result<SoundDoc, String> {
if self.tracks.is_empty() {
return Err("song has no tracks".into());
}
for pl in &self.arrangement {
if !self.tracks.iter().any(|t| t.name == pl.track) {
return Err(format!(
"arrangement references unknown track '{}'",
pl.track
));
}
if !self.patterns.iter().any(|p| p.name == pl.pattern) {
return Err(format!(
"arrangement references unknown pattern '{}'",
pl.pattern
));
}
}
let steps_per_bar = self.beats_per_bar.max(1) * self.steps_per_beat.max(1);
let sec_per_step = 60.0 / (self.bpm.max(1.0) * self.steps_per_beat.max(1) as f32);
let mut end_step = 0u32;
let mut doc_tracks = Vec::with_capacity(self.tracks.len());
for t in &self.tracks {
let mut notes: Vec<SeqNote> = t.notes.clone();
for n in ¬es {
end_step = end_step.max(n.step + n.len.max(1));
}
for pl in self.arrangement.iter().filter(|p| p.track == t.name) {
let pat = self
.patterns
.iter()
.find(|p| p.name == pl.pattern)
.expect("pattern existence checked above");
let offset = pl.bar * steps_per_bar;
for n in &pat.notes {
let step = n.step + offset;
end_step = end_step.max(step + n.len.max(1));
notes.push(SeqNote {
step,
len: n.len,
pitch: n.pitch.clone(),
gain: n.gain,
});
}
}
notes.sort_by_key(|n| n.step);
let mut seq_json = serde_json::json!({
"type": "seq",
"bpm": self.bpm,
"steps_per_beat": self.steps_per_beat,
"wave": serde_json::to_value(t.wave).map_err(|e| e.to_string())?,
"env": serde_json::to_value(t.env).map_err(|e| e.to_string())?,
"swing": t.swing.unwrap_or(self.swing),
"humanize": t.humanize.unwrap_or(self.humanize),
"sf2": t.sf2,
"sf2_preset": t.sf2_preset,
"sf2_bank": t.sf2_bank,
"notes": serde_json::to_value(¬es).map_err(|e| e.to_string())?,
});
if let Some(k) = t.voice.kit {
seq_json["kit"] = serde_json::to_value(k).map_err(|e| e.to_string())?;
}
for (key, val) in [
("duty", t.voice.duty),
("fm_ratio", t.voice.fm_ratio),
("fm_index", t.voice.fm_index),
("fm_strike", t.voice.fm_strike),
("pluck_decay", t.voice.pluck_decay),
("pluck_body", t.voice.pluck_body),
("pluck_pick", t.voice.pluck_pick),
("pluck_tone", t.voice.pluck_tone),
("piano_hammer", t.voice.piano_hammer),
("piano_strike", t.voice.piano_strike),
("piano_inharm", t.voice.piano_inharm),
("piano_detune", t.voice.piano_detune),
("piano_decay", t.voice.piano_decay),
("bass_cutoff", t.voice.bass_cutoff),
("bass_env", t.voice.bass_env),
("bass_env_vel", t.voice.bass_env_vel),
("bass_decay", t.voice.bass_decay),
("bass_click", t.voice.bass_click),
("bass_body", t.voice.bass_body),
("bass_sub", t.voice.bass_sub),
("bass_sub_ratio", t.voice.bass_sub_ratio),
("bass_drive", t.voice.bass_drive),
("bass_body_decay", t.voice.bass_body_decay),
] {
if let Some(v) = val {
seq_json[key] = serde_json::json!(v);
}
}
let seq: Node = serde_json::from_value(seq_json)
.map_err(|e| format!("track '{}' seq build: {e}", t.name))?;
let node = if t.reverb > 0.0 {
let rv = t.reverb.clamp(0.0, 1.0);
Node::Chain {
stages: vec![
seq,
Node::Reverb {
room: 0.6,
mix: 0.5 * rv,
},
],
}
} else {
seq
};
doc_tracks.push(Track {
id: Some(t.name.clone()),
node,
pan: t.pan,
gain: t.gain,
at: 0.0,
mute: false,
automation: Vec::new(),
});
}
let duration = end_step as f32 * sec_per_step + 2.0; let root = Node::Tracks {
tracks: doc_tracks,
master: self.master.clone(),
};
let mut json = serde_json::json!({
"name": self.name,
"duration": duration,
"engine": self.engine.unwrap_or(ENGINE_VERSION),
"root": serde_json::to_value(&root).map_err(|e| e.to_string())?,
});
if let Some(v) = self.version {
json["version"] = serde_json::json!(v);
}
let doc: SoundDoc =
serde_json::from_value(json).map_err(|e| format!("song doc build: {e}"))?;
Ok(doc)
}
}
pub struct Phrase {
steps_per_beat: u32,
cursor_beat: f32,
velocity: f32,
notes: Vec<SeqNote>,
}
impl Phrase {
fn new(steps_per_beat: u32) -> Self {
Phrase {
steps_per_beat: steps_per_beat.max(1),
cursor_beat: 0.0,
velocity: 1.0,
notes: Vec::new(),
}
}
fn step_of(&self, beat: f32) -> u32 {
(beat.max(0.0) * self.steps_per_beat as f32).round() as u32
}
fn len_of(&self, dur_beats: f32) -> u32 {
((dur_beats.max(0.0) * self.steps_per_beat as f32).round() as u32).max(1)
}
pub fn at(&mut self, beat: f32) -> &mut Self {
self.cursor_beat = beat;
self
}
pub fn vel(&mut self, velocity: f32) -> &mut Self {
self.velocity = velocity.clamp(0.0, 1.0);
self
}
pub fn note(&mut self, pitch: &str, dur_beats: f32) -> &mut Self {
self.push(pitch, dur_beats);
self
}
pub fn chord(&mut self, pitches: &[&str], dur_beats: f32) -> &mut Self {
for p in pitches {
self.push(p, dur_beats);
}
self
}
pub fn play(&mut self, pitch: &str, dur_beats: f32) -> &mut Self {
self.push(pitch, dur_beats);
self.cursor_beat += dur_beats;
self
}
pub fn rest(&mut self, dur_beats: f32) -> &mut Self {
self.cursor_beat += dur_beats;
self
}
fn push(&mut self, pitch: &str, dur_beats: f32) {
self.notes.push(SeqNote {
step: self.step_of(self.cursor_beat),
len: self.len_of(dur_beats),
pitch: Value::Note(pitch.to_string()),
gain: self.velocity,
});
}
pub fn hit(&mut self, gm_note: u8) -> &mut Self {
self.notes.push(SeqNote {
step: self.step_of(self.cursor_beat),
len: 1,
pitch: Value::Note(format!("midi:{gm_note}")),
gain: self.velocity,
});
self
}
pub fn kick(&mut self) -> &mut Self {
self.hit(36)
}
pub fn snare(&mut self) -> &mut Self {
self.hit(38)
}
pub fn hat(&mut self) -> &mut Self {
self.hit(42)
}
pub fn open_hat(&mut self) -> &mut Self {
self.hit(46)
}
pub fn clap(&mut self) -> &mut Self {
self.hit(39)
}
pub fn crash(&mut self) -> &mut Self {
self.hit(49)
}
pub fn ride(&mut self) -> &mut Self {
self.hit(51)
}
pub fn tom(&mut self) -> &mut Self {
self.hit(45)
}
}
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!(a.to_doc().unwrap_err().contains("unknown track"));
let mut b = Song::new("s", 120.0);
b.add_track("t", SeqWave::Sine, amp());
b.arrange("t", "ghost", 0);
assert!(b.to_doc().unwrap_err().contains("unknown pattern"));
}
#[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_decay, .. } = &tracks[0].node else {
panic!("seq");
};
assert!((*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 amp = Adsr {
a: 0.005,
d: 0.1,
s: 0.8,
r: 0.2,
punch: 0.0,
};
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 amp = Adsr {
a: 0.005,
d: 0.1,
s: 0.8,
r: 0.2,
punch: 0.0,
};
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);
}
}
#[cfg(test)]
mod slug_tests {
use super::slugify;
#[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");
}
}