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::{AutoLane, AutoTarget, Node, Playback, SeqWave, SoundDoc};
use crate::dsp::{Rng, layer_stream_key, peak_limit};
fn pan_gains(pan: f32, gain: f32) -> (f32, f32) {
let theta = (pan + 1.0) * std::f32::consts::FRAC_PI_4;
(theta.cos() * gain, theta.sin() * gain)
}
fn track_stream_seed(seed: u64, stream: u64) -> u64 {
crate::dsp::splitmix_mix(
seed ^ stream
.wrapping_add(1)
.wrapping_mul(crate::dsp::GOLDEN_GAMMA),
)
}
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>,
}
fn lane_for(
automation: &[AutoLane],
target: AutoTarget,
n: usize,
sr: u32,
default: f32,
) -> Option<Vec<f32>> {
let lane = automation.iter().find(|l| l.target == target)?;
if lane.points.is_empty() {
return Some(vec![default; n]);
}
let mut pts = lane.points.clone();
pts.sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap_or(std::cmp::Ordering::Equal));
let mut idx = 0;
Some(
(0..n)
.map(|i| {
let t = i as f32 / sr as f32;
if t <= pts[0].t {
return pts[0].v;
}
let last = &pts[pts.len() - 1];
if t >= last.t {
return last.v;
}
while t > pts[idx + 1].t {
idx += 1;
}
let (w0, w1) = (&pts[idx], &pts[idx + 1]);
let span = (w1.t - w0.t).max(1e-9);
w0.v + (w1.v - w0.v) * ((t - w0.t) / span)
})
.collect(),
)
}
pub fn render_tracks(doc: &SoundDoc) -> Option<TracksRender> {
let Node::Tracks { tracks, master } = &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 layers = Vec::with_capacity(tracks.len());
let mut energies = 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);
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),
);
}
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 off = ((t.at.max(0.0) * sr as f32).round() as usize).min(n);
let (glc, grc) = pan_gains(t.pan.clamp(-1.0, 1.0), t.gain);
let gain_lane = lane_for(&t.automation, AutoTarget::Gain, n, sr, t.gain);
let pan_lane = lane_for(&t.automation, AutoTarget::Pan, n, sr, t.pan);
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)
}
}
};
let (mut tpeak, mut tsum) = (0.0f32, 0.0f64);
if let Some((l, r)) = track_native_stereo(&t.node, n, sr) {
for i in 0..n - off {
let (gl, gr) = gl_gr(i + off);
let (la, ra) = (
l[i] * gl * std::f32::consts::SQRT_2,
r[i] * gr * std::f32::consts::SQRT_2,
);
left[i + off] += la;
right[i + off] += ra;
tpeak = tpeak.max(la.abs()).max(ra.abs());
tsum += (la * la + ra * ra) as f64;
}
} 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)
};
for (i, x) in mono.into_iter().take(n - off).enumerate() {
let (gl, gr) = gl_gr(i + off);
let (la, ra) = (x * gl, x * gr);
left[i + off] += la;
right[i + off] += ra;
tpeak = tpeak.max(la.abs()).max(ra.abs());
tsum += (la * la + ra * ra) as f64;
}
}
let rms = ((tsum / (2 * n) as f64) as f32).sqrt();
layers.push(LayerStats {
id: layer_id,
peak_dbfs: crate::dsp::dbfs(tpeak),
rms_dbfs: crate::dsp::dbfs(rms),
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;
}
}
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);
right = make_loop_buffer(&right, sr, start_secs, end_secs, crossfade_secs);
}
if let Some(nz) = &doc.normalize {
if engine >= 4 {
normalize_output_v4(&mut [&mut left, &mut right], nz, sr);
} else {
normalize_output(&mut left, nz);
normalize_output(&mut right, nz);
}
}
peak_limit(&mut [&mut left, &mut right]);
Some(TracksRender {
left,
right,
layers,
})
}
#[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
}