use serde::{Deserialize, Serialize};
use crate::catalog::{Instrument, VoiceParams};
use crate::dsl::{Adsr, ENGINE_VERSION, Node, SeqNote, SeqWave, SoundDoc, Track, Value};
#[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,
}
#[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,
}
#[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>,
}
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(),
}
}
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(),
});
self
}
pub fn add(mut self, instrument: Instrument, 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,
});
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 {
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)
}
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": self.swing,
"humanize": 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(v) = t.voice.duty {
seq_json["duty"] = serde_json::json!(v);
}
if let Some(v) = t.voice.fm_ratio {
seq_json["fm_ratio"] = serde_json::json!(v);
}
if let Some(v) = t.voice.fm_index {
seq_json["fm_index"] = serde_json::json!(v);
}
if let Some(v) = t.voice.fm_strike {
seq_json["fm_strike"] = serde_json::json!(v);
}
if let Some(v) = t.voice.pluck_decay {
seq_json["pluck_decay"] = serde_json::json!(v);
}
if let Some(v) = t.voice.pluck_body {
seq_json["pluck_body"] = serde_json::json!(v);
}
if let Some(v) = t.voice.pluck_pick {
seq_json["pluck_pick"] = serde_json::json!(v);
}
if let Some(v) = t.voice.pluck_tone {
seq_json["pluck_tone"] = serde_json::json!(v);
}
if let Some(v) = t.voice.piano_hammer {
seq_json["piano_hammer"] = serde_json::json!(v);
}
if let Some(v) = t.voice.piano_strike {
seq_json["piano_strike"] = serde_json::json!(v);
}
if let Some(v) = t.voice.piano_inharm {
seq_json["piano_inharm"] = serde_json::json!(v);
}
if let Some(v) = t.voice.piano_detune {
seq_json["piano_detune"] = serde_json::json!(v);
}
if let Some(v) = t.voice.piano_decay {
seq_json["piano_decay"] = serde_json::json!(v);
}
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 [
("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))?;
doc_tracks.push(Track {
id: Some(t.name.clone()),
node: seq,
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 doc: SoundDoc = serde_json::from_value(serde_json::json!({
"name": self.name,
"duration": duration,
"engine": ENGINE_VERSION,
"root": serde_json::to_value(&root).map_err(|e| e.to_string())?,
}))
.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 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"
);
}
}
}
#[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");
}
}