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, .. } => scale(freq, ratio),
151 // Pitch-determining processors: the ring-mod carrier and the modal
152 // body's resonant partials must track the note, or a bell/metallic patch
153 // plays the same pitch for every key.
154 Node::RingMod { freq } => scale(freq, ratio),
155 Node::Modal { modes, .. } => {
156 for m in modes.iter_mut() {
157 m.freq *= ratio;
158 }
159 }
160 Node::Seq { notes, .. } => {
161 for note in notes.iter_mut() {
162 scale(&mut note.pitch, ratio);
163 }
164 }
165 _ => {}
166 }
167 // The recursion goes through the shared `children_mut` traversal — every
168 // nesting spot (mix/mul/chain, a tracks' layers and master chain, a
169 // duck's trigger) is covered by construction.
170 node.children_mut().for_each(|c| transpose(c, ratio));
171}
172
173impl Instrument {
174 /// Build an instrument from a design. Errors if the patch can't instantiate
175 /// or its graph is outside the streamable subset — so every note is
176 /// guaranteed to play in real time.
177 pub fn new(design: InstrumentDesign, sample_rate: u32) -> Result<Self, InstrumentError> {
178 let master = if design.master.is_empty() {
179 None
180 } else {
181 let engine = design.patch.doc.effective_engine();
182 let build = || EffectChain::try_new(&design.master, sample_rate, engine);
183 let (l, r) = (
184 build().ok_or(InstrumentError::NotStreamable)?,
185 build().ok_or(InstrumentError::NotStreamable)?,
186 );
187 Some((l, r))
188 };
189 let values = design.patch.defaults();
190 let inst = Instrument {
191 sample_rate,
192 design,
193 values,
194 voices: Vec::new(),
195 next_handle: 1,
196 sustain: false,
197 bend: 1.0,
198 brightness: 1.0,
199 vib_phase: 0.0,
200 flt_phase: 0.0,
201 trem_phase: 0.0,
202 trem: 1.0,
203 held: Vec::new(),
204 master,
205 scratch: Vec::new(),
206 env_buf: Vec::new(),
207 mix_l: Vec::new(),
208 mix_r: Vec::new(),
209 };
210 inst.build_result(Note::A4, 1.0, 1.0)?; // validate the reference voice
211 Ok(inst)
212 }
213
214 /// Build the streamable graph for one note at the current parameter values.
215 /// `detune` is a frequency multiplier baked into the graph (1.0 = none) — a
216 /// unison copy bakes its slight detune here, so glide/bend (which ride the
217 /// live pitch scale, keyed off the *nominal* note) preserve the spread.
218 fn build_result(
219 &self,
220 note: Note,
221 velocity: f32,
222 detune: f32,
223 ) -> Result<StreamGraph, InstrumentError> {
224 let hz = note.freq() * detune;
225 let mut values = self.values.clone();
226 if let PitchMap::Param(name) = &self.design.pitch {
227 values.insert(name.clone(), hz);
228 }
229 if let Some(vp) = &self.design.velocity_param {
230 // Map velocity across the param's declared [min, max] (a musical
231 // range), not the raw 0..1 — which would clamp to the minimum.
232 if let Some(spec) = self.design.patch.params.iter().find(|p| &p.name == vp) {
233 let (lo, hi) = (spec.min.min(spec.max), spec.min.max(spec.max));
234 values.insert(vp.clone(), lo + velocity.clamp(0.0, 1.0) * (hi - lo));
235 }
236 }
237 let mut doc: SoundDoc = self
238 .design
239 .patch
240 .instantiate(&values)
241 .map_err(|e| InstrumentError::BadPatch(e.to_string()))?;
242 doc.sample_rate = self.sample_rate;
243 if let PitchMap::Transpose { reference } = &self.design.pitch {
244 transpose(&mut doc.root, hz / reference.freq());
245 }
246 StreamGraph::try_from_doc(&doc).ok_or(InstrumentError::NotStreamable)
247 }
248
249 /// Build the unison stack for `note`: `unison` detuned, panned, level-
250 /// normalised copies (one copy, centered, when unison is off). `None` if the
251 /// patch can't build.
252 fn build_copies(&self, note: Note, velocity: f32) -> Option<Vec<UnisonCopy>> {
253 let n = self.design.unison.max(1);
254 let norm = 1.0 / (n as f32).sqrt(); // a stack shouldn't be louder than one
255 let mut copies = Vec::with_capacity(n);
256 for k in 0..n {
257 // Spread copies symmetrically over [-1, 1] × the configured amounts.
258 let spread = if n == 1 {
259 0.0
260 } else {
261 (k as f32 / (n - 1) as f32 - 0.5) * 2.0
262 };
263 let detune = 2f32.powf(spread * self.design.detune_cents / 1200.0);
264 let mut graph = self.build_result(note, velocity, detune).ok()?;
265 if self.bend != 1.0 {
266 graph.set_bend(self.bend);
267 }
268 if self.brightness != 1.0 {
269 graph.set_cutoff(self.brightness); // catch a new note up to the knob
270 }
271 let pan = spread * self.design.unison_width;
272 copies.push(UnisonCopy {
273 graph,
274 l: (1.0 - pan).min(1.0) * norm,
275 r: (1.0 + pan).min(1.0) * norm,
276 });
277 }
278 Some(copies)
279 }
280
281 /// Start a note; `velocity` in `[0, 1]` shapes its level. Returns the voice's
282 /// handle. In poly mode each note is its own voice; if the pool is full the
283 /// **quietest** voice is stolen (the least audible cut). In mono mode the one
284 /// voice is retuned (gliding) to the new note. A patch made un-buildable by a
285 /// bad param yields a silent voice rather than panicking — a control event
286 /// never crashes the audio thread. MIDI convention: a note-on with
287 /// `velocity == 0.0` is a note-off (spawning a silent voice for it would
288 /// leak a voice that never releases) and returns the inert handle.
289 pub fn note_on(&mut self, note: Note, velocity: f32) -> VoiceHandle {
290 // NaN folds to 0.0 → the safe reading (note-off), not a loud surprise.
291 let velocity = if velocity.is_nan() {
292 0.0
293 } else {
294 velocity.clamp(0.0, 1.0)
295 };
296 if velocity == 0.0 {
297 self.note_off(note);
298 return VoiceHandle(0);
299 }
300 if let PlayMode::Mono { legato } = self.design.mode {
301 return self.mono_note_on(note, velocity, legato);
302 }
303 let handle = self.next_handle;
304 self.next_handle += 1;
305 self.spawn_voice(handle, note, velocity);
306 VoiceHandle(handle)
307 }
308
309 /// Build a fresh voice at `note` and add it to the pool, stealing the quietest
310 /// if full. A no-op on an un-buildable patch (a bad param) — the caller still
311 /// gets a handle, just a silent voice.
312 fn spawn_voice(&mut self, handle: u64, note: Note, velocity: f32) {
313 let Some(copies) = self.build_copies(note, velocity) else {
314 return; // un-buildable patch ⇒ the caller keeps its handle, the voice is silent
315 };
316 let mut env = EnvGen::new(&self.design.amp, self.sample_rate);
317 env.gate_on();
318 // Steal by forcing the quietest sounding voice into a ~5 ms release —
319 // never a mid-sample cut (an audible click on every steal). The pool
320 // briefly holds the declicking voices on top of max_voices; a note
321 // flood faster than the declick window falls back to hard removal so
322 // the pool stays bounded.
323 let sounding = self.voices.iter().filter(|v| !v.releasing).count();
324 if sounding >= self.design.max_voices
325 && let Some(victim) = self.quietest(|v| !v.releasing)
326 {
327 self.voices[victim].env.kill();
328 self.voices[victim].releasing = true;
329 }
330 if self.voices.len() >= self.design.max_voices * 2
331 && let Some(victim) = self.quietest(|_| true)
332 {
333 self.voices.remove(victim);
334 }
335 self.voices.push(Voice {
336 handle,
337 note,
338 built_hz: note.freq(),
339 copies,
340 env,
341 gain: velocity,
342 releasing: false,
343 sustained: false,
344 });
345 }
346
347 /// The per-sample one-pole coefficient for the configured glide time (`1.0` =
348 /// instant when glide is off).
349 fn glide_coeff(&self) -> f32 {
350 let secs = self.design.glide_secs;
351 if secs <= 0.0 {
352 1.0
353 } else {
354 1.0 - (-1.0 / (secs * self.sample_rate as f32)).exp()
355 }
356 }
357
358 /// Mono note-on: retune the live voice (gliding) to `note`, or strike a fresh
359 /// one if none is sounding. `legato` keeps the amp envelope running.
360 fn mono_note_on(&mut self, note: Note, velocity: f32, legato: bool) -> VoiceHandle {
361 self.held.retain(|&n| n != note);
362 self.held.push(note);
363 let coeff = self.glide_coeff();
364 if let Some(v) = self.voices.iter_mut().find(|v| !v.releasing) {
365 v.note = note;
366 v.sustained = false;
367 let scale = note.freq() / v.built_hz;
368 for c in v.copies.iter_mut() {
369 c.graph.glide_pitch(scale, coeff);
370 }
371 if !legato {
372 v.env.gate_on(); // re-strike unless we're playing legato
373 v.gain = velocity;
374 }
375 VoiceHandle(v.handle)
376 } else {
377 let handle = self.next_handle;
378 self.next_handle += 1;
379 self.spawn_voice(handle, note, velocity); // fresh attack — no glide
380 VoiceHandle(handle)
381 }
382 }
383
384 /// Mono note-off: fall back to the most-recent still-held note (gliding), or
385 /// release the voice (deferred by the sustain pedal) when nothing is held.
386 fn mono_note_off(&mut self, note: Note) -> bool {
387 let before = self.held.len();
388 self.held.retain(|&n| n != note);
389 if self.held.len() == before {
390 return false; // that note wasn't held
391 }
392 match self.held.last().copied() {
393 Some(prev) => {
394 let coeff = self.glide_coeff();
395 if let Some(v) = self.voices.iter_mut().find(|v| !v.releasing) {
396 v.note = prev;
397 let scale = prev.freq() / v.built_hz;
398 for c in v.copies.iter_mut() {
399 c.graph.glide_pitch(scale, coeff);
400 }
401 }
402 true
403 }
404 None => {
405 let sustain = self.sustain;
406 for v in self.voices.iter_mut().filter(|v| !v.releasing) {
407 if sustain {
408 v.sustained = true;
409 } else {
410 v.env.gate_off();
411 v.releasing = true;
412 }
413 }
414 true
415 }
416 }
417 }
418
419 /// Index of the quietest voice among those matching `pick`.
420 fn quietest(&self, pick: impl Fn(&Voice) -> bool) -> Option<usize> {
421 self.voices
422 .iter()
423 .enumerate()
424 .filter(|(_, v)| pick(v))
425 .min_by(|(_, a), (_, b)| a.env.level().total_cmp(&b.env.level()))
426 .map(|(i, _)| i)
427 }
428
429 /// Release the newest still-held voice of `note` (or defer it if the
430 /// sustain pedal is down); returns whether a voice was released/deferred.
431 /// MIDI note-off arrives by pitch, so this is the common path.
432 pub fn note_off(&mut self, note: Note) -> bool {
433 if matches!(self.design.mode, PlayMode::Mono { .. }) {
434 return self.mono_note_off(note);
435 }
436 let sustain = self.sustain;
437 match self
438 .voices
439 .iter_mut()
440 .rev()
441 .find(|v| v.note == note && !v.releasing && !v.sustained)
442 {
443 Some(v) if sustain => {
444 v.sustained = true; // hold until pedal-up
445 true
446 }
447 Some(v) => {
448 v.env.gate_off();
449 v.releasing = true;
450 true
451 }
452 None => false,
453 }
454 }
455
456 /// Set the sustain pedal. While down, note-offs are held; on release, every
457 /// deferred voice enters its release. (MIDI CC64.)
458 pub fn set_sustain(&mut self, down: bool) {
459 self.sustain = down;
460 if !down {
461 for v in self.voices.iter_mut() {
462 if v.sustained {
463 v.env.gate_off();
464 v.releasing = true;
465 v.sustained = false;
466 }
467 }
468 }
469 }
470
471 /// Bend every sounding voice (and any struck later) by `semitones` — the
472 /// pitch wheel. `0.0` is centered; a MIDI pitch wheel maps its ±8192 range to
473 /// your chosen semitone span (commonly ±2). The bend is a pure repitch of the
474 /// oscillators, applied live without rebuilding a voice.
475 pub fn set_bend(&mut self, semitones: f32) {
476 self.bend = 2f32.powf(semitones / 12.0);
477 for v in self.voices.iter_mut() {
478 for c in v.copies.iter_mut() {
479 c.graph.set_bend(self.bend);
480 }
481 }
482 }
483
484 /// Sweep the filter cutoff of every sounding voice (and any struck later) —
485 /// a live brightness control (`scale` multiplies each filter's cutoff, 1.0 =
486 /// as designed). Recomputes coefficients in place, so a knob/CC74 sweep is
487 /// click-free. Voices with no filter are simply unaffected.
488 pub fn set_brightness(&mut self, scale: f32) {
489 self.brightness = scale.max(0.01);
490 for v in self.voices.iter_mut() {
491 for c in v.copies.iter_mut() {
492 c.graph.set_cutoff(self.brightness);
493 }
494 }
495 }
496
497 /// Release a specific voice by handle; returns whether it was found.
498 pub fn release(&mut self, handle: VoiceHandle) -> bool {
499 match self.voices.iter_mut().find(|v| v.handle == handle.0) {
500 Some(v) => {
501 v.env.gate_off();
502 v.releasing = true;
503 true
504 }
505 None => false,
506 }
507 }
508
509 /// Release every held voice.
510 pub fn all_notes_off(&mut self) {
511 self.held.clear();
512 for v in self.voices.iter_mut() {
513 v.env.gate_off();
514 v.releasing = true;
515 }
516 }
517
518 /// Whether a handle still refers to a sounding voice.
519 pub fn is_active(&self, handle: VoiceHandle) -> bool {
520 self.voices.iter().any(|v| v.handle == handle.0)
521 }
522
523 /// The note a live voice is playing.
524 pub fn voice_note(&self, handle: VoiceHandle) -> Option<Note> {
525 self.voices
526 .iter()
527 .find(|v| v.handle == handle.0)
528 .map(|v| v.note)
529 }
530
531 /// The pitch scale a voice is currently sounding at (1.0 = its built note),
532 /// following an in-progress glide, excluding the pitch wheel. Useful for a
533 /// live pitch readout.
534 pub fn voice_pitch_scale(&self, handle: VoiceHandle) -> Option<f32> {
535 self.voices
536 .iter()
537 .find(|v| v.handle == handle.0)
538 .and_then(|v| v.copies.first())
539 .map(|c| c.graph.pitch())
540 }
541
542 /// Set a named parameter for future notes. Returns whether it was accepted —
543 /// rejected (and the previous value kept) if the name is unknown or the value
544 /// would make the patch invalid, so the instrument can never reach an
545 /// un-buildable state.
546 pub fn set_param(&mut self, name: &str, value: f32) -> bool {
547 if !self.design.patch.params.iter().any(|p| p.name == name) {
548 return false;
549 }
550 let prev = self.values.insert(name.to_string(), value);
551 if self.design.patch.instantiate(&self.values).is_ok() {
552 true
553 } else {
554 match prev {
555 Some(p) => self.values.insert(name.to_string(), p),
556 None => self.values.remove(name),
557 };
558 false
559 }
560 }
561
562 /// Number of live voices.
563 pub fn active_voices(&self) -> usize {
564 self.voices.len()
565 }
566}
567
568impl Instrument {
569 /// Update the modulation LFOs at their current phases, apply them to every
570 /// voice (vibrato rides the bend channel, wobble the cutoff, tremolo the
571 /// gain), then advance the phases by this `frames`-long control block.
572 fn apply_modulation(&mut self, frames: usize) {
573 let m = self.design.modulation;
574 let tau = std::f32::consts::TAU;
575 let vib = if m.vibrato_cents > 0.0 {
576 2f32.powf((m.vibrato_cents / 1200.0) * (tau * self.vib_phase).sin())
577 } else {
578 1.0
579 };
580 let flt = if m.filter_octaves > 0.0 {
581 2f32.powf(m.filter_octaves * (tau * self.flt_phase).sin())
582 } else {
583 1.0
584 };
585 self.trem = if m.tremolo_depth > 0.0 {
586 1.0 - m.tremolo_depth * 0.5 * (1.0 - (tau * self.trem_phase).sin())
587 } else {
588 1.0
589 };
590 let step = frames as f32 / self.sample_rate as f32;
591 self.vib_phase = (self.vib_phase + m.vibrato_rate * step).fract();
592 self.flt_phase = (self.flt_phase + m.filter_rate * step).fract();
593 self.trem_phase = (self.trem_phase + m.tremolo_rate * step).fract();
594 let (bend, cutoff) = (self.bend * vib, self.brightness * flt);
595 let wobble = m.filter_octaves > 0.0;
596 for v in self.voices.iter_mut() {
597 for c in v.copies.iter_mut() {
598 c.graph.set_bend(bend);
599 if wobble {
600 c.graph.set_cutoff(cutoff);
601 }
602 }
603 }
604 }
605
606 /// Render one block at the current modulation state (the tremolo gain is
607 /// baked into the amp envelope). Split out so `fill` can drive it at control
608 /// rate when modulation is active.
609 fn render_block(&mut self, out: &mut [f32]) {
610 let frames = out.len() / 2;
611 out.fill(0.0);
612 if frames == 0 {
613 return;
614 }
615 for buf in [
616 &mut self.scratch,
617 &mut self.env_buf,
618 &mut self.mix_l,
619 &mut self.mix_r,
620 ] {
621 if buf.len() < frames {
622 buf.resize(frames, 0.0);
623 }
624 }
625 let trem = self.trem;
626 let copy = &mut self.scratch[..frames]; // per-copy render
627 let env = &mut self.env_buf[..frames]; // per-voice envelope × gain
628 let (mix_l, mix_r) = (&mut self.mix_l[..frames], &mut self.mix_r[..frames]);
629 mix_l.fill(0.0);
630 mix_r.fill(0.0);
631 for v in self.voices.iter_mut() {
632 // The amp envelope advances once per sample and is shared across the
633 // voice's unison copies (they differ only in detune and pan).
634 for e in env.iter_mut() {
635 *e = v.env.tick() * v.gain * trem;
636 }
637 for c in v.copies.iter_mut() {
638 c.graph.fill(copy);
639 for f in 0..frames {
640 let s = copy[f] * env[f];
641 mix_l[f] += s * c.l;
642 mix_r[f] += s * c.r;
643 }
644 }
645 }
646 // One shared master per channel (a reverb tail is not multiplied per
647 // voice); identical coefficients, independent state ⇒ a stereo image.
648 if let Some((chain_l, chain_r)) = &mut self.master {
649 chain_l.process(mix_l);
650 chain_r.process(mix_r);
651 }
652 for f in 0..frames {
653 out[f * 2] = mix_l[f];
654 out[f * 2 + 1] = mix_r[f];
655 }
656 // Cull voices whose envelope has fully released — or a percussive voice
657 // (sustain ≈ 0) that has decayed to silence but never got a note-off.
658 self.voices.retain(|v| v.env.active() && !v.env.faded());
659 }
660}
661
662impl AudioSource for Instrument {
663 fn fill(&mut self, out: &mut [f32]) -> usize {
664 let frames = out.len() / 2;
665 // No modulation ⇒ render the whole block directly (byte-identical to a
666 // pre-modulation instrument: trem stays 1.0).
667 if !self.design.modulation.is_active() {
668 self.render_block(out);
669 return frames;
670 }
671 // Modulated ⇒ step the LFOs at control rate (64-frame sub-blocks) so
672 // vibrato/wobble/tremolo move smoothly without per-sample coefficient cost.
673 const CTRL: usize = 64;
674 let mut done = 0;
675 while done < frames {
676 let n = CTRL.min(frames - done);
677 self.apply_modulation(n);
678 self.render_block(&mut out[done * 2..(done + n) * 2]);
679 done += n;
680 }
681 frames
682 }
683}