mod layers;
mod schedule;
mod sections;
use crate::dsl::SoundDoc;
use crate::render;
use crate::runtime::AudioSource;
pub use schedule::Quantize;
pub struct LoopBuffer {
left: Vec<f32>,
right: Vec<f32>,
pos: usize,
}
impl LoopBuffer {
pub fn from_doc(doc: &SoundDoc) -> Self {
let (left, right) = render_stereo_pair(doc);
LoopBuffer::from_stereo(left, right)
}
pub fn from_doc_at(doc: &SoundDoc, sample_rate: u32) -> Self {
LoopBuffer::from_doc(&doc_at(doc, sample_rate))
}
pub fn from_stereo(mut left: Vec<f32>, mut right: Vec<f32>) -> Self {
let n = left.len().min(right.len());
left.truncate(n);
right.truncate(n);
LoopBuffer {
left,
right,
pos: 0,
}
}
pub fn from_doc_len(doc: &SoundDoc, frames: usize) -> Self {
let (mut left, mut right) = render_stereo_pair(doc);
left.resize(frames, 0.0);
right.resize(frames, 0.0);
LoopBuffer {
left,
right,
pos: 0,
}
}
pub fn from_doc_len_at(doc: &SoundDoc, frames: usize, sample_rate: u32) -> Self {
LoopBuffer::from_doc_len(&doc_at(doc, sample_rate), frames)
}
}
fn doc_at(doc: &SoundDoc, sample_rate: u32) -> SoundDoc {
let mut doc = doc.clone();
doc.sample_rate = sample_rate;
doc
}
impl AudioSource for LoopBuffer {
fn fill(&mut self, out: &mut [f32]) -> usize {
let frames = out.len() / 2;
let n = self.left.len();
if n == 0 {
out.fill(0.0);
return frames;
}
for f in 0..frames {
out[f * 2] = self.left[self.pos];
out[f * 2 + 1] = self.right[self.pos];
self.pos += 1;
if self.pos == n {
self.pos = 0;
}
}
frames
}
fn reset(&mut self) {
self.pos = 0;
}
}
struct Layer {
source: Box<dyn AudioSource + Send>,
fade_in_at: f32,
gain: f32,
target: f32,
}
struct Stinger {
left: Vec<f32>,
right: Vec<f32>,
pos: usize,
}
impl Stinger {
fn new(mut left: Vec<f32>, mut right: Vec<f32>) -> Self {
let n = left.len().min(right.len());
left.truncate(n);
right.truncate(n);
Stinger {
left,
right,
pos: 0,
}
}
}
const DUCK_ATTACK_SECS: f32 = 0.002;
const DUCK_SNAP_EPSILON: f32 = 1e-4;
pub fn render_stereo_pair(doc: &SoundDoc) -> (Vec<f32>, Vec<f32>) {
let p = render::render_product(doc);
p.stereo.unwrap_or_else(|| (p.mono.clone(), p.mono))
}
struct Scheduled {
fire_at: u64,
action: Action,
}
enum Action {
SetIntensity(f32),
Stinger(Stinger),
Transition { to: usize },
}
struct Section {
name: String,
buffer: LoopBuffer,
}
struct SectionFade {
to: usize,
from_gain: f32,
step: f32,
frames_done: usize,
}
pub struct AdaptiveMusic {
layers: Vec<Layer>,
stingers: Vec<Stinger>,
intensity: f32,
fade_coeff: f32,
scratch: Vec<f32>,
sample_rate: u32,
paused: bool,
position: u64,
duck_gain: f32,
duck_target: f32,
duck_attack: f32,
duck_coeff: f32,
bpm: f32,
beats_per_bar: u32,
pending: Vec<Scheduled>,
sections: Vec<Section>,
current_section: Option<usize>,
section_fade: Option<SectionFade>,
section_step: f32,
}
impl AdaptiveMusic {
pub fn new(sample_rate: u32) -> Self {
let fade_coeff = 1.0 - (-1.0 / (1.5 * sample_rate as f32)).exp();
AdaptiveMusic {
layers: Vec::new(),
stingers: Vec::new(),
intensity: 0.0,
fade_coeff,
scratch: Vec::new(),
sample_rate,
paused: false,
position: 0,
duck_gain: 1.0,
duck_target: 1.0,
duck_attack: 0.0,
duck_coeff: 0.0,
bpm: 0.0,
beats_per_bar: 4,
pending: Vec::new(),
sections: Vec::new(),
current_section: None,
section_fade: None,
section_step: 1.0 / (0.06 * sample_rate as f32),
}
}
pub fn pause(&mut self) {
self.paused = true;
}
pub fn resume(&mut self) {
self.paused = false;
}
pub fn is_paused(&self) -> bool {
self.paused
}
pub fn reset(&mut self) {
self.position = 0;
self.stingers.clear();
self.pending.clear();
self.section_fade = None;
self.duck_gain = 1.0;
self.duck_target = 1.0;
for l in &mut self.layers {
l.source.reset();
}
for s in &mut self.sections {
s.buffer.reset();
}
}
}
impl AudioSource for AdaptiveMusic {
fn fill(&mut self, out: &mut [f32]) -> usize {
let frames = out.len() / 2;
out.fill(0.0);
if self.paused {
return frames;
}
let mut done = 0usize;
while done < frames {
self.fire_due();
let next = self
.pending
.iter()
.map(|s| s.fire_at)
.filter(|&t| t > self.position)
.min();
let span = match next {
Some(t) => ((t - self.position) as usize).min(frames - done),
None => frames - done,
};
self.render_span(&mut out[done * 2..(done + span) * 2], span);
done += span;
}
frames
}
fn reset(&mut self) {
AdaptiveMusic::reset(self);
}
}
impl AdaptiveMusic {
fn render_span(&mut self, out: &mut [f32], frames: usize) {
if self.scratch.len() < frames * 2 {
self.scratch.resize(frames * 2, 0.0);
}
let coeff = self.fade_coeff;
let scratch = &mut self.scratch[..frames * 2];
let fade = self
.section_fade
.as_ref()
.map(|f| (f.to, f.from_gain, f.step, f.frames_done));
if let Some((to, start, step, done)) = fade {
let g = |f: usize| (start + (done + f) as f32 * step).clamp(0.0, 1.0);
if let Some(cur) = self.current_section {
self.sections[cur].buffer.fill(scratch);
for f in 0..frames {
let g = g(f);
out[f * 2] += scratch[f * 2] * (1.0 - g);
out[f * 2 + 1] += scratch[f * 2 + 1] * (1.0 - g);
}
}
self.sections[to].buffer.fill(scratch);
for f in 0..frames {
let g = g(f);
out[f * 2] += scratch[f * 2] * g;
out[f * 2 + 1] += scratch[f * 2 + 1] * g;
}
let end = g(frames);
if end >= 1.0 {
self.current_section = Some(to);
self.section_fade = None;
} else if end <= 0.0 {
self.section_fade = None;
} else if let Some(fd) = self.section_fade.as_mut() {
fd.frames_done = done + frames;
}
} else if let Some(cur) = self.current_section {
self.sections[cur].buffer.fill(scratch);
for f in 0..frames {
out[f * 2] += scratch[f * 2];
out[f * 2 + 1] += scratch[f * 2 + 1];
}
}
for layer in &mut self.layers {
layer.source.fill(scratch);
for f in 0..frames {
layer.gain += (layer.target - layer.gain) * coeff;
if (layer.target - layer.gain).abs() < DUCK_SNAP_EPSILON {
layer.gain = layer.target;
}
out[f * 2] += scratch[f * 2] * layer.gain;
out[f * 2 + 1] += scratch[f * 2 + 1] * layer.gain;
}
}
for st in &mut self.stingers {
let n = (st.left.len() - st.pos).min(frames);
for f in 0..n {
out[f * 2] += st.left[st.pos];
out[f * 2 + 1] += st.right[st.pos];
st.pos += 1;
}
}
self.stingers.retain(|s| s.pos < s.left.len());
if self.duck_gain < 1.0 || self.duck_target < 1.0 {
for f in 0..frames {
if self.duck_gain > self.duck_target {
self.duck_gain += (self.duck_target - self.duck_gain) * self.duck_attack;
if self.duck_gain <= self.duck_target + DUCK_SNAP_EPSILON {
self.duck_gain = self.duck_target;
self.duck_target = 1.0;
}
} else {
self.duck_gain += (1.0 - self.duck_gain) * self.duck_coeff;
if self.duck_gain >= 1.0 - DUCK_SNAP_EPSILON {
self.duck_gain = 1.0;
}
}
out[f * 2] *= self.duck_gain;
out[f * 2 + 1] *= self.duck_gain;
}
}
self.position += frames as u64;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn doc(json: &str) -> SoundDoc {
serde_json::from_str(json).unwrap()
}
fn peak(s: &[f32]) -> f32 {
s.iter().fold(0.0f32, |m, &x| m.max(x.abs()))
}
#[test]
fn layers_fade_with_intensity() {
let base = doc(r#"{ "name":"b", "duration":0.2, "root": { "type":"sine", "freq":220 } }"#);
let hi = doc(r#"{ "name":"h", "duration":0.2, "root": { "type":"sine", "freq":880 } }"#);
let mut music = AdaptiveMusic::new(48_000);
music.add_layer(LoopBuffer::from_doc(&base), 0.0); music.add_layer(LoopBuffer::from_doc(&hi), 0.5);
let mut out = vec![0.0f32; 512 * 2];
music.fill(&mut out);
assert!(peak(&out) > 0.0, "base layer sounds");
assert_eq!(
music.layer_gain(1),
Some(0.0),
"hi layer silent at intensity 0"
);
music.set_intensity(1.0);
for _ in 0..400 {
music.fill(&mut vec![0.0f32; 512 * 2]); }
assert!(
music.layer_gain(1).unwrap() > 0.9,
"hi layer swelled in with intensity"
);
}
#[test]
fn stingers_fire_and_finish() {
let base = doc(r#"{ "name":"b", "duration":0.2, "root": { "type":"sine", "freq":220 } }"#);
let sting =
doc(r#"{ "name":"s", "duration":0.05, "root": { "type":"sine", "freq":1320 } }"#);
let mut music = AdaptiveMusic::new(48_000);
music.add_layer(LoopBuffer::from_doc(&base), 0.0);
music.stinger(&sting);
assert_eq!(music.active_stingers(), 1);
for _ in 0..20 {
music.fill(&mut vec![0.0f32; 512 * 2]);
}
assert_eq!(
music.active_stingers(),
0,
"stinger finished and was culled"
);
}
#[test]
fn loop_buffer_reset_rewinds_to_head() {
let base = doc(r#"{ "name":"b", "duration":0.2, "root": { "type":"sine", "freq":220 } }"#);
let mut buf = LoopBuffer::from_doc(&base);
let mut first = vec![0.0f32; 64 * 2];
buf.fill(&mut first);
buf.fill(&mut vec![0.0f32; 512 * 2]); buf.reset();
let mut again = vec![0.0f32; 64 * 2];
buf.fill(&mut again);
assert_eq!(
first, again,
"reset replays from the loop head, sample-identical"
);
}
#[test]
fn position_advances_while_playing_and_holds_when_paused() {
let base = doc(r#"{ "name":"b", "duration":0.2, "root": { "type":"sine", "freq":220 } }"#);
let mut music = AdaptiveMusic::new(48_000);
music.add_layer(LoopBuffer::from_doc(&base), 0.0);
assert_eq!(music.position_frames(), 0);
music.fill(&mut vec![0.0f32; 512 * 2]);
assert_eq!(music.position_frames(), 512, "the clock advances by frames");
music.pause();
assert!(music.is_paused());
let mut out = vec![0.1f32; 512 * 2];
music.fill(&mut out);
assert_eq!(peak(&out), 0.0, "paused output is silent");
assert_eq!(music.position_frames(), 512, "the clock holds while paused");
music.resume();
music.fill(&mut vec![0.0f32; 512 * 2]);
assert_eq!(music.position_frames(), 1024, "resumes advancing");
}
#[test]
fn reset_zeroes_the_clock_and_restarts_layers() {
let base = doc(r#"{ "name":"b", "duration":0.2, "root": { "type":"sine", "freq":220 } }"#);
let mut music = AdaptiveMusic::new(48_000);
music.add_layer(LoopBuffer::from_doc(&base), 0.0);
music.set_intensity(1.0);
music.fill(&mut vec![0.0f32; 512 * 2]);
assert!(music.position_frames() > 0);
music.reset();
assert_eq!(music.position_frames(), 0, "the clock is back at sample 0");
let mut out = vec![0.0f32; 512 * 2];
music.fill(&mut out);
assert!(peak(&out) > 0.0, "the bed plays again from the top");
}
#[test]
fn duck_attenuates_then_recovers() {
use std::time::Duration;
let base = doc(r#"{ "name":"b", "duration":0.2, "root": { "type":"sine", "freq":220 } }"#);
let mut plain = AdaptiveMusic::new(48_000);
plain.add_layer(LoopBuffer::from_doc(&base), 0.0);
let mut plain_out = vec![0.0f32; 512 * 2];
plain.fill(&mut plain_out);
let reference = peak(&plain_out);
let mut music = AdaptiveMusic::new(48_000);
music.add_layer(LoopBuffer::from_doc(&base), 0.0);
music.duck(0.9, Duration::from_millis(180));
let mut ducked = vec![0.0f32; 512 * 2];
music.fill(&mut ducked);
assert!(
peak(&ducked) < reference,
"ducked block is quieter than the undicked reference"
);
for _ in 0..64 {
music.fill(&mut vec![0.0f32; 512 * 2]);
}
let mut recovered = vec![0.0f32; 512 * 2];
music.fill(&mut recovered);
assert!(
peak(&recovered) > 0.9 * reference,
"the duck recovered toward unity"
);
}
fn tone(freq: f32) -> SoundDoc {
doc(&format!(
r#"{{ "name":"t", "duration":0.25, "root": {{ "type":"sine", "freq":{freq} }} }}"#
))
}
fn advance(m: &mut AdaptiveMusic, frames: usize) {
m.fill(&mut vec![0.0f32; frames * 2]);
}
#[test]
fn clock_counts_beats_and_bars() {
let mut m = AdaptiveMusic::new(48_000);
m.add_layer(LoopBuffer::from_doc(&tone(220.0)), 0.0);
m.set_tempo(120.0, 4); advance(&mut m, 24_000);
assert!(
(m.beats() - 1.0).abs() < 1e-6,
"one beat elapsed: {}",
m.beats()
);
advance(&mut m, 24_000 * 7); assert!(
(m.bars() - 2.0).abs() < 1e-6,
"two bars elapsed: {}",
m.bars()
);
}
#[test]
fn intensity_change_waits_for_the_bar() {
let mut m = AdaptiveMusic::new(48_000);
m.add_layer(LoopBuffer::from_doc(&tone(220.0)), 0.0);
m.add_layer(LoopBuffer::from_doc(&tone(880.0)), 0.5); m.set_tempo(120.0, 4); m.set_intensity_at(1.0, Quantize::Bar);
advance(&mut m, 48_000);
assert_eq!(m.intensity(), 0.0, "intensity holds before the bar");
advance(&mut m, 48_100);
assert_eq!(m.intensity(), 1.0, "intensity applied on the bar");
}
#[test]
fn transition_swaps_sections_on_the_bar() {
let mut m = AdaptiveMusic::new(48_000);
let explore = m.add_section("explore", &tone(330.0));
let battle = m.add_section("battle", &tone(660.0));
assert_eq!(m.current_section(), Some(explore));
assert_eq!(m.section_named("battle"), Some(battle));
m.set_tempo(120.0, 4); m.transition_to(battle, Quantize::Bar);
let mut out = vec![0.0f32; 512 * 2];
m.fill(&mut out);
assert!(peak(&out) > 0.0, "section audio plays");
assert_eq!(m.current_section(), Some(explore), "holds until the bar");
for _ in 0..200 {
advance(&mut m, 512);
}
assert_eq!(m.current_section(), Some(battle), "swapped to battle");
}
#[test]
fn buffer_and_doc_section_apis_agree() {
let d = tone(220.0);
let via_doc = {
let mut m = AdaptiveMusic::new(48_000);
m.add_section("a", &d);
let mut o = vec![0.0f32; 256 * 2];
m.fill(&mut o);
o
};
let via_buf = {
let mut m = AdaptiveMusic::new(48_000);
m.add_section_buffer("a", LoopBuffer::from_doc_at(&d, 48_000));
let mut o = vec![0.0f32; 256 * 2];
m.fill(&mut o);
o
};
assert_eq!(
via_doc, via_buf,
"add_section_buffer must match add_section"
);
}
#[test]
fn doc_apis_render_at_the_engine_rate() {
let d = tone(220.0); assert_ne!(d.sample_rate, 48_000, "test needs a mismatched doc");
let mut at_engine_rate = AdaptiveMusic::new(48_000);
at_engine_rate.add_layer_doc(&d, 0.0);
let mut a = vec![0.0f32; 512];
at_engine_rate.fill(&mut a);
let mut explicit = AdaptiveMusic::new(48_000);
explicit.add_layer(LoopBuffer::from_doc_at(&d, 48_000), 0.0);
let mut b = vec![0.0f32; 512];
explicit.fill(&mut b);
assert_eq!(a, b, "add_layer_doc == from_doc_at at the engine rate");
let mut wrong = AdaptiveMusic::new(48_000);
wrong.add_layer(LoopBuffer::from_doc(&d), 0.0);
let mut c = vec![0.0f32; 512];
wrong.fill(&mut c);
assert_ne!(a, c, "the doc-rate render is a different (detuned) signal");
}
#[test]
fn transition_to_the_current_section_is_a_pure_noop() {
let plain = {
let mut m = AdaptiveMusic::new(48_000);
m.add_section("a", &tone(220.0));
m.add_section("b", &tone(440.0));
let mut o = vec![0.0f32; 256 * 2];
m.fill(&mut o);
o
};
let mut m = AdaptiveMusic::new(48_000);
let a = m.add_section("a", &tone(220.0));
m.add_section("b", &tone(440.0));
m.transition_to(a, Quantize::Immediate); let mut o = vec![0.0f32; 256 * 2];
m.fill(&mut o);
assert_eq!(
o, plain,
"a transition to the current section changed the mix"
);
}
#[test]
fn stinger_fires_on_the_beat() {
let mut m = AdaptiveMusic::new(48_000);
m.add_layer(LoopBuffer::from_doc(&tone(220.0)), 0.0);
m.set_tempo(120.0, 4); m.stinger_at(&tone(1320.0), Quantize::Beat);
assert_eq!(m.active_stingers(), 0, "not yet — waiting for the beat");
for _ in 0..46 {
advance(&mut m, 512); }
assert_eq!(m.active_stingers(), 0, "still before the beat");
advance(&mut m, 512); assert_eq!(m.active_stingers(), 1, "stinger fired on the beat");
}
#[test]
fn quantized_schedule_is_deterministic() {
let run = || {
let mut m = AdaptiveMusic::new(48_000);
m.add_section("a", &tone(330.0));
let b = m.add_section("b", &tone(660.0));
m.add_layer(LoopBuffer::from_doc(&tone(220.0)), 0.0);
m.set_tempo(140.0, 4);
m.set_intensity_at(0.8, Quantize::Beat);
m.transition_to(b, Quantize::Bar);
m.stinger_at(&tone(990.0), Quantize::Bars(2));
let mut acc = Vec::new();
let mut out = vec![0.0f32; 333 * 2]; for _ in 0..300 {
m.fill(&mut out);
acc.extend_from_slice(&out);
}
acc
};
assert_eq!(
run(),
run(),
"a fixed tempo + block schedule replays identically"
);
}
#[test]
fn immediate_api_unchanged_without_tempo() {
let mut m = AdaptiveMusic::new(48_000);
m.add_layer(LoopBuffer::from_doc(&tone(220.0)), 0.5);
m.set_intensity_at(1.0, Quantize::Bar); assert_eq!(m.intensity(), 1.0, "no tempo → applies immediately");
}
#[test]
fn from_doc_len_forces_the_grid_and_loops_at_it() {
let grid = 1000;
let mut buf = LoopBuffer::from_doc_len(&tone(220.0), grid);
let mut first = vec![0.0f32; grid * 2];
let mut second = vec![0.0f32; grid * 2];
buf.fill(&mut first);
buf.fill(&mut second);
assert_eq!(first, second, "the buffer loops exactly at the grid length");
}
#[test]
fn stem_set_shares_one_beat_grid() {
let mut music = AdaptiveMusic::new(48_000);
music.set_tempo(120.0, 4);
let base = tone(220.0);
let hi = tone(880.0);
let grid = music.add_stem_set(&[(&base, 0.0), (&hi, 0.5)], 4.0);
assert_eq!(grid, 96_000, "grid = duration_beats × frames_per_beat");
assert_eq!(music.layer_gain(0), Some(1.0), "base always on");
assert_eq!(music.layer_gain(1), Some(0.0), "hi silent until intensity");
}
#[test]
fn stem_set_without_tempo_falls_back_to_the_first_stem() {
let mut music = AdaptiveMusic::new(48_000);
let base = tone(220.0);
let grid = music.add_stem_set(&[(&base, 0.0)], 4.0);
assert!(
grid > 0,
"no tempo → grid falls back to the first stem's natural length"
);
}
#[test]
fn scheduled_events_are_block_size_invariant() {
let run = |block: usize| {
let mut m = AdaptiveMusic::new(48_000);
m.add_section("a", &tone(330.0));
let b = m.add_section("b", &tone(660.0));
m.add_layer(LoopBuffer::from_doc(&tone(220.0)), 0.0);
m.add_layer(LoopBuffer::from_doc(&tone(880.0)), 0.5);
m.set_tempo(120.0, 4);
m.set_intensity_at(1.0, Quantize::Beat);
m.stinger_at(&tone(990.0), Quantize::Beat);
m.transition_to(b, Quantize::Bar);
let mut acc = Vec::new();
let mut out = vec![0.0f32; block * 2];
for _ in 0..(200_000 / block) {
m.fill(&mut out);
acc.extend_from_slice(&out);
}
acc
};
let a = run(128);
let b = run(512);
let n = a.len().min(b.len());
assert_eq!(
a[..n],
b[..n],
"output must not depend on the host block size"
);
}
#[test]
fn transition_reversal_mid_fade_cancels_back() {
let mut m = AdaptiveMusic::new(48_000);
let a = m.add_section("a", &tone(330.0));
let b = m.add_section("b", &tone(660.0));
m.transition_to(b, Quantize::Immediate);
advance(&mut m, 512); m.transition_to(a, Quantize::Immediate);
for _ in 0..40 {
advance(&mut m, 512);
}
assert_eq!(m.current_section(), Some(a), "cancelled back to a");
}
#[test]
fn re_transition_to_the_fade_target_is_a_noop() {
let mut m = AdaptiveMusic::new(48_000);
m.add_section("a", &tone(330.0));
let b = m.add_section("b", &tone(660.0));
m.transition_to(b, Quantize::Immediate);
advance(&mut m, 512); m.transition_to(b, Quantize::Immediate);
advance(&mut m, 2500); assert_eq!(
m.current_section(),
Some(b),
"the fade completed on its original clock"
);
}
#[test]
fn audio_source_reset_restarts_the_bed() {
let mut m = AdaptiveMusic::new(48_000);
m.add_layer(LoopBuffer::from_doc(&tone(220.0)), 0.0);
m.stinger(&tone(990.0));
advance(&mut m, 512);
assert!(m.position_frames() > 0);
let src: &mut dyn AudioSource = &mut m;
src.reset();
assert_eq!(
m.position_frames(),
0,
"reset through the trait restarts the clock"
);
assert_eq!(m.active_stingers(), 0, "and clears stingers");
}
#[test]
fn third_section_switch_completes_the_fade_first() {
let mut m = AdaptiveMusic::new(48_000);
m.add_section("a", &tone(330.0));
let b = m.add_section("b", &tone(660.0));
let c = m.add_section("c", &tone(990.0));
m.transition_to(b, Quantize::Immediate);
advance(&mut m, 512); m.transition_to(c, Quantize::Immediate);
advance(&mut m, 2880); assert_eq!(
m.current_section(),
Some(b),
"the in-flight fade completed before the onward transition"
);
for _ in 0..20 {
advance(&mut m, 512);
}
assert_eq!(m.current_section(), Some(c), "then c faded in");
}
}