mod proc;
mod source;
mod tracks;
pub(crate) mod value;
#[cfg(test)]
pub(crate) mod tests;
use std::fmt;
use crate::dsl::{Node, Playback, SeqWave, SoundDoc, Stereo, Value};
use crate::dsp::node_path;
use proc::{Proc, try_proc};
use source::{Src, try_src};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StreamBlocker {
Normalize,
LoopPlayback,
StereoTreatment,
TracksRoot,
TracksPart {
part: String,
cause: Box<StreamBlocker>,
},
LegacyRng {
engine: u32,
},
Sampler,
ModulatedFilter,
OfflineEffect {
name: &'static str,
},
}
impl fmt::Display for StreamBlocker {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StreamBlocker::Normalize => write!(
f,
"the normalize output stage is a whole-buffer op — bake the level into the graph (a gain node) instead"
),
StreamBlocker::LoopPlayback => write!(
f,
"loop playback renders as its crossfaded loop body — stream the one-shot and loop at the host (Player), or bounce the loop offline"
),
StreamBlocker::StereoTreatment => write!(
f,
"a Haas/Wide stereo treatment is applied at write time, not in the graph — stream mono and widen at the host"
),
StreamBlocker::TracksRoot => write!(
f,
"a schema-v1 tracks root threads one shared RNG stream through its tracks in order — set \"version\": 2 for id-keyed per-track streams that stream natively"
),
StreamBlocker::TracksPart { part, cause } => write!(f, "{part}: {cause}"),
StreamBlocker::LegacyRng { engine } => write!(
f,
"noise/dust/seq draw from the shared order-dependent RNG stream under engine {engine} — set \"engine\": 2 or later for structurally-seeded RNG"
),
StreamBlocker::Sampler => write!(
f,
"the SoundFont sampler seq is an external stateful voice — bounce the part offline, or voice it with the built-in waves"
),
StreamBlocker::ModulatedFilter => write!(
f,
"a filter/EQ/gain carries a modulated cutoff or amount — bake it constant, or sweep it live with set_cutoff"
),
StreamBlocker::OfflineEffect { name } => write!(
f,
"{name} is an offline effect — it needs the whole input buffer; bounce it offline / apply it at bounce time and keep the streamed graph causal"
),
}
}
}
fn node_blockers(node: &Node, engine: u32, push: &mut impl FnMut(StreamBlocker)) {
match node {
Node::Tracks { .. } => push(StreamBlocker::TracksRoot),
Node::Noise { .. } | Node::Dust { .. } if engine < 2 => {
push(StreamBlocker::LegacyRng { engine });
}
Node::Seq { wave, .. } => {
if engine < 2 {
push(StreamBlocker::LegacyRng { engine });
}
if *wave == SeqWave::Sampler {
push(StreamBlocker::Sampler);
}
}
Node::Gain {
amount: Value::Modulated(_),
} => {
push(StreamBlocker::ModulatedFilter);
}
Node::Lowpass {
cutoff: Value::Modulated(_),
..
}
| Node::Highpass {
cutoff: Value::Modulated(_),
..
}
| Node::Bandpass {
cutoff: Value::Modulated(_),
..
}
| Node::Notch {
cutoff: Value::Modulated(_),
..
}
| Node::Peak {
cutoff: Value::Modulated(_),
..
}
| Node::Lowshelf {
cutoff: Value::Modulated(_),
..
}
| Node::Highshelf {
cutoff: Value::Modulated(_),
..
} => {
push(StreamBlocker::ModulatedFilter);
}
Node::Convolve { .. } => {
push(StreamBlocker::OfflineEffect { name: "convolve" });
}
Node::Granular { .. } => {
push(StreamBlocker::OfflineEffect { name: "granular" });
}
_ => {}
}
}
pub struct StreamGraph {
root: Root,
pos: usize,
engine: u32,
pitch: f32,
pitch_target: f32,
glide: f32,
bend: f32,
}
enum Root {
Mono(Src),
Tracks(tracks::StreamTracks),
}
impl StreamGraph {
pub fn blockers(doc: &SoundDoc) -> Vec<StreamBlocker> {
let mut out = Vec::new();
if doc.normalize.is_some() {
out.push(StreamBlocker::Normalize);
}
if matches!(doc.playback, Playback::Loop { .. }) {
out.push(StreamBlocker::LoopPlayback);
}
if !matches!(doc.stereo, Stereo::Mono) {
out.push(StreamBlocker::StereoTreatment);
}
let engine = doc.effective_engine();
match &doc.root {
Node::Tracks {
tracks,
master,
buses,
} if doc.effective_version() >= 2 => {
for (ti, t) in tracks.iter().enumerate() {
let layer_id = t.id.clone().unwrap_or_else(|| format!("layer_{ti}"));
let part = format!("track '{layer_id}'");
t.node.walk(&mut |node| {
node_blockers(node, engine, &mut |cause| {
out.push(StreamBlocker::TracksPart {
part: part.clone(),
cause: Box::new(cause),
});
});
});
}
for m in master {
m.walk(&mut |node| {
node_blockers(node, engine, &mut |cause| {
out.push(StreamBlocker::TracksPart {
part: "the master chain".to_string(),
cause: Box::new(cause),
});
});
});
}
for b in buses {
let part = format!("bus '{}'", b.id);
for fx in &b.effects {
fx.walk(&mut |node| {
node_blockers(node, engine, &mut |cause| {
out.push(StreamBlocker::TracksPart {
part: part.clone(),
cause: Box::new(cause),
});
});
});
}
}
}
Node::Tracks { .. } => {
out.push(StreamBlocker::TracksRoot);
doc.root
.walk(&mut |node| node_blockers(node, engine, &mut |b| out.push(b)));
}
_ => doc
.root
.walk(&mut |node| node_blockers(node, engine, &mut |b| out.push(b))),
}
out.dedup();
out
}
pub fn try_from_doc(doc: &SoundDoc) -> Option<Self> {
if !Self::blockers(doc).is_empty() {
return None;
}
let n = ((doc.duration.clamp(0.0, 600.0) * doc.sample_rate as f32).ceil() as usize).max(1);
let engine = doc.effective_engine();
let root = if matches!(doc.root, Node::Tracks { .. }) {
Root::Tracks(tracks::StreamTracks::build(doc)?)
} else {
Root::Mono(try_src(&doc.root, doc.sample_rate, n, engine, doc.seed)?)
};
Some(StreamGraph {
root,
pos: 0,
engine,
pitch: 1.0,
pitch_target: 1.0,
glide: 1.0,
bend: 1.0,
})
}
pub fn fill(&mut self, out: &mut [f32]) {
let engine = self.engine;
for s in out.iter_mut() {
self.pitch += (self.pitch_target - self.pitch) * self.glide;
let pitch = self.pitch * self.bend;
*s = match &mut self.root {
Root::Mono(src) => src.step(self.pos, pitch, engine),
Root::Tracks(mix) => {
let (l, r) = mix.step(pitch);
0.5 * (l + r)
}
};
self.pos += 1;
}
}
pub fn fill_stereo(&mut self, left: &mut [f32], right: &mut [f32]) {
assert_eq!(left.len(), right.len(), "stereo blocks must match");
let engine = self.engine;
for (l, r) in left.iter_mut().zip(right.iter_mut()) {
self.pitch += (self.pitch_target - self.pitch) * self.glide;
let pitch = self.pitch * self.bend;
match &mut self.root {
Root::Mono(src) => {
let v = src.step(self.pos, pitch, engine);
*l = v;
*r = v;
}
Root::Tracks(mix) => {
let (a, b) = mix.step(pitch);
*l = a;
*r = b;
}
}
self.pos += 1;
}
}
pub fn is_stereo(&self) -> bool {
matches!(self.root, Root::Tracks(_))
}
pub fn set_pitch(&mut self, scale: f32) {
self.pitch = scale.max(0.0);
self.pitch_target = self.pitch;
self.glide = 1.0;
}
pub fn glide_pitch(&mut self, scale: f32, coeff: f32) {
self.pitch_target = scale.max(0.0);
self.glide = if coeff.is_nan() {
1.0
} else {
coeff.clamp(f32::MIN_POSITIVE, 1.0)
};
}
pub fn pitch(&self) -> f32 {
self.pitch
}
pub fn set_bend(&mut self, mul: f32) {
self.bend = mul.max(0.0);
}
pub fn set_cutoff(&mut self, scale: f32) {
let scale = scale.max(0.01);
match &mut self.root {
Root::Mono(src) => src.set_cutoff(scale),
Root::Tracks(mix) => mix.set_cutoff(scale),
}
}
}
pub struct EffectChain {
procs: Vec<Proc>,
pos: usize,
engine: u32,
}
impl EffectChain {
pub fn try_new(nodes: &[Node], sr: u32, engine: u32) -> Option<Self> {
let n = sr as usize;
let procs = nodes
.iter()
.enumerate()
.map(|(i, node)| try_proc(node, sr, n, engine, node_path(0, i)))
.collect::<Option<_>>()?;
Some(EffectChain {
procs,
pos: 0,
engine,
})
}
pub fn process(&mut self, block: &mut [f32]) {
let engine = self.engine;
for x in block.iter_mut() {
let mut v = *x;
for p in self.procs.iter_mut() {
v = p.step(v, self.pos, 1.0, engine);
}
*x = v;
self.pos += 1;
}
}
}