use std::collections::BTreeMap;
use super::SCRATCH_FRAMES;
use super::ring::{Controller, Renderer, spsc};
use super::source::AudioSource;
use crate::dsl::{Node, SoundDoc};
use crate::edit::{EditOp, apply_ops};
use crate::patch::Patch;
use crate::player::Player;
const DECLICK_MS: f32 = 5.0;
const CROSSFADE_MIN_MS: f32 = 8.0;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct PatchId(usize);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct InstanceHandle(pub(super) u64);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct ParamId {
patch: usize,
index: usize,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct LayerId {
patch: usize,
index: usize,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct Priority(pub u8);
impl Priority {
pub const LOW: Priority = Priority(0);
pub const NORMAL: Priority = Priority(64);
pub const HIGH: Priority = Priority(128);
pub const CRITICAL: Priority = Priority(255);
}
impl Default for Priority {
fn default() -> Self {
Priority::NORMAL
}
}
#[derive(Clone, Copy, Debug)]
pub struct Tween {
frames: u32,
}
impl Tween {
pub const IMMEDIATE: Tween = Tween { frames: 0 };
pub const fn frames(n: u32) -> Self {
Tween { frames: n }
}
pub fn ms(ms: f32, sample_rate: u32) -> Self {
let f = (ms / 1000.0 * sample_rate as f32).round();
Tween {
frames: if f > 0.0 { f as u32 } else { 0 },
}
}
}
impl Default for Tween {
fn default() -> Self {
Tween::IMMEDIATE
}
}
#[derive(Clone, Copy)]
struct Ramp {
value: f32,
target: f32,
step: f32,
remaining: u32,
}
impl Ramp {
fn new(v: f32) -> Self {
Ramp {
value: v,
target: v,
step: 0.0,
remaining: 0,
}
}
fn set(&mut self, target: f32, tw: Tween) {
self.target = target;
if tw.frames == 0 {
self.value = target;
self.step = 0.0;
self.remaining = 0;
} else {
self.step = (target - self.value) / tw.frames as f32;
self.remaining = tw.frames;
}
}
fn tick(&mut self) -> f32 {
if self.remaining > 0 {
self.value += self.step;
self.remaining -= 1;
if self.remaining == 0 {
self.value = self.target;
}
}
self.value
}
fn at_target(&self) -> bool {
self.remaining == 0
}
}
pub(super) fn balance(pan: f32) -> (f32, f32) {
let l = if pan <= 0.0 { 1.0 } else { 1.0 - pan };
let r = if pan >= 0.0 { 1.0 } else { 1.0 + pan };
(l, r)
}
pub(super) struct Instance {
pub(super) id: u64,
patch: usize,
values: BTreeMap<String, f32>,
layer_gains: BTreeMap<usize, f32>,
player: Player,
fading_in: Option<(Player, Ramp)>,
gain: Ramp,
pan: Ramp,
pub(super) stopping: bool,
priority: Priority,
}
pub struct Engine {
sample_rate: u32,
patches: Vec<Patch>,
pub(super) instances: Vec<Instance>,
next_id: u64,
max_voices: Option<usize>,
buf_a: Vec<f32>,
buf_b: Vec<f32>,
}
impl Engine {
pub fn new(sample_rate: u32) -> Self {
Engine {
sample_rate,
patches: Vec::new(),
instances: Vec::new(),
next_id: 1,
max_voices: None,
buf_a: vec![0.0; SCRATCH_FRAMES * 2],
buf_b: vec![0.0; SCRATCH_FRAMES * 2],
}
}
pub fn set_max_voices(&mut self, max: usize) {
self.max_voices = Some(max.max(1));
}
pub fn max_voices(&self) -> Option<usize> {
self.max_voices
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub fn load(&mut self, doc: &SoundDoc) -> PatchId {
self.load_patch(&Patch {
doc: doc.clone(),
params: Vec::new(),
})
}
pub fn load_patch(&mut self, patch: &Patch) -> PatchId {
self.patches.push(patch.clone());
PatchId(self.patches.len() - 1)
}
pub fn param(&self, patch: PatchId, name: &str) -> Option<ParamId> {
self.patches
.get(patch.0)?
.params
.iter()
.position(|p| p.name == name)
.map(|index| ParamId {
patch: patch.0,
index,
})
}
pub fn layer(&self, patch: PatchId, name: &str) -> Option<LayerId> {
match &self.patches.get(patch.0)?.doc.root {
Node::Tracks { tracks, .. } => tracks
.iter()
.position(|t| t.id.as_deref() == Some(name))
.map(|index| LayerId {
patch: patch.0,
index,
}),
_ => None,
}
}
pub fn play(&mut self, patch: PatchId) -> InstanceHandle {
self.spawn(patch, false, Priority::NORMAL)
}
pub fn play_looping(&mut self, patch: PatchId) -> InstanceHandle {
self.spawn(patch, true, Priority::NORMAL)
}
pub fn play_prioritized(&mut self, patch: PatchId, priority: Priority) -> InstanceHandle {
self.spawn(patch, false, priority)
}
pub fn play_looping_prioritized(
&mut self,
patch: PatchId,
priority: Priority,
) -> InstanceHandle {
self.spawn(patch, true, priority)
}
pub fn set_priority(&mut self, h: InstanceHandle, priority: Priority) {
if let Some(i) = self.instance_mut(h) {
i.priority = priority;
}
}
fn spawn(&mut self, patch: PatchId, looping: bool, priority: Priority) -> InstanceHandle {
if let Some(max) = self.max_voices
&& !self.make_room(max, priority)
{
return InstanceHandle(0);
}
let Some(values) = self.patches.get(patch.0).map(Patch::defaults) else {
return InstanceHandle(0);
};
let doc = self.build_doc(patch.0, &values, &BTreeMap::new());
let player = self.new_player(doc, looping, 0);
let id = self.next_id;
self.next_id += 1;
self.instances.push(Instance {
id,
patch: patch.0,
values,
layer_gains: BTreeMap::new(),
player,
fading_in: None,
gain: Ramp::new(1.0),
pan: Ramp::new(0.0),
stopping: false,
priority,
});
InstanceHandle(id)
}
fn make_room(&mut self, max: usize, priority: Priority) -> bool {
let sounding = self.instances.iter().filter(|i| !i.stopping).count();
if sounding >= max {
let victim = self
.instances
.iter()
.filter(|i| !i.stopping)
.min_by(|a, b| a.priority.cmp(&b.priority).then(a.id.cmp(&b.id)))
.map(|i| (i.id, i.priority));
match victim {
Some((id, vp)) if vp <= priority => {
let fade = Tween::ms(DECLICK_MS, self.sample_rate);
if let Some(v) = self.instances.iter_mut().find(|i| i.id == id) {
v.gain.set(0.0, fade);
v.stopping = true;
}
}
_ => return false,
}
}
if self.instances.len() >= max * 2
&& let Some(pos) = self
.instances
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.priority.cmp(&b.priority).then(a.id.cmp(&b.id)))
.map(|(pos, _)| pos)
{
self.instances.remove(pos);
}
true
}
fn build_doc(
&self,
patch: usize,
values: &BTreeMap<String, f32>,
layer_gains: &BTreeMap<usize, f32>,
) -> SoundDoc {
let p = &self.patches[patch];
let doc = p.instantiate(values).unwrap_or_else(|_| p.doc.clone());
if layer_gains.is_empty() {
return doc;
}
let ops: Vec<EditOp> = layer_gains
.iter()
.map(|(i, g)| EditOp::Set {
path: format!("root.tracks[{i}].gain"),
value: serde_json::json!(g),
})
.collect();
apply_ops(&doc, &ops).unwrap_or(doc)
}
fn new_player(&self, mut doc: SoundDoc, looping: bool, seek: usize) -> Player {
doc.sample_rate = self.sample_rate;
let mut player = Player::new(doc);
player.looping = looping;
player.seek(seek);
player.play();
player
}
fn instance_mut(&mut self, h: InstanceHandle) -> Option<&mut Instance> {
self.instances.iter_mut().find(|i| i.id == h.0)
}
fn find(&self, h: InstanceHandle) -> Option<usize> {
self.instances.iter().position(|i| i.id == h.0)
}
pub fn set_gain(&mut self, h: InstanceHandle, gain: f32, tw: Tween) {
if let Some(i) = self.instance_mut(h) {
i.gain.set(gain.max(0.0), tw);
}
}
pub fn set_pan(&mut self, h: InstanceHandle, pan: f32, tw: Tween) {
if let Some(i) = self.instance_mut(h) {
let pan = if pan.is_nan() {
0.0
} else {
pan.clamp(-1.0, 1.0)
};
i.pan.set(pan, tw);
}
}
pub fn set_param(&mut self, h: InstanceHandle, param: ParamId, value: f32, tw: Tween) {
let Some(idx) = self.find(h) else { return };
if self.instances[idx].patch != param.patch {
return;
}
let Some(name) = self
.patches
.get(param.patch)
.and_then(|p| p.params.get(param.index))
.map(|p| p.name.clone())
else {
return;
};
self.instances[idx].values.insert(name, value);
self.rerender(idx, tw);
}
pub fn set_layer_gain(&mut self, h: InstanceHandle, layer: LayerId, gain: f32, tw: Tween) {
let Some(idx) = self.find(h) else { return };
if self.instances[idx].patch != layer.patch {
return;
}
self.instances[idx].layer_gains.insert(layer.index, gain);
self.rerender(idx, tw);
}
fn rerender(&mut self, idx: usize, tw: Tween) {
let (patch, looping, pos) = {
let i = &self.instances[idx];
(i.patch, i.player.looping, i.player.position())
};
let values = self.instances[idx].values.clone();
let layer_gains = self.instances[idx].layer_gains.clone();
let doc = self.build_doc(patch, &values, &layer_gains);
let fresh = self.new_player(doc, looping, pos);
let fade = if tw.frames == 0 {
Tween::ms(CROSSFADE_MIN_MS, self.sample_rate)
} else {
tw
};
let inst = &mut self.instances[idx];
let outgoing = std::mem::replace(&mut inst.player, fresh);
let w0 = inst.fading_in.as_ref().map_or(0.0, |(_, m)| m.value);
let mut mix = Ramp::new(w0);
mix.set(1.0, fade);
inst.fading_in = Some((outgoing, mix));
}
pub fn stop(&mut self, h: InstanceHandle, fade: Tween) {
let min_fade = Tween::ms(DECLICK_MS, self.sample_rate);
if let Some(i) = self.instance_mut(h) {
let fade = if fade.frames == 0 { min_fade } else { fade };
i.gain.set(0.0, fade);
i.stopping = true;
}
}
pub fn active(&self) -> usize {
self.instances.len()
}
pub fn is_active(&self, h: InstanceHandle) -> bool {
self.instances.iter().any(|i| i.id == h.0)
}
#[cfg(test)]
pub(super) fn fade_weight(&self, idx: usize) -> Option<f32> {
self.instances
.get(idx)?
.fading_in
.as_ref()
.map(|(_, m)| m.value)
}
}
impl AudioSource for Engine {
fn fill(&mut self, out: &mut [f32]) -> usize {
let frames = out.len() / 2;
out.fill(0.0);
if frames == 0 {
return 0;
}
if self.buf_a.len() < out.len() {
self.buf_a.resize(out.len(), 0.0);
self.buf_b.resize(out.len(), 0.0);
}
let (a, b) = (&mut self.buf_a[..out.len()], &mut self.buf_b[..out.len()]);
for inst in self.instances.iter_mut() {
inst.player.fill(a);
if let Some((out_player, _)) = inst.fading_in.as_mut() {
out_player.fill(b);
}
for f in 0..frames {
let g = inst.gain.tick();
let (lg, rg) = balance(inst.pan.tick());
let (mut l, mut r) = (a[f * 2], a[f * 2 + 1]);
if let Some((_, mix)) = inst.fading_in.as_mut() {
let w = mix.tick();
l = l * w + b[f * 2] * (1.0 - w);
r = r * w + b[f * 2 + 1] * (1.0 - w);
}
out[f * 2] += l * g * lg;
out[f * 2 + 1] += r * g * rg;
}
if let Some((_, mix)) = &inst.fading_in
&& mix.at_target()
{
inst.fading_in = None; }
}
self.instances.retain(|i| {
let outgoing_sounding = i.fading_in.as_ref().is_some_and(|(p, _)| p.playing);
(i.player.playing || outgoing_sounding) && !(i.stopping && i.gain.at_target())
});
frames
}
}
impl Engine {
pub fn split(self, ring_frames: usize) -> (Controller, Renderer) {
spsc(self, ring_frames)
}
}