use super::effects::reverb;
use super::output::{make_loop_buffer, normalize_output, normalize_output_v4};
#[cfg(feature = "sampler")]
use super::seq::{SeqVoice, sampler_seq_stereo};
use super::{Signal, apply_processor, render_node};
use crate::dsl::{
AutoCurve, AutoLane, AutoPoint, AutoTarget, Node, Playback, SeqWave, Sidechain, SoundDoc, Track,
};
use crate::dsp::{Rng, layer_stream_key, peak_limit};
enum TrackRender {
Muted,
Mono { off: usize, sig: Signal },
Stereo {
off: usize,
left: Signal,
right: Signal,
},
}
pub(crate) fn pan_gains(pan: f32, gain: f32, engine: u32) -> (f32, f32) {
let theta = (pan + 1.0) * std::f32::consts::FRAC_PI_4;
(
crate::dsp::cos(theta, engine) * gain,
crate::dsp::sin(theta, engine) * gain,
)
}
pub(crate) fn track_stream_seed(seed: u64, stream: u64) -> u64 {
crate::dsp::splitmix_mix(
seed ^ stream
.wrapping_add(1)
.wrapping_mul(crate::dsp::GOLDEN_GAMMA),
)
}
pub(crate) const MASTER_STREAM: u64 = u64::MAX;
fn is_native_stereo(node: &Node) -> bool {
matches!(
node,
Node::Seq {
wave: SeqWave::Sampler,
..
}
)
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct LayerStats {
pub id: String,
pub peak_dbfs: f32,
pub rms_dbfs: f32,
pub energy_pct: f32,
pub mute: bool,
}
#[derive(Debug, PartialEq)]
pub struct TracksRender {
pub left: Signal,
pub right: Signal,
pub layers: Vec<LayerStats>,
}
pub(crate) struct LaneCursor {
pts: Vec<AutoPoint>,
curve: AutoCurve,
idx: usize,
}
impl LaneCursor {
pub(crate) fn build(automation: &[AutoLane], target: AutoTarget, default: f32) -> Option<Self> {
let lane = automation.iter().find(|l| l.target == target)?;
if lane.points.len() < 2 {
let v = lane.points.first().map_or(default, |p| p.v);
return Some(LaneCursor {
pts: vec![AutoPoint { t: 0.0, v }],
curve: lane.curve,
idx: 0,
});
}
let mut pts = lane.points.clone();
pts.sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap_or(std::cmp::Ordering::Equal));
Some(LaneCursor {
pts,
curve: lane.curve,
idx: 0,
})
}
pub(crate) fn at(&mut self, i: usize, sr: u32, engine: u32) -> f32 {
let t = i as f32 / sr as f32;
if t.is_nan() {
return self.pts[0].v;
}
if t <= self.pts[0].t {
return self.pts[0].v;
}
let last = &self.pts[self.pts.len() - 1];
if t >= last.t {
return last.v;
}
while t > self.pts[self.idx + 1].t {
self.idx += 1;
}
let (w0, w1) = (&self.pts[self.idx], &self.pts[self.idx + 1]);
let span = (w1.t - w0.t).max(1e-9);
let u = (t - w0.t) / span;
match self.curve {
AutoCurve::Linear => w0.v + (w1.v - w0.v) * u,
AutoCurve::Step => {
if u >= 1.0 {
w1.v
} else {
w0.v
}
}
AutoCurve::Exp => {
if w0.v > 0.0 && w1.v > 0.0 {
w0.v * crate::dsp::powf(w1.v / w0.v, u, engine)
} else {
w0.v + (w1.v - w0.v) * u
}
}
}
}
}
fn lane_for(
automation: &[AutoLane],
target: AutoTarget,
n: usize,
sr: u32,
default: f32,
engine: u32,
) -> Option<Vec<f32>> {
let mut cursor = LaneCursor::build(automation, target, default)?;
Some((0..n).map(|i| cursor.at(i, sr, engine)).collect())
}
fn duck_envelope(
source: &TrackRender,
source_track: &Track,
sc: &Sidechain,
n: usize,
sr: u32,
engine: u32,
) -> Vec<f32> {
let mut sig = vec![0.0f32; n];
let gain_lane = lane_for(
&source_track.automation,
AutoTarget::Gain,
n,
sr,
source_track.gain,
engine,
);
let g = |pos: usize| gain_lane.as_ref().map_or(source_track.gain, |a| a[pos]);
match source {
TrackRender::Muted => {}
TrackRender::Mono { off, sig: mono } => {
for (i, x) in mono.iter().take(n - off).enumerate() {
sig[i + off] = x * g(i + off);
}
}
TrackRender::Stereo { off, left, right } => {
for i in 0..n - off {
sig[i + off] = 0.5 * (left[i] + right[i]) * g(i + off);
}
}
}
let srf = sr as f32;
let at = crate::dsp::exp(-1.0 / (sc.attack.max(1e-4) * srf), engine);
let rt = crate::dsp::exp(-1.0 / (sc.release.max(1e-4) * srf), engine);
let mut env = 0.0f32;
sig.into_iter()
.map(|t| {
let rect = t.abs().min(1.0);
let coeff = if rect > env { at } else { rt };
env = rect + coeff * (env - rect);
1.0 - sc.amount * env
})
.collect()
}
pub fn render_tracks(doc: &SoundDoc) -> Option<TracksRender> {
render_tracks_impl(doc, false).map(|(r, _)| r)
}
#[derive(Debug, Clone)]
pub struct Stem {
pub id: String,
pub is_bus: bool,
pub bus: Option<String>,
pub left: Signal,
pub right: Signal,
}
pub fn render_stems(doc: &SoundDoc) -> Option<Vec<Stem>> {
let (_, stems) = render_tracks_impl(doc, true)?;
Some(stems.expect("stems requested"))
}
fn render_tracks_impl(
doc: &SoundDoc,
want_stems: bool,
) -> Option<(TracksRender, Option<Vec<Stem>>)> {
let Node::Tracks {
tracks,
master,
buses,
} = &doc.root
else {
return None;
};
let sr = doc.sample_rate;
let n = ((doc.duration.clamp(0.0, 600.0) * sr as f32).ceil() as usize).max(1);
let per_track_streams = doc.effective_version() >= 2;
let engine = doc.effective_engine();
let mut rng = Rng::new(doc.seed);
let (mut left, mut right) = (vec![0.0f32; n], vec![0.0f32; n]);
let mut layer_ids = Vec::with_capacity(tracks.len());
let mut rendered = Vec::with_capacity(tracks.len());
for (ti, t) in tracks.iter().enumerate() {
let layer_id = t.id.clone().unwrap_or_else(|| format!("layer_{ti}"));
let stream = layer_stream_key(&layer_id);
layer_ids.push(layer_id);
if t.mute {
if !per_track_streams && !is_native_stereo(&t.node) {
let _ = render_node(
&t.node,
n,
sr,
&mut rng,
engine,
track_stream_seed(doc.seed, stream),
);
}
rendered.push(TrackRender::Muted);
continue;
}
let off = ((t.at.max(0.0) * sr as f32).round() as usize).min(n);
if let Some((l, r)) = track_native_stereo(&t.node, n, sr) {
rendered.push(TrackRender::Stereo {
off,
left: l,
right: r,
});
} else {
let base = track_stream_seed(doc.seed, stream);
let mono = if per_track_streams {
let mut trng = Rng::new(base);
render_node(&t.node, n, sr, &mut trng, engine, base)
} else {
render_node(&t.node, n, sr, &mut rng, engine, base)
};
rendered.push(TrackRender::Mono { off, sig: mono });
}
}
let mut layers = Vec::with_capacity(tracks.len());
let mut energies = Vec::with_capacity(tracks.len());
let mut stems = want_stems.then(Vec::new);
let mut bus_bufs: Vec<(Vec<f32>, Vec<f32>)> = buses
.iter()
.map(|_| (vec![0.0f32; n], vec![0.0f32; n]))
.collect();
let bus_index = |id: &str| buses.iter().position(|b| b.id == id);
let (mut cl, mut cr) = (vec![0.0f32; n], vec![0.0f32; n]);
for (ti, t) in tracks.iter().enumerate() {
let layer_id = layer_ids[ti].clone();
if let TrackRender::Muted = &rendered[ti] {
if let Some(stems) = &mut stems {
stems.push(Stem {
id: layer_id.clone(),
is_bus: false,
bus: t.bus.clone(),
left: vec![0.0f32; n],
right: vec![0.0f32; n],
});
}
layers.push(LayerStats {
id: layer_id,
peak_dbfs: -180.0,
rms_dbfs: -180.0,
energy_pct: 0.0,
mute: true,
});
energies.push(0.0f64);
continue;
}
let (glc, grc) = pan_gains(t.pan.clamp(-1.0, 1.0), t.gain, engine);
let gain_lane = lane_for(&t.automation, AutoTarget::Gain, n, sr, t.gain, engine);
let pan_lane = lane_for(&t.automation, AutoTarget::Pan, n, sr, t.pan, engine);
let gl_gr = |pos: usize| -> (f32, f32) {
match (&gain_lane, &pan_lane) {
(None, None) => (glc, grc),
(g, p) => {
let gain = g.as_ref().map_or(t.gain, |a| a[pos]);
let pan = p.as_ref().map_or(t.pan, |a| a[pos]).clamp(-1.0, 1.0);
pan_gains(pan, gain, engine)
}
}
};
let duck = t.sidechain.as_ref().and_then(|sc| {
let (si, source) = tracks
.iter()
.enumerate()
.find(|(_, s)| s.id.as_deref() == Some(sc.source.as_str()))?;
Some(duck_envelope(&rendered[si], source, sc, n, sr, engine))
});
let (mut tpeak, mut tsum) = (0.0f32, 0.0f64);
cl.fill(0.0);
cr.fill(0.0);
let off = match &rendered[ti] {
TrackRender::Muted => unreachable!("muted layers continue above"),
TrackRender::Stereo { off, .. } | TrackRender::Mono { off, .. } => *off,
};
match &rendered[ti] {
TrackRender::Muted => unreachable!("muted layers continue above"),
TrackRender::Stereo {
off,
left: l,
right: r,
} => {
for i in 0..n - off {
let (gl, gr) = gl_gr(i + off);
let d = duck.as_ref().map_or(1.0, |v| v[i + off]);
let (la, ra) = (
l[i] * gl * std::f32::consts::SQRT_2 * d,
r[i] * gr * std::f32::consts::SQRT_2 * d,
);
cl[i + off] = la;
cr[i + off] = ra;
tpeak = tpeak.max(la.abs()).max(ra.abs());
tsum += (la * la + ra * ra) as f64;
}
}
TrackRender::Mono { off, sig } => {
for (i, x) in sig.iter().take(n - off).enumerate() {
let (gl, gr) = gl_gr(i + off);
let d = duck.as_ref().map_or(1.0, |v| v[i + off]);
let (la, ra) = (x * gl * d, x * gr * d);
cl[i + off] = la;
cr[i + off] = ra;
tpeak = tpeak.max(la.abs()).max(ra.abs());
tsum += (la * la + ra * ra) as f64;
}
}
}
let (dl, dr) = match t.bus.as_deref().and_then(bus_index) {
None => (&mut left, &mut right),
Some(bi) => {
let (bl, br) = &mut bus_bufs[bi];
(bl, br)
}
};
for i in off..n {
dl[i] += cl[i];
dr[i] += cr[i];
}
for s in &t.sends {
let Some(bi) = bus_index(&s.bus) else {
continue; };
let amount = s.amount.clamp(0.0, 1.0);
let (bl, br) = &mut bus_bufs[bi];
for i in off..n {
bl[i] += cl[i] * amount;
br[i] += cr[i] * amount;
}
}
if let Some(stems) = &mut stems {
stems.push(Stem {
id: layer_id.clone(),
is_bus: false,
bus: t.bus.clone(),
left: cl.clone(),
right: cr.clone(),
});
}
let rms = ((tsum / (2 * n) as f64) as f32).sqrt();
layers.push(LayerStats {
id: layer_id,
peak_dbfs: crate::dsp::dbfs_e(tpeak, engine),
rms_dbfs: crate::dsp::dbfs_e(rms, engine),
energy_pct: 0.0, mute: false,
});
energies.push(tsum);
}
let total: f64 = energies.iter().sum();
if total > 0.0 {
for (l, e) in layers.iter_mut().zip(&energies) {
l.energy_pct = ((e / total) * 100.0) as f32;
}
}
for (bi, b) in buses.iter().enumerate() {
let (mut bl, mut br) = std::mem::take(&mut bus_bufs[bi]);
let bpath = track_stream_seed(doc.seed, layer_stream_key(&format!("bus:{}", b.id)));
let mut brng = Rng::new(bpath);
for fx in &b.effects {
if let Node::Reverb { room, mix } = fx {
bl = reverb(&bl, *room, *mix, sr, 0);
br = reverb(&br, *room, *mix, sr, 23);
} else {
let mut rl = brng.clone();
bl = apply_processor(fx, &bl, sr, &mut rl, engine, bpath);
br = apply_processor(fx, &br, sr, &mut brng, engine, bpath);
}
}
let gain = if b.gain.is_finite() { b.gain } else { 1.0 };
if let Some(stems) = &mut stems {
stems.push(Stem {
id: format!("bus:{}", b.id),
is_bus: true,
bus: None,
left: bl.iter().map(|x| x * gain).collect(),
right: br.iter().map(|x| x * gain).collect(),
});
}
for i in 0..n {
left[i] += bl[i] * gain;
right[i] += br[i] * gain;
}
}
if per_track_streams {
rng = Rng::new(track_stream_seed(doc.seed, MASTER_STREAM));
}
for m in master {
if let Node::Reverb { room, mix } = m {
left = reverb(&left, *room, *mix, sr, 0);
right = reverb(&right, *room, *mix, sr, 23);
} else {
let mpath = track_stream_seed(doc.seed, MASTER_STREAM);
let mut rl = rng.clone();
left = apply_processor(m, &left, sr, &mut rl, engine, mpath);
right = apply_processor(m, &right, sr, &mut rng, engine, mpath);
}
}
if let Playback::Loop {
start_secs,
end_secs,
crossfade_secs,
} = doc.playback
{
left = make_loop_buffer(&left, sr, start_secs, end_secs, crossfade_secs, engine);
right = make_loop_buffer(&right, sr, start_secs, end_secs, crossfade_secs, engine);
}
if let Some(nz) = &doc.normalize {
if engine >= 4 {
normalize_output_v4(&mut [&mut left, &mut right], nz, sr, engine);
} else {
normalize_output(&mut left, nz);
normalize_output(&mut right, nz);
}
}
peak_limit(&mut [&mut left, &mut right]);
Some((
TracksRender {
left,
right,
layers,
},
stems,
))
}
#[cfg(feature = "sampler")]
pub(super) fn track_native_stereo(node: &Node, n: usize, sr: u32) -> Option<(Signal, Signal)> {
let (voice, bpm, steps_per_beat, notes) = SeqVoice::from_node(node, 0)?;
if voice.wave != SeqWave::Sampler {
return None;
}
let step_dur = sr as f32 * 60.0 / bpm / steps_per_beat.max(1) as f32;
sampler_seq_stereo(&voice, notes, step_dur, n, sr)
}
#[cfg(not(feature = "sampler"))]
pub(super) fn track_native_stereo(_node: &Node, _n: usize, _sr: u32) -> Option<(Signal, Signal)> {
None
}