mod design;
mod envelope;
mod note;
#[cfg(test)]
mod tests;
pub use design::{InstrumentDesign, Modulation, PitchMap, PlayMode};
pub use envelope::EnvGen;
pub use note::{InstrumentError, Note};
use std::collections::BTreeMap;
use crate::dsl::{Node, SoundDoc, Value, note_to_hz};
use crate::runtime::AudioSource;
use crate::streaming::{EffectChain, StreamGraph};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct VoiceHandle(u64);
struct UnisonCopy {
graph: StreamGraph,
l: f32,
r: f32,
}
struct Voice {
handle: u64,
note: Note,
copies: Vec<UnisonCopy>,
built_hz: f32,
env: EnvGen,
gain: f32,
releasing: bool,
sustained: bool,
}
pub struct Instrument {
sample_rate: u32,
design: InstrumentDesign,
values: BTreeMap<String, f32>,
voices: Vec<Voice>,
next_handle: u64,
sustain: bool,
bend: f32,
brightness: f32,
vib_phase: f32,
flt_phase: f32,
trem_phase: f32,
trem: f32,
held: Vec<Note>,
master: Option<(EffectChain, EffectChain)>,
scratch: Vec<f32>,
env_buf: Vec<f32>,
mix_l: Vec<f32>,
mix_r: Vec<f32>,
}
fn transpose(node: &mut Node, ratio: f32) {
fn scale(v: &mut Value, ratio: f32) {
match v {
Value::Const(c) => *c *= ratio,
Value::Note(s) => {
if let Some(hz) = note_to_hz(s) {
*v = Value::Const(hz * ratio);
}
}
Value::Modulated(_) => {}
}
}
match node {
Node::Sine { freq }
| Node::Triangle { freq }
| Node::Sawtooth { freq }
| Node::Square { freq, .. }
| Node::Fm { freq, .. }
| Node::Super { freq, .. } => scale(freq, ratio),
Node::RingMod { freq } => scale(freq, ratio),
Node::Modal { modes, .. } => {
for m in modes.iter_mut() {
m.freq *= ratio;
}
}
Node::Seq { notes, .. } => {
for note in notes.iter_mut() {
scale(&mut note.pitch, ratio);
}
}
_ => {}
}
node.children_mut().for_each(|c| transpose(c, ratio));
}
impl Instrument {
pub fn new(design: InstrumentDesign, sample_rate: u32) -> Result<Self, InstrumentError> {
let master = if design.master.is_empty() {
None
} else {
let engine = design.patch.doc.effective_engine();
let build = || EffectChain::try_new(&design.master, sample_rate, engine);
let (l, r) = (
build().ok_or(InstrumentError::NotStreamable)?,
build().ok_or(InstrumentError::NotStreamable)?,
);
Some((l, r))
};
let values = design.patch.defaults();
let inst = Instrument {
sample_rate,
design,
values,
voices: Vec::new(),
next_handle: 1,
sustain: false,
bend: 1.0,
brightness: 1.0,
vib_phase: 0.0,
flt_phase: 0.0,
trem_phase: 0.0,
trem: 1.0,
held: Vec::new(),
master,
scratch: Vec::new(),
env_buf: Vec::new(),
mix_l: Vec::new(),
mix_r: Vec::new(),
};
inst.build_result(Note::A4, 1.0, 1.0)?; Ok(inst)
}
fn build_result(
&self,
note: Note,
velocity: f32,
detune: f32,
) -> Result<StreamGraph, InstrumentError> {
let hz = note.freq() * detune;
let mut values = self.values.clone();
if let PitchMap::Param(name) = &self.design.pitch {
values.insert(name.clone(), hz);
}
if let Some(vp) = &self.design.velocity_param {
if let Some(spec) = self.design.patch.params.iter().find(|p| &p.name == vp) {
let (lo, hi) = (spec.min.min(spec.max), spec.min.max(spec.max));
values.insert(vp.clone(), lo + velocity.clamp(0.0, 1.0) * (hi - lo));
}
}
let mut doc: SoundDoc = self
.design
.patch
.instantiate(&values)
.map_err(|e| InstrumentError::BadPatch(e.to_string()))?;
doc.sample_rate = self.sample_rate;
if let PitchMap::Transpose { reference } = &self.design.pitch {
transpose(&mut doc.root, hz / reference.freq());
}
StreamGraph::try_from_doc(&doc).ok_or(InstrumentError::NotStreamable)
}
fn build_copies(&self, note: Note, velocity: f32) -> Option<Vec<UnisonCopy>> {
let n = self.design.unison.max(1);
let norm = 1.0 / (n as f32).sqrt(); let mut copies = Vec::with_capacity(n);
for k in 0..n {
let spread = if n == 1 {
0.0
} else {
(k as f32 / (n - 1) as f32 - 0.5) * 2.0
};
let detune = 2f32.powf(spread * self.design.detune_cents / 1200.0);
let mut graph = self.build_result(note, velocity, detune).ok()?;
if self.bend != 1.0 {
graph.set_bend(self.bend);
}
if self.brightness != 1.0 {
graph.set_cutoff(self.brightness); }
let pan = spread * self.design.unison_width;
copies.push(UnisonCopy {
graph,
l: (1.0 - pan).min(1.0) * norm,
r: (1.0 + pan).min(1.0) * norm,
});
}
Some(copies)
}
pub fn note_on(&mut self, note: Note, velocity: f32) -> VoiceHandle {
let velocity = if velocity.is_nan() {
0.0
} else {
velocity.clamp(0.0, 1.0)
};
if velocity == 0.0 {
self.note_off(note);
return VoiceHandle(0);
}
if let PlayMode::Mono { legato } = self.design.mode {
return self.mono_note_on(note, velocity, legato);
}
let handle = self.next_handle;
self.next_handle += 1;
self.spawn_voice(handle, note, velocity);
VoiceHandle(handle)
}
fn spawn_voice(&mut self, handle: u64, note: Note, velocity: f32) {
let Some(copies) = self.build_copies(note, velocity) else {
return; };
let mut env = EnvGen::new(&self.design.amp, self.sample_rate);
env.gate_on();
let sounding = self.voices.iter().filter(|v| !v.releasing).count();
if sounding >= self.design.max_voices
&& let Some(victim) = self.quietest(|v| !v.releasing)
{
self.voices[victim].env.kill();
self.voices[victim].releasing = true;
}
if self.voices.len() >= self.design.max_voices * 2
&& let Some(victim) = self.quietest(|_| true)
{
self.voices.remove(victim);
}
self.voices.push(Voice {
handle,
note,
built_hz: note.freq(),
copies,
env,
gain: velocity,
releasing: false,
sustained: false,
});
}
fn glide_coeff(&self) -> f32 {
let secs = self.design.glide_secs;
if secs <= 0.0 {
1.0
} else {
1.0 - (-1.0 / (secs * self.sample_rate as f32)).exp()
}
}
fn mono_note_on(&mut self, note: Note, velocity: f32, legato: bool) -> VoiceHandle {
self.held.retain(|&n| n != note);
self.held.push(note);
let coeff = self.glide_coeff();
if let Some(v) = self.voices.iter_mut().find(|v| !v.releasing) {
v.note = note;
v.sustained = false;
let scale = note.freq() / v.built_hz;
for c in v.copies.iter_mut() {
c.graph.glide_pitch(scale, coeff);
}
if !legato {
v.env.gate_on(); v.gain = velocity;
}
VoiceHandle(v.handle)
} else {
let handle = self.next_handle;
self.next_handle += 1;
self.spawn_voice(handle, note, velocity); VoiceHandle(handle)
}
}
fn mono_note_off(&mut self, note: Note) -> bool {
let before = self.held.len();
self.held.retain(|&n| n != note);
if self.held.len() == before {
return false; }
match self.held.last().copied() {
Some(prev) => {
let coeff = self.glide_coeff();
if let Some(v) = self.voices.iter_mut().find(|v| !v.releasing) {
v.note = prev;
let scale = prev.freq() / v.built_hz;
for c in v.copies.iter_mut() {
c.graph.glide_pitch(scale, coeff);
}
}
true
}
None => {
let sustain = self.sustain;
for v in self.voices.iter_mut().filter(|v| !v.releasing) {
if sustain {
v.sustained = true;
} else {
v.env.gate_off();
v.releasing = true;
}
}
true
}
}
}
fn quietest(&self, pick: impl Fn(&Voice) -> bool) -> Option<usize> {
self.voices
.iter()
.enumerate()
.filter(|(_, v)| pick(v))
.min_by(|(_, a), (_, b)| a.env.level().total_cmp(&b.env.level()))
.map(|(i, _)| i)
}
pub fn note_off(&mut self, note: Note) -> bool {
if matches!(self.design.mode, PlayMode::Mono { .. }) {
return self.mono_note_off(note);
}
let sustain = self.sustain;
match self
.voices
.iter_mut()
.rev()
.find(|v| v.note == note && !v.releasing && !v.sustained)
{
Some(v) if sustain => {
v.sustained = true; true
}
Some(v) => {
v.env.gate_off();
v.releasing = true;
true
}
None => false,
}
}
pub fn set_sustain(&mut self, down: bool) {
self.sustain = down;
if !down {
for v in self.voices.iter_mut() {
if v.sustained {
v.env.gate_off();
v.releasing = true;
v.sustained = false;
}
}
}
}
pub fn set_bend(&mut self, semitones: f32) {
self.bend = 2f32.powf(semitones / 12.0);
for v in self.voices.iter_mut() {
for c in v.copies.iter_mut() {
c.graph.set_bend(self.bend);
}
}
}
pub fn set_brightness(&mut self, scale: f32) {
self.brightness = scale.max(0.01);
for v in self.voices.iter_mut() {
for c in v.copies.iter_mut() {
c.graph.set_cutoff(self.brightness);
}
}
}
pub fn release(&mut self, handle: VoiceHandle) -> bool {
match self.voices.iter_mut().find(|v| v.handle == handle.0) {
Some(v) => {
v.env.gate_off();
v.releasing = true;
true
}
None => false,
}
}
pub fn all_notes_off(&mut self) {
self.held.clear();
for v in self.voices.iter_mut() {
v.env.gate_off();
v.releasing = true;
}
}
pub fn is_active(&self, handle: VoiceHandle) -> bool {
self.voices.iter().any(|v| v.handle == handle.0)
}
pub fn voice_note(&self, handle: VoiceHandle) -> Option<Note> {
self.voices
.iter()
.find(|v| v.handle == handle.0)
.map(|v| v.note)
}
pub fn voice_pitch_scale(&self, handle: VoiceHandle) -> Option<f32> {
self.voices
.iter()
.find(|v| v.handle == handle.0)
.and_then(|v| v.copies.first())
.map(|c| c.graph.pitch())
}
pub fn set_param(&mut self, name: &str, value: f32) -> bool {
if !self.design.patch.params.iter().any(|p| p.name == name) {
return false;
}
let prev = self.values.insert(name.to_string(), value);
if self.design.patch.instantiate(&self.values).is_ok() {
true
} else {
match prev {
Some(p) => self.values.insert(name.to_string(), p),
None => self.values.remove(name),
};
false
}
}
pub fn active_voices(&self) -> usize {
self.voices.len()
}
}
impl Instrument {
fn apply_modulation(&mut self, frames: usize) {
let m = self.design.modulation;
let tau = std::f32::consts::TAU;
let vib = if m.vibrato_cents > 0.0 {
2f32.powf((m.vibrato_cents / 1200.0) * (tau * self.vib_phase).sin())
} else {
1.0
};
let flt = if m.filter_octaves > 0.0 {
2f32.powf(m.filter_octaves * (tau * self.flt_phase).sin())
} else {
1.0
};
self.trem = if m.tremolo_depth > 0.0 {
1.0 - m.tremolo_depth * 0.5 * (1.0 - (tau * self.trem_phase).sin())
} else {
1.0
};
let step = frames as f32 / self.sample_rate as f32;
self.vib_phase = (self.vib_phase + m.vibrato_rate * step).fract();
self.flt_phase = (self.flt_phase + m.filter_rate * step).fract();
self.trem_phase = (self.trem_phase + m.tremolo_rate * step).fract();
let (bend, cutoff) = (self.bend * vib, self.brightness * flt);
let wobble = m.filter_octaves > 0.0;
for v in self.voices.iter_mut() {
for c in v.copies.iter_mut() {
c.graph.set_bend(bend);
if wobble {
c.graph.set_cutoff(cutoff);
}
}
}
}
fn render_block(&mut self, out: &mut [f32]) {
let frames = out.len() / 2;
out.fill(0.0);
if frames == 0 {
return;
}
for buf in [
&mut self.scratch,
&mut self.env_buf,
&mut self.mix_l,
&mut self.mix_r,
] {
if buf.len() < frames {
buf.resize(frames, 0.0);
}
}
let trem = self.trem;
let copy = &mut self.scratch[..frames]; let env = &mut self.env_buf[..frames]; let (mix_l, mix_r) = (&mut self.mix_l[..frames], &mut self.mix_r[..frames]);
mix_l.fill(0.0);
mix_r.fill(0.0);
for v in self.voices.iter_mut() {
for e in env.iter_mut() {
*e = v.env.tick() * v.gain * trem;
}
for c in v.copies.iter_mut() {
c.graph.fill(copy);
for f in 0..frames {
let s = copy[f] * env[f];
mix_l[f] += s * c.l;
mix_r[f] += s * c.r;
}
}
}
if let Some((chain_l, chain_r)) = &mut self.master {
chain_l.process(mix_l);
chain_r.process(mix_r);
}
for f in 0..frames {
out[f * 2] = mix_l[f];
out[f * 2 + 1] = mix_r[f];
}
self.voices.retain(|v| v.env.active() && !v.env.faded());
}
}
impl AudioSource for Instrument {
fn fill(&mut self, out: &mut [f32]) -> usize {
let frames = out.len() / 2;
if !self.design.modulation.is_active() {
self.render_block(out);
return frames;
}
const CTRL: usize = 64;
let mut done = 0;
while done < frames {
let n = CTRL.min(frames - done);
self.apply_modulation(n);
self.render_block(&mut out[done * 2..(done + n) * 2]);
done += n;
}
frames
}
}