use serde::{Deserialize, Serialize};
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,
}
#[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,
});
self
}
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> = Vec::new();
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 seq: Node = serde_json::from_value(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())?,
}))
.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)
}
}
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");
}
}