tono_core/instrument/mod.rs
1//! instrument — a playable, pitched, polyphonic instrument built from a patch.
2//!
3//! Turns a [`Patch`](crate::patch::Patch) (a graph + named params) into something you *play* like a
4//! GarageBand software instrument: pick the sound, then [`note_on`](Instrument::note_on) /
5//! [`note_off`](Instrument::note_off) pitched notes with velocity. Each note is a
6//! **voice** — the patch graph rendered at that note's pitch by the byte-identical
7//! streaming renderer, shaped by a **gated** amplitude envelope (attack/decay/
8//! sustain-while-held/release, unlike the graph's fixed-duration `Env`). Voices are
9//! pooled with stealing, and the instrument mixes them to stereo.
10//!
11//! `Instrument` implements [`AudioSource`], so it drops straight onto a cpal /
12//! AudioWorklet callback, or into a [`Mixer`](crate::runtime::Mixer) alongside SFX.
13//!
14//! ```
15//! use tono_core::instrument::{Instrument, InstrumentDesign, Note};
16//! use tono_core::patch::Patch;
17//! use tono_core::dsl::{Node, SoundDoc};
18//! use tono_core::runtime::AudioSource;
19//!
20//! let patch = Patch::new(SoundDoc::new("lead", Node::Sine { freq: 440.0.into() }));
21//! let mut inst = Instrument::new(InstrumentDesign::new(patch), 48_000).unwrap();
22//!
23//! inst.note_on(Note::C4, 0.9); // strike…
24//! let mut out = vec![0.0f32; 512];
25//! inst.fill(&mut out); // …and it sounds
26//! assert!(out.iter().any(|s| s.abs() > 0.0));
27//! inst.note_off(Note::C4); // release
28//! ```
29
30mod design;
31mod envelope;
32mod note;
33
34#[cfg(test)]
35mod tests;
36
37pub use design::{InstrumentDesign, Modulation, PitchMap, PlayMode};
38pub use envelope::EnvGen;
39pub use note::{InstrumentError, Note};
40
41use std::collections::BTreeMap;
42
43use crate::dsl::{Node, SoundDoc, Value, note_to_hz};
44use crate::runtime::AudioSource;
45use crate::streaming::{EffectChain, StreamGraph};
46
47/// Handle to one sounding voice (a single note-on). Stable until the voice is
48/// culled.
49#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
50pub struct VoiceHandle(u64);
51
52/// One detuned, panned copy in a (possibly unison) voice. The detune is baked
53/// into the graph at build; `l`/`r` are its channel gains (already unison-
54/// normalised so a stack isn't louder than a single voice).
55struct UnisonCopy {
56 graph: StreamGraph,
57 l: f32,
58 r: f32,
59}
60
61struct Voice {
62 handle: u64,
63 note: Note,
64 /// The unison stack — one entry unless unison is on. All copies share the
65 /// note pitch (so glide/bend move them together); each is detuned + panned.
66 copies: Vec<UnisonCopy>,
67 /// The frequency the graphs were baked at (the note the voice was built for).
68 /// A live pitch scale of `target.freq() / built_hz` retunes to any other note
69 /// without a rebuild — how mono glide moves between notes.
70 built_hz: f32,
71 env: EnvGen,
72 gain: f32,
73 /// Set once `note_off` has gated the envelope into release.
74 releasing: bool,
75 /// A note-off arrived while the sustain pedal was down — release on pedal-up.
76 sustained: bool,
77}
78
79/// A polyphonic, pitched, gated instrument. Play it with
80/// [`note_on`](Self::note_on) / [`note_off`](Self::note_off); it mixes its live
81/// voices through [`AudioSource::fill`].
82pub struct Instrument {
83 sample_rate: u32,
84 design: InstrumentDesign,
85 /// Current parameter values (name → value); each new voice is built with these.
86 values: BTreeMap<String, f32>,
87 voices: Vec<Voice>,
88 next_handle: u64,
89 /// Sustain-pedal state: while down, note-offs are deferred until pedal-up.
90 sustain: bool,
91 /// Pitch-wheel bend as a frequency ratio (1.0 = centered), applied live to
92 /// every sounding voice and any new one.
93 bend: f32,
94 /// Filter-cutoff (brightness) scale, 1.0 = as designed. Applied live to every
95 /// voice's filters — a mod-wheel / CC74 brightness sweep without a rebuild.
96 brightness: f32,
97 /// Modulation LFO phases in `0..1`, advanced per control block. Phase
98 /// accumulators (not an absolute sample clock) so precision never degrades:
99 /// a `u64` clock cast to `f32` quantizes to whole control blocks after a
100 /// few hours and the LFOs go steppy, then freeze.
101 vib_phase: f32,
102 flt_phase: f32,
103 trem_phase: f32,
104 /// Current tremolo gain (1.0 = no tremolo), updated at control rate.
105 trem: f32,
106 /// Notes physically held, oldest→newest — mono note priority. On a note-off
107 /// the voice falls back to the last still-held note. Unused in poly mode.
108 held: Vec<Note>,
109 /// The shared master effect chain, one instance per stereo channel (identical
110 /// coefficients, independent state) so a reverb/chorus reads as stereo. Both
111 /// are one shared instance for the whole instrument — a tail outlives its note.
112 master: Option<(EffectChain, EffectChain)>,
113 /// Per-copy render scratch (mono).
114 scratch: Vec<f32>,
115 /// Per-voice amp-envelope scratch (one env, shared across its unison copies).
116 env_buf: Vec<f32>,
117 /// Summed-voices stereo bus, fed to the master chains.
118 mix_l: Vec<f32>,
119 mix_r: Vec<f32>,
120}
121
122/// Multiply every pitch-determining frequency (oscillator freqs, seq note
123/// pitches) by `ratio`. Constant and note-name values are transposed; modulated
124/// fundamentals are left as authored.
125///
126/// Looks like `vary::transpose_node` but is deliberately NOT unified with it:
127/// the two walkers invert Modulated/RingMod/Modal handling (live play must
128/// track the key on pitch-determining processors; humanize must not), and
129/// unifying them would change rendered bytes.
130fn transpose(node: &mut Node, ratio: f32) {
131 fn scale(v: &mut Value, ratio: f32) {
132 match v {
133 Value::Const(c) => *c *= ratio,
134 // An unparseable note is left as authored — substituting a silent
135 // A4 (the old unwrap_or(440.0)) would hide the typo.
136 Value::Note(s) => {
137 if let Some(hz) = note_to_hz(s) {
138 *v = Value::Const(hz * ratio);
139 }
140 }
141 Value::Modulated(_) => {}
142 }
143 }
144 match node {
145 Node::Sine { freq }
146 | Node::Triangle { freq }
147 | Node::Sawtooth { freq }
148 | Node::Square { freq, .. }
149 | Node::Fm { freq, .. }
150 | Node::Super { freq, .. }
151 | Node::Wavetable { freq, .. } => scale(freq, ratio),
152 // Pitch-determining processors: the ring-mod carrier and the modal
153 // body's resonant partials must track the note, or a bell/metallic patch
154 // plays the same pitch for every key.
155 Node::RingMod { freq } => scale(freq, ratio),
156 Node::Modal { modes, .. } => {
157 for m in modes.iter_mut() {
158 m.freq *= ratio;
159 }
160 }
161 Node::Seq { notes, .. } => {
162 for note in notes.iter_mut() {
163 scale(&mut note.pitch, ratio);
164 }
165 }
166 _ => {}
167 }
168 // The recursion goes through the shared `children_mut` traversal — every
169 // nesting spot (mix/mul/chain, a tracks' layers and master chain, a
170 // duck's trigger) is covered by construction.
171 node.children_mut().for_each(|c| transpose(c, ratio));
172}
173
174impl Instrument {
175 /// Build an instrument from a design. Errors if the patch can't instantiate
176 /// or its graph is outside the streamable subset — so every note is
177 /// guaranteed to play in real time.
178 pub fn new(design: InstrumentDesign, sample_rate: u32) -> Result<Self, InstrumentError> {
179 let master = if design.master.is_empty() {
180 None
181 } else {
182 let engine = design.patch.doc.effective_engine();
183 let build = || EffectChain::try_new(&design.master, sample_rate, engine);
184 let (l, r) = (
185 build().ok_or(InstrumentError::NotStreamable)?,
186 build().ok_or(InstrumentError::NotStreamable)?,
187 );
188 Some((l, r))
189 };
190 let values = design.patch.defaults();
191 let inst = Instrument {
192 sample_rate,
193 design,
194 values,
195 voices: Vec::new(),
196 next_handle: 1,
197 sustain: false,
198 bend: 1.0,
199 brightness: 1.0,
200 vib_phase: 0.0,
201 flt_phase: 0.0,
202 trem_phase: 0.0,
203 trem: 1.0,
204 held: Vec::new(),
205 master,
206 scratch: Vec::new(),
207 env_buf: Vec::new(),
208 mix_l: Vec::new(),
209 mix_r: Vec::new(),
210 };
211 inst.build_result(Note::A4, 1.0, 1.0)?; // validate the reference voice
212 Ok(inst)
213 }
214
215 /// Build the streamable graph for one note at the current parameter values.
216 /// `detune` is a frequency multiplier baked into the graph (1.0 = none) — a
217 /// unison copy bakes its slight detune here, so glide/bend (which ride the
218 /// live pitch scale, keyed off the *nominal* note) preserve the spread.
219 fn build_result(
220 &self,
221 note: Note,
222 velocity: f32,
223 detune: f32,
224 ) -> Result<StreamGraph, InstrumentError> {
225 let hz = note.freq() * detune;
226 let mut values = self.values.clone();
227 if let PitchMap::Param(name) = &self.design.pitch {
228 values.insert(name.clone(), hz);
229 }
230 if let Some(vp) = &self.design.velocity_param {
231 // Map velocity across the param's declared [min, max] (a musical
232 // range), not the raw 0..1 — which would clamp to the minimum.
233 if let Some(spec) = self.design.patch.params.iter().find(|p| &p.name == vp) {
234 let (lo, hi) = (spec.min.min(spec.max), spec.min.max(spec.max));
235 values.insert(vp.clone(), lo + velocity.clamp(0.0, 1.0) * (hi - lo));
236 }
237 }
238 let mut doc: SoundDoc = self
239 .design
240 .patch
241 .instantiate(&values)
242 .map_err(|e| InstrumentError::BadPatch(e.to_string()))?;
243 doc.sample_rate = self.sample_rate;
244 if let PitchMap::Transpose { reference } = &self.design.pitch {
245 transpose(&mut doc.root, hz / reference.freq());
246 }
247 StreamGraph::try_from_doc(&doc).ok_or(InstrumentError::NotStreamable)
248 }
249
250 /// Build the unison stack for `note`: `unison` detuned, panned, level-
251 /// normalised copies (one copy, centered, when unison is off). `None` if the
252 /// patch can't build.
253 fn build_copies(&self, note: Note, velocity: f32) -> Option<Vec<UnisonCopy>> {
254 let n = self.design.unison.max(1);
255 let norm = 1.0 / (n as f32).sqrt(); // a stack shouldn't be louder than one
256 let mut copies = Vec::with_capacity(n);
257 for k in 0..n {
258 // Spread copies symmetrically over [-1, 1] × the configured amounts.
259 let spread = if n == 1 {
260 0.0
261 } else {
262 (k as f32 / (n - 1) as f32 - 0.5) * 2.0
263 };
264 let detune = 2f32.powf(spread * self.design.detune_cents / 1200.0);
265 let mut graph = self.build_result(note, velocity, detune).ok()?;
266 if self.bend != 1.0 {
267 graph.set_bend(self.bend);
268 }
269 if self.brightness != 1.0 {
270 graph.set_cutoff(self.brightness); // catch a new note up to the knob
271 }
272 // The builder clamps unison_width to 0..1; a deserialized design
273 // may not have come through it — without the clamp a width > 1
274 // flips a copy's polarity (negative gain).
275 let pan = (spread * self.design.unison_width).clamp(-1.0, 1.0);
276 copies.push(UnisonCopy {
277 graph,
278 l: (1.0 - pan).min(1.0) * norm,
279 r: (1.0 + pan).min(1.0) * norm,
280 });
281 }
282 Some(copies)
283 }
284
285 /// Start a note; `velocity` in `[0, 1]` shapes its level. Returns the voice's
286 /// handle. In poly mode each note is its own voice; if the pool is full the
287 /// **quietest** voice is stolen (the least audible cut). In mono mode the one
288 /// voice is retuned (gliding) to the new note. A patch made un-buildable by a
289 /// bad param yields a silent voice rather than panicking — a control event
290 /// never crashes the audio thread. MIDI convention: a note-on with
291 /// `velocity == 0.0` is a note-off (spawning a silent voice for it would
292 /// leak a voice that never releases) and returns the inert handle.
293 pub fn note_on(&mut self, note: Note, velocity: f32) -> VoiceHandle {
294 // NaN folds to 0.0 → the safe reading (note-off), not a loud surprise.
295 let velocity = if velocity.is_nan() {
296 0.0
297 } else {
298 velocity.clamp(0.0, 1.0)
299 };
300 if velocity == 0.0 {
301 self.note_off(note);
302 return VoiceHandle(0);
303 }
304 if let PlayMode::Mono { legato } = self.design.mode {
305 return self.mono_note_on(note, velocity, legato);
306 }
307 let handle = self.next_handle;
308 self.next_handle += 1;
309 self.spawn_voice(handle, note, velocity);
310 VoiceHandle(handle)
311 }
312
313 /// Build a fresh voice at `note` and add it to the pool, stealing the quietest
314 /// if full. A no-op on an un-buildable patch (a bad param) — the caller still
315 /// gets a handle, just a silent voice.
316 fn spawn_voice(&mut self, handle: u64, note: Note, velocity: f32) {
317 let Some(copies) = self.build_copies(note, velocity) else {
318 return; // un-buildable patch ⇒ the caller keeps its handle, the voice is silent
319 };
320 let mut env = EnvGen::new(&self.design.amp, self.sample_rate);
321 env.gate_on();
322 // Steal by forcing the quietest sounding voice into a ~5 ms release —
323 // never a mid-sample cut (an audible click on every steal). The pool
324 // briefly holds the declicking voices on top of max_voices; a note
325 // flood faster than the declick window falls back to hard removal so
326 // the pool stays bounded.
327 let sounding = self.voices.iter().filter(|v| !v.releasing).count();
328 if sounding >= self.design.max_voices
329 && let Some(victim) = self.quietest(|v| !v.releasing)
330 {
331 self.voices[victim].env.kill();
332 self.voices[victim].releasing = true;
333 }
334 if self.voices.len() >= self.design.max_voices * 2
335 && let Some(victim) = self.quietest(|_| true)
336 {
337 self.voices.remove(victim);
338 }
339 self.voices.push(Voice {
340 handle,
341 note,
342 built_hz: note.freq(),
343 copies,
344 env,
345 gain: velocity,
346 releasing: false,
347 sustained: false,
348 });
349 }
350
351 /// The per-sample one-pole coefficient for the configured glide time (`1.0` =
352 /// instant when glide is off).
353 fn glide_coeff(&self) -> f32 {
354 let secs = self.design.glide_secs;
355 if secs <= 0.0 {
356 1.0
357 } else {
358 1.0 - (-1.0 / (secs * self.sample_rate as f32)).exp()
359 }
360 }
361
362 /// Mono note-on: retune the live voice (gliding) to `note`, or strike a fresh
363 /// one if none is sounding. `legato` keeps the amp envelope running.
364 fn mono_note_on(&mut self, note: Note, velocity: f32, legato: bool) -> VoiceHandle {
365 self.held.retain(|&n| n != note);
366 self.held.push(note);
367 let coeff = self.glide_coeff();
368 if let Some(v) = self.voices.iter_mut().find(|v| !v.releasing) {
369 v.note = note;
370 v.sustained = false;
371 let scale = note.freq() / v.built_hz;
372 for c in v.copies.iter_mut() {
373 c.graph.glide_pitch(scale, coeff);
374 }
375 if !legato {
376 v.env.gate_on(); // re-strike unless we're playing legato
377 v.gain = velocity;
378 }
379 VoiceHandle(v.handle)
380 } else {
381 let handle = self.next_handle;
382 self.next_handle += 1;
383 self.spawn_voice(handle, note, velocity); // fresh attack — no glide
384 VoiceHandle(handle)
385 }
386 }
387
388 /// Mono note-off: fall back to the most-recent still-held note (gliding), or
389 /// release the voice (deferred by the sustain pedal) when nothing is held.
390 fn mono_note_off(&mut self, note: Note) -> bool {
391 let before = self.held.len();
392 self.held.retain(|&n| n != note);
393 if self.held.len() == before {
394 return false; // that note wasn't held
395 }
396 match self.held.last().copied() {
397 Some(prev) => {
398 let coeff = self.glide_coeff();
399 if let Some(v) = self.voices.iter_mut().find(|v| !v.releasing) {
400 v.note = prev;
401 let scale = prev.freq() / v.built_hz;
402 for c in v.copies.iter_mut() {
403 c.graph.glide_pitch(scale, coeff);
404 }
405 }
406 true
407 }
408 None => {
409 let sustain = self.sustain;
410 for v in self.voices.iter_mut().filter(|v| !v.releasing) {
411 if sustain {
412 v.sustained = true;
413 } else {
414 v.env.gate_off();
415 v.releasing = true;
416 }
417 }
418 true
419 }
420 }
421 }
422
423 /// Index of the quietest voice among those matching `pick`.
424 fn quietest(&self, pick: impl Fn(&Voice) -> bool) -> Option<usize> {
425 self.voices
426 .iter()
427 .enumerate()
428 .filter(|(_, v)| pick(v))
429 .min_by(|(_, a), (_, b)| a.env.level().total_cmp(&b.env.level()))
430 .map(|(i, _)| i)
431 }
432
433 /// Release the newest still-held voice of `note` (or defer it if the
434 /// sustain pedal is down); returns whether a voice was released/deferred.
435 /// MIDI note-off arrives by pitch, so this is the common path.
436 pub fn note_off(&mut self, note: Note) -> bool {
437 if matches!(self.design.mode, PlayMode::Mono { .. }) {
438 return self.mono_note_off(note);
439 }
440 let sustain = self.sustain;
441 match self
442 .voices
443 .iter_mut()
444 .rev()
445 .find(|v| v.note == note && !v.releasing && !v.sustained)
446 {
447 Some(v) if sustain => {
448 v.sustained = true; // hold until pedal-up
449 true
450 }
451 Some(v) => {
452 v.env.gate_off();
453 v.releasing = true;
454 true
455 }
456 None => false,
457 }
458 }
459
460 /// Set the sustain pedal. While down, note-offs are held; on release, every
461 /// deferred voice enters its release. (MIDI CC64.)
462 pub fn set_sustain(&mut self, down: bool) {
463 self.sustain = down;
464 if !down {
465 for v in self.voices.iter_mut() {
466 if v.sustained {
467 v.env.gate_off();
468 v.releasing = true;
469 v.sustained = false;
470 }
471 }
472 }
473 }
474
475 /// Bend every sounding voice (and any struck later) by `semitones` — the
476 /// pitch wheel. `0.0` is centered; a MIDI pitch wheel maps its ±8192 range to
477 /// your chosen semitone span (commonly ±2). The bend is a pure repitch of the
478 /// oscillators, applied live without rebuilding a voice.
479 pub fn set_bend(&mut self, semitones: f32) {
480 self.bend = 2f32.powf(semitones / 12.0);
481 for v in self.voices.iter_mut() {
482 for c in v.copies.iter_mut() {
483 c.graph.set_bend(self.bend);
484 }
485 }
486 }
487
488 /// Sweep the filter cutoff of every sounding voice (and any struck later) —
489 /// a live brightness control (`scale` multiplies each filter's cutoff, 1.0 =
490 /// as designed). Recomputes coefficients in place, so a knob/CC74 sweep is
491 /// click-free. Voices with no filter are simply unaffected.
492 pub fn set_brightness(&mut self, scale: f32) {
493 self.brightness = scale.max(0.01);
494 for v in self.voices.iter_mut() {
495 for c in v.copies.iter_mut() {
496 c.graph.set_cutoff(self.brightness);
497 }
498 }
499 }
500
501 /// Release a specific voice by handle; returns whether it was found.
502 pub fn release(&mut self, handle: VoiceHandle) -> bool {
503 match self.voices.iter_mut().find(|v| v.handle == handle.0) {
504 Some(v) => {
505 v.env.gate_off();
506 v.releasing = true;
507 true
508 }
509 None => false,
510 }
511 }
512
513 /// Release every held voice.
514 pub fn all_notes_off(&mut self) {
515 self.held.clear();
516 for v in self.voices.iter_mut() {
517 v.env.gate_off();
518 v.releasing = true;
519 }
520 }
521
522 /// Whether a handle still refers to a sounding voice.
523 pub fn is_active(&self, handle: VoiceHandle) -> bool {
524 self.voices.iter().any(|v| v.handle == handle.0)
525 }
526
527 /// The note a live voice is playing.
528 pub fn voice_note(&self, handle: VoiceHandle) -> Option<Note> {
529 self.voices
530 .iter()
531 .find(|v| v.handle == handle.0)
532 .map(|v| v.note)
533 }
534
535 /// The pitch scale a voice is currently sounding at (1.0 = its built note),
536 /// following an in-progress glide, excluding the pitch wheel. Useful for a
537 /// live pitch readout.
538 pub fn voice_pitch_scale(&self, handle: VoiceHandle) -> Option<f32> {
539 self.voices
540 .iter()
541 .find(|v| v.handle == handle.0)
542 .and_then(|v| v.copies.first())
543 .map(|c| c.graph.pitch())
544 }
545
546 /// Set a named parameter for future notes. Returns whether it was accepted —
547 /// rejected (and the previous value kept) if the name is unknown or the value
548 /// would make the patch invalid, so the instrument can never reach an
549 /// un-buildable state.
550 pub fn set_param(&mut self, name: &str, value: f32) -> bool {
551 if !self.design.patch.params.iter().any(|p| p.name == name) {
552 return false;
553 }
554 let prev = self.values.insert(name.to_string(), value);
555 if self.design.patch.instantiate(&self.values).is_ok() {
556 true
557 } else {
558 match prev {
559 Some(p) => self.values.insert(name.to_string(), p),
560 None => self.values.remove(name),
561 };
562 false
563 }
564 }
565
566 /// Number of live voices.
567 pub fn active_voices(&self) -> usize {
568 self.voices.len()
569 }
570}
571
572impl Instrument {
573 /// Update the modulation LFOs at their current phases, apply them to every
574 /// voice (vibrato rides the bend channel, wobble the cutoff, tremolo the
575 /// gain), then advance the phases by this `frames`-long control block.
576 fn apply_modulation(&mut self, frames: usize) {
577 let m = self.design.modulation;
578 let tau = std::f32::consts::TAU;
579 let vib = if m.vibrato_cents > 0.0 {
580 2f32.powf((m.vibrato_cents / 1200.0) * (tau * self.vib_phase).sin())
581 } else {
582 1.0
583 };
584 let flt = if m.filter_octaves > 0.0 {
585 2f32.powf(m.filter_octaves * (tau * self.flt_phase).sin())
586 } else {
587 1.0
588 };
589 self.trem = if m.tremolo_depth > 0.0 {
590 1.0 - m.tremolo_depth * 0.5 * (1.0 - (tau * self.trem_phase).sin())
591 } else {
592 1.0
593 };
594 let step = frames as f32 / self.sample_rate as f32;
595 self.vib_phase = (self.vib_phase + m.vibrato_rate * step).fract();
596 self.flt_phase = (self.flt_phase + m.filter_rate * step).fract();
597 self.trem_phase = (self.trem_phase + m.tremolo_rate * step).fract();
598 let (bend, cutoff) = (self.bend * vib, self.brightness * flt);
599 let wobble = m.filter_octaves > 0.0;
600 for v in self.voices.iter_mut() {
601 for c in v.copies.iter_mut() {
602 c.graph.set_bend(bend);
603 if wobble {
604 c.graph.set_cutoff(cutoff);
605 }
606 }
607 }
608 }
609
610 /// Render one block at the current modulation state (the tremolo gain is
611 /// baked into the amp envelope). Split out so `fill` can drive it at control
612 /// rate when modulation is active.
613 fn render_block(&mut self, out: &mut [f32]) {
614 let frames = out.len() / 2;
615 out.fill(0.0);
616 if frames == 0 {
617 return;
618 }
619 for buf in [
620 &mut self.scratch,
621 &mut self.env_buf,
622 &mut self.mix_l,
623 &mut self.mix_r,
624 ] {
625 if buf.len() < frames {
626 buf.resize(frames, 0.0);
627 }
628 }
629 let trem = self.trem;
630 let copy = &mut self.scratch[..frames]; // per-copy render
631 let env = &mut self.env_buf[..frames]; // per-voice envelope × gain
632 let (mix_l, mix_r) = (&mut self.mix_l[..frames], &mut self.mix_r[..frames]);
633 mix_l.fill(0.0);
634 mix_r.fill(0.0);
635 for v in self.voices.iter_mut() {
636 // The amp envelope advances once per sample and is shared across the
637 // voice's unison copies (they differ only in detune and pan).
638 for e in env.iter_mut() {
639 *e = v.env.tick() * v.gain * trem;
640 }
641 for c in v.copies.iter_mut() {
642 c.graph.fill(copy);
643 for f in 0..frames {
644 let s = copy[f] * env[f];
645 mix_l[f] += s * c.l;
646 mix_r[f] += s * c.r;
647 }
648 }
649 }
650 // One shared master per channel (a reverb tail is not multiplied per
651 // voice); identical coefficients, independent state ⇒ a stereo image.
652 if let Some((chain_l, chain_r)) = &mut self.master {
653 chain_l.process(mix_l);
654 chain_r.process(mix_r);
655 }
656 for f in 0..frames {
657 out[f * 2] = mix_l[f];
658 out[f * 2 + 1] = mix_r[f];
659 }
660 // Cull voices whose envelope has fully released — or a percussive voice
661 // (sustain ≈ 0) that has decayed to silence but never got a note-off.
662 self.voices.retain(|v| v.env.active() && !v.env.faded());
663 }
664}
665
666impl AudioSource for Instrument {
667 fn fill(&mut self, out: &mut [f32]) -> usize {
668 let frames = out.len() / 2;
669 // No modulation ⇒ render the whole block directly (byte-identical to a
670 // pre-modulation instrument: trem stays 1.0).
671 if !self.design.modulation.is_active() {
672 self.render_block(out);
673 return frames;
674 }
675 // Modulated ⇒ step the LFOs at control rate (64-frame sub-blocks) so
676 // vibrato/wobble/tremolo move smoothly without per-sample coefficient cost.
677 const CTRL: usize = 64;
678 let mut done = 0;
679 while done < frames {
680 let n = CTRL.min(frames - done);
681 self.apply_modulation(n);
682 self.render_block(&mut out[done * 2..(done + n) * 2]);
683 done += n;
684 }
685 frames
686 }
687}