Skip to main content

max / audiofiles

19.0 KB · 601 lines History Blame Raw
1 //! Instrument playback state: voice pool, loaded zones, envelope processing, and voice rendering.
2
3 use audiofiles_core::instrument::{AdsrEnvelope, InstrumentConfig};
4
5 use crate::preview::PreviewBuffer;
6
7 /// ADSR envelope phase for a single voice.
8 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
9 pub enum EnvelopePhase {
10 Idle,
11 Attack,
12 Decay,
13 Sustain,
14 Release,
15 }
16
17 /// A single polyphonic voice.
18 pub struct Voice {
19 /// Whether this voice is currently producing audio.
20 pub active: bool,
21 /// MIDI note number being played.
22 pub note: u8,
23 /// Note-on velocity (0.0 to 1.0).
24 pub velocity: f32,
25 /// Monotonic counter for voice-stealing (oldest = lowest).
26 pub age: u64,
27 /// Fractional sample position for pitch interpolation.
28 pub position: f64,
29 /// Index into `InstrumentPlayback::zone_buffers`.
30 pub zone_index: usize,
31 /// Current envelope phase.
32 pub envelope_phase: EnvelopePhase,
33 /// Current envelope output level (0.0 to 1.0).
34 pub envelope_level: f32,
35 /// Time spent in the current envelope phase (seconds).
36 pub envelope_time: f32,
37 /// Envelope level captured at note-off for smooth release ramp.
38 pub release_start_level: f32,
39 }
40
41 impl Voice {
42 fn new() -> Self {
43 Self {
44 active: false,
45 note: 0,
46 velocity: 0.0,
47 age: 0,
48 position: 0.0,
49 zone_index: 0,
50 envelope_phase: EnvelopePhase::Idle,
51 envelope_level: 0.0,
52 envelope_time: 0.0,
53 release_start_level: 0.0,
54 }
55 }
56 }
57
58 /// A decoded sample buffer assigned to a key zone, ready for playback.
59 pub struct LoadedZone {
60 /// Decoded audio data (interleaved stereo f32).
61 pub buffer: PreviewBuffer,
62 /// MIDI note that plays at original pitch.
63 pub root_note: u8,
64 /// Lowest MIDI note in the zone (inclusive).
65 pub low_note: u8,
66 /// Highest MIDI note in the zone (inclusive).
67 pub high_note: u8,
68 /// Minimum velocity to trigger (0.0 to 1.0).
69 pub vel_low: f32,
70 /// Maximum velocity to trigger (0.0 to 1.0).
71 pub vel_high: f32,
72 }
73
74 /// Instrument playback state managed by the GUI thread.
75 pub struct InstrumentPlayback {
76 /// Current instrument configuration (mode, envelope, voices).
77 pub config: InstrumentConfig,
78 /// Loaded sample zones with decoded audio data.
79 pub zone_buffers: Vec<LoadedZone>,
80 /// Fixed-size voice pool.
81 pub voices: Vec<Voice>,
82 /// Monotonic counter incremented on each note-on for voice-stealing.
83 pub note_counter: u64,
84 /// Whether instrument mode is engaged (MIDI input active).
85 pub active: bool,
86 /// Host sample rate for envelope timing.
87 pub sample_rate: f32,
88 }
89
90 impl InstrumentPlayback {
91 /// Create a new instrument with `max_voices` pre-allocated voices.
92 pub fn new(max_voices: usize) -> Self {
93 let mut voices = Vec::with_capacity(max_voices);
94 for _ in 0..max_voices {
95 voices.push(Voice::new());
96 }
97 Self {
98 config: InstrumentConfig::default(),
99 zone_buffers: Vec::new(),
100 voices,
101 note_counter: 0,
102 active: false,
103 sample_rate: 44100.0,
104 }
105 }
106
107 /// Trigger a note: find a matching zone, allocate a voice, and start the attack phase.
108 ///
109 /// Voice allocation prefers a free (inactive) slot. If none is available, the oldest
110 /// active voice (lowest `age`) is stolen.
111 pub fn note_on(&mut self, note: u8, velocity: u8) {
112 if self.zone_buffers.is_empty() {
113 return;
114 }
115
116 let vel_f = velocity as f32 / 127.0;
117
118 // Find the first zone matching note + velocity range
119 let zone_idx = self
120 .zone_buffers
121 .iter()
122 .position(|z| {
123 note >= z.low_note && note <= z.high_note && vel_f >= z.vel_low && vel_f <= z.vel_high
124 });
125 let Some(zone_idx) = zone_idx else { return };
126
127 // Allocate voice: free slot or steal oldest
128 let voice_idx = self
129 .voices
130 .iter()
131 .position(|v| !v.active)
132 .unwrap_or_else(|| {
133 self.voices
134 .iter()
135 .enumerate()
136 .min_by_key(|(_, v)| v.age)
137 .map(|(i, _)| i)
138 .unwrap_or(0)
139 });
140
141 self.note_counter += 1;
142 let voice = &mut self.voices[voice_idx];
143 voice.active = true;
144 voice.note = note;
145 voice.velocity = vel_f;
146 voice.age = self.note_counter;
147 voice.position = 0.0;
148 voice.zone_index = zone_idx;
149 voice.envelope_phase = EnvelopePhase::Attack;
150 voice.envelope_level = 0.0;
151 voice.envelope_time = 0.0;
152 voice.release_start_level = 0.0;
153 }
154
155 /// Release a note: transition all active voices matching this note to the release phase.
156 pub fn note_off(&mut self, note: u8) {
157 for voice in &mut self.voices {
158 if voice.active && voice.note == note && voice.envelope_phase != EnvelopePhase::Release {
159 voice.release_start_level = voice.envelope_level;
160 voice.envelope_phase = EnvelopePhase::Release;
161 voice.envelope_time = 0.0;
162 }
163 }
164 }
165 }
166
167 /// Advance the ADSR envelope by one sample and return the new level.
168 fn step_envelope(voice: &mut Voice, env: &AdsrEnvelope, dt: f32) -> f32 {
169 voice.envelope_time += dt;
170
171 match voice.envelope_phase {
172 EnvelopePhase::Attack => {
173 if env.attack <= 0.0 {
174 voice.envelope_level = 1.0;
175 voice.envelope_phase = EnvelopePhase::Decay;
176 voice.envelope_time = 0.0;
177 } else {
178 voice.envelope_level = (voice.envelope_time / env.attack).min(1.0);
179 if voice.envelope_level >= 1.0 {
180 voice.envelope_level = 1.0;
181 voice.envelope_phase = EnvelopePhase::Decay;
182 voice.envelope_time = 0.0;
183 }
184 }
185 }
186 EnvelopePhase::Decay => {
187 if env.decay <= 0.0 {
188 voice.envelope_level = env.sustain;
189 voice.envelope_phase = EnvelopePhase::Sustain;
190 voice.envelope_time = 0.0;
191 } else {
192 let progress = (voice.envelope_time / env.decay).min(1.0);
193 voice.envelope_level = 1.0 + (env.sustain - 1.0) * progress;
194 if progress >= 1.0 {
195 voice.envelope_level = env.sustain;
196 voice.envelope_phase = EnvelopePhase::Sustain;
197 voice.envelope_time = 0.0;
198 }
199 }
200 }
201 EnvelopePhase::Sustain => {
202 voice.envelope_level = env.sustain;
203 }
204 EnvelopePhase::Release => {
205 if env.release <= 0.0 {
206 voice.envelope_level = 0.0;
207 voice.envelope_phase = EnvelopePhase::Idle;
208 voice.active = false;
209 } else {
210 let progress = (voice.envelope_time / env.release).min(1.0);
211 voice.envelope_level = voice.release_start_level * (1.0 - progress);
212 if progress >= 1.0 {
213 voice.envelope_level = 0.0;
214 voice.envelope_phase = EnvelopePhase::Idle;
215 voice.active = false;
216 }
217 }
218 }
219 EnvelopePhase::Idle => {
220 voice.envelope_level = 0.0;
221 }
222 }
223
224 voice.envelope_level
225 }
226
227 /// Render all active voices into an f32 mix buffer (additive).
228 ///
229 /// The buffer is **not** zeroed — samples are summed into whatever is already there.
230 /// `channels` is the output channel count, `device_sr` the output sample rate.
231 pub fn render_voices(
232 inst: &mut InstrumentPlayback,
233 buf: &mut [f32],
234 channels: usize,
235 device_sr: u32,
236 ) {
237 if !inst.active || inst.zone_buffers.is_empty() || channels == 0 || device_sr == 0 {
238 return;
239 }
240
241 let num_frames = buf.len() / channels;
242 let dt = 1.0 / device_sr as f32;
243 let envelope = inst.config.envelope;
244
245 for voice in &mut inst.voices {
246 if !voice.active {
247 continue;
248 }
249
250 if voice.zone_index >= inst.zone_buffers.len() {
251 voice.active = false;
252 voice.envelope_phase = EnvelopePhase::Idle;
253 continue;
254 }
255 let zone = &inst.zone_buffers[voice.zone_index];
256 let total_frames = zone.buffer.data.len() / 2; // stereo interleaved
257 if total_frames == 0 {
258 voice.active = false;
259 voice.envelope_phase = EnvelopePhase::Idle;
260 continue;
261 }
262
263 let pitch_ratio =
264 2.0_f64.powf((voice.note as f64 - zone.root_note as f64) / 12.0)
265 * (zone.buffer.sample_rate as f64 / device_sr as f64);
266
267 for frame in 0..num_frames {
268 let pos_int = voice.position as usize;
269 if pos_int >= total_frames {
270 voice.active = false;
271 voice.envelope_phase = EnvelopePhase::Idle;
272 voice.envelope_level = 0.0;
273 break;
274 }
275
276 let env_level = step_envelope(voice, &envelope, dt);
277 if !voice.active {
278 break;
279 }
280
281 let frac = (voice.position - pos_int as f64) as f32;
282 let next = (pos_int + 1).min(total_frames - 1);
283
284 let l0 = zone.buffer.data[pos_int * 2];
285 let r0 = zone.buffer.data[pos_int * 2 + 1];
286 let l1 = zone.buffer.data[next * 2];
287 let r1 = zone.buffer.data[next * 2 + 1];
288
289 let left = l0 + (l1 - l0) * frac;
290 let right = r0 + (r1 - r0) * frac;
291
292 let gain = env_level * voice.velocity;
293 let base = frame * channels;
294 if channels >= 2 {
295 buf[base] += left * gain;
296 buf[base + 1] += right * gain;
297 } else if channels == 1 {
298 buf[base] += (left + right) * 0.5 * gain;
299 }
300
301 voice.position += pitch_ratio;
302 }
303 }
304 }
305
306 #[cfg(test)]
307 mod tests {
308 use super::*;
309
310 fn make_zone(num_frames: usize, root_note: u8, sample_rate: u32) -> LoadedZone {
311 let mut data = Vec::with_capacity(num_frames * 2);
312 for i in 0..num_frames {
313 let val = (i as f32 + 1.0) / num_frames as f32;
314 data.push(val); // L
315 data.push(val); // R
316 }
317 LoadedZone {
318 buffer: PreviewBuffer {
319 data,
320 channels: 2,
321 sample_rate,
322 },
323 root_note,
324 low_note: 0,
325 high_note: 127,
326 vel_low: 0.0,
327 vel_high: 1.0,
328 }
329 }
330
331 fn make_inst(num_frames: usize, root_note: u8) -> InstrumentPlayback {
332 let mut inst = InstrumentPlayback::new(8);
333 inst.zone_buffers.push(make_zone(num_frames, root_note, 44100));
334 inst.active = true;
335 inst.sample_rate = 44100.0;
336 inst
337 }
338
339 #[test]
340 fn new_creates_inactive_voices() {
341 let playback = InstrumentPlayback::new(8);
342 assert_eq!(playback.voices.len(), 8);
343 for voice in &playback.voices {
344 assert!(!voice.active);
345 assert_eq!(voice.envelope_phase, EnvelopePhase::Idle);
346 }
347 }
348
349 #[test]
350 fn new_defaults() {
351 let playback = InstrumentPlayback::new(4);
352 assert!(!playback.active);
353 assert_eq!(playback.zone_buffers.len(), 0);
354 assert_eq!(playback.note_counter, 0);
355 assert!((playback.sample_rate - 44100.0).abs() < f32::EPSILON);
356 }
357
358 #[test]
359 fn note_on_allocates_voice() {
360 let mut inst = make_inst(1000, 60);
361 inst.note_on(60, 100);
362
363 let active: Vec<_> = inst.voices.iter().filter(|v| v.active).collect();
364 assert_eq!(active.len(), 1);
365 assert_eq!(active[0].note, 60);
366 assert_eq!(active[0].envelope_phase, EnvelopePhase::Attack);
367 assert!(active[0].velocity > 0.0);
368 }
369
370 #[test]
371 fn note_on_no_zones_is_noop() {
372 let mut inst = InstrumentPlayback::new(4);
373 inst.active = true;
374 inst.note_on(60, 100);
375 assert!(inst.voices.iter().all(|v| !v.active));
376 }
377
378 #[test]
379 fn note_on_steals_oldest_voice() {
380 let mut inst = make_inst(1000, 60);
381 // Fill all 8 voices
382 for i in 0..8 {
383 inst.note_on(60 + i, 100);
384 }
385 assert_eq!(inst.voices.iter().filter(|v| v.active).count(), 8);
386
387 // 9th note should steal the oldest (note 60, age=1)
388 inst.note_on(80, 100);
389 let stolen = &inst.voices[0]; // voice 0 was the first allocated
390 assert_eq!(stolen.note, 80);
391 assert_eq!(stolen.age, 9);
392 }
393
394 #[test]
395 fn note_off_triggers_release() {
396 let mut inst = make_inst(1000, 60);
397 // Set envelope with non-zero sustain so envelope_level > 0
398 inst.config.envelope.attack = 0.0;
399 inst.config.envelope.sustain = 0.8;
400 inst.note_on(60, 100);
401
402 // Advance a few samples to get past attack
403 let dt = 1.0 / 44100.0;
404 let voice = &mut inst.voices[0];
405 for _ in 0..100 {
406 step_envelope(voice, &inst.config.envelope, dt);
407 }
408
409 inst.note_off(60);
410 let voice = &inst.voices[0];
411 assert_eq!(voice.envelope_phase, EnvelopePhase::Release);
412 assert!(voice.release_start_level > 0.0);
413 }
414
415 #[test]
416 fn note_off_wrong_note_is_noop() {
417 let mut inst = make_inst(1000, 60);
418 inst.note_on(60, 100);
419 inst.note_off(61); // different note
420 assert_eq!(inst.voices[0].envelope_phase, EnvelopePhase::Attack);
421 }
422
423 #[test]
424 fn envelope_attack_ramp() {
425 let env = AdsrEnvelope {
426 attack: 0.01,
427 decay: 0.0,
428 sustain: 1.0,
429 release: 0.0,
430 };
431 let mut voice = Voice::new();
432 voice.active = true;
433 voice.envelope_phase = EnvelopePhase::Attack;
434
435 let dt = 0.001; // 1ms steps
436 for _ in 0..5 {
437 step_envelope(&mut voice, &env, dt);
438 }
439 // At 5ms into 10ms attack, should be ~0.5
440 assert!((voice.envelope_level - 0.5).abs() < 0.01);
441 assert_eq!(voice.envelope_phase, EnvelopePhase::Attack);
442
443 // Complete the attack (zero-length decay transitions immediately to sustain)
444 for _ in 0..6 {
445 step_envelope(&mut voice, &env, dt);
446 }
447 assert_eq!(voice.envelope_phase, EnvelopePhase::Sustain);
448 assert!((voice.envelope_level - 1.0).abs() < f32::EPSILON);
449 }
450
451 #[test]
452 fn envelope_zero_attack_skips() {
453 let env = AdsrEnvelope {
454 attack: 0.0,
455 decay: 0.1,
456 sustain: 0.5,
457 release: 0.0,
458 };
459 let mut voice = Voice::new();
460 voice.active = true;
461 voice.envelope_phase = EnvelopePhase::Attack;
462
463 step_envelope(&mut voice, &env, 0.001);
464 assert_eq!(voice.envelope_level, 1.0);
465 assert_eq!(voice.envelope_phase, EnvelopePhase::Decay);
466 }
467
468 #[test]
469 fn envelope_release_ramps_to_zero() {
470 let env = AdsrEnvelope {
471 attack: 0.0,
472 decay: 0.0,
473 sustain: 1.0,
474 release: 0.01,
475 };
476 let mut voice = Voice::new();
477 voice.active = true;
478 voice.envelope_phase = EnvelopePhase::Release;
479 voice.release_start_level = 0.8;
480
481 let dt = 0.001;
482 for _ in 0..5 {
483 step_envelope(&mut voice, &env, dt);
484 }
485 // At 5ms into 10ms release from 0.8, should be ~0.4
486 assert!((voice.envelope_level - 0.4).abs() < 0.05);
487 assert!(voice.active);
488
489 // Complete release
490 for _ in 0..6 {
491 step_envelope(&mut voice, &env, dt);
492 }
493 assert!(!voice.active);
494 assert_eq!(voice.envelope_phase, EnvelopePhase::Idle);
495 assert_eq!(voice.envelope_level, 0.0);
496 }
497
498 #[test]
499 fn envelope_zero_release_immediately_deactivates() {
500 let env = AdsrEnvelope {
501 attack: 0.0,
502 decay: 0.0,
503 sustain: 1.0,
504 release: 0.0,
505 };
506 let mut voice = Voice::new();
507 voice.active = true;
508 voice.envelope_phase = EnvelopePhase::Release;
509 voice.release_start_level = 1.0;
510
511 step_envelope(&mut voice, &env, 0.001);
512 assert!(!voice.active);
513 assert_eq!(voice.envelope_phase, EnvelopePhase::Idle);
514 }
515
516 #[test]
517 fn render_voices_produces_output() {
518 let mut inst = make_inst(1000, 60);
519 inst.config.envelope.attack = 0.0;
520 inst.config.envelope.sustain = 1.0;
521 inst.note_on(60, 127); // max velocity, root pitch
522
523 let mut buf = vec![0.0f32; 20]; // 10 frames stereo
524 render_voices(&mut inst, &mut buf, 2, 44100);
525
526 // Should have non-zero audio
527 assert!(buf.iter().any(|&s| s != 0.0));
528 }
529
530 #[test]
531 fn render_voices_inactive_produces_silence() {
532 let mut inst = make_inst(1000, 60);
533 inst.active = false;
534 inst.note_on(60, 100);
535
536 let mut buf = vec![0.0f32; 20];
537 render_voices(&mut inst, &mut buf, 2, 44100);
538 assert!(buf.iter().all(|&s| s == 0.0));
539 }
540
541 #[test]
542 fn render_voices_pitch_ratio_at_root() {
543 // At root note with same sample rate, pitch ratio = 1.0
544 let mut inst = make_inst(1000, 60);
545 inst.config.envelope.attack = 0.0;
546 inst.config.envelope.sustain = 1.0;
547 inst.note_on(60, 127);
548
549 let mut buf = vec![0.0f32; 4]; // 2 frames stereo
550 render_voices(&mut inst, &mut buf, 2, 44100);
551
552 // Position should have advanced by ~2.0 (ratio 1.0, 2 frames)
553 let voice = &inst.voices[0];
554 assert!((voice.position - 2.0).abs() < 0.01);
555 }
556
557 #[test]
558 fn render_voices_pitch_ratio_octave_up() {
559 // An octave up (note + 12) should double the pitch ratio
560 let mut inst = make_inst(1000, 60);
561 inst.config.envelope.attack = 0.0;
562 inst.config.envelope.sustain = 1.0;
563 inst.note_on(72, 127); // octave above root
564
565 let mut buf = vec![0.0f32; 4]; // 2 frames stereo
566 render_voices(&mut inst, &mut buf, 2, 44100);
567
568 // Position should have advanced by ~4.0 (ratio 2.0, 2 frames)
569 let voice = &inst.voices[0];
570 assert!((voice.position - 4.0).abs() < 0.01);
571 }
572
573 #[test]
574 fn render_voices_deactivates_at_buffer_end() {
575 let mut inst = make_inst(5, 60); // only 5 frames of audio
576 inst.config.envelope.attack = 0.0;
577 inst.config.envelope.sustain = 1.0;
578 inst.note_on(60, 127);
579
580 let mut buf = vec![0.0f32; 40]; // 20 frames — way past buffer
581 render_voices(&mut inst, &mut buf, 2, 44100);
582
583 assert!(!inst.voices[0].active);
584 }
585
586 #[test]
587 fn render_voices_sums_additively() {
588 let mut inst = make_inst(1000, 60);
589 inst.config.envelope.attack = 0.0;
590 inst.config.envelope.sustain = 1.0;
591
592 // Start with pre-filled buffer
593 let mut buf = vec![0.5f32; 4]; // 2 frames stereo
594 inst.note_on(60, 127);
595 render_voices(&mut inst, &mut buf, 2, 44100);
596
597 // All values should be > 0.5 since voice output is added
598 assert!(buf.iter().all(|&s| s > 0.5));
599 }
600 }
601