//! Instrument playback state: voice pool, loaded zones, envelope processing, and voice rendering. use audiofiles_core::instrument::{AdsrEnvelope, InstrumentConfig}; use crate::preview::PreviewBuffer; /// ADSR envelope phase for a single voice. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EnvelopePhase { Idle, Attack, Decay, Sustain, Release, } /// A single polyphonic voice. pub struct Voice { /// Whether this voice is currently producing audio. pub active: bool, /// MIDI note number being played. pub note: u8, /// Note-on velocity (0.0 to 1.0). pub velocity: f32, /// Monotonic counter for voice-stealing (oldest = lowest). pub age: u64, /// Fractional sample position for pitch interpolation. pub position: f64, /// Index into `InstrumentPlayback::zone_buffers`. pub zone_index: usize, /// Current envelope phase. pub envelope_phase: EnvelopePhase, /// Current envelope output level (0.0 to 1.0). pub envelope_level: f32, /// Time spent in the current envelope phase (seconds). pub envelope_time: f32, /// Envelope level captured at note-off for smooth release ramp. pub release_start_level: f32, } impl Voice { fn new() -> Self { Self { active: false, note: 0, velocity: 0.0, age: 0, position: 0.0, zone_index: 0, envelope_phase: EnvelopePhase::Idle, envelope_level: 0.0, envelope_time: 0.0, release_start_level: 0.0, } } } /// A decoded sample buffer assigned to a key zone, ready for playback. pub struct LoadedZone { /// Decoded audio data (interleaved stereo f32). pub buffer: PreviewBuffer, /// MIDI note that plays at original pitch. pub root_note: u8, /// Lowest MIDI note in the zone (inclusive). pub low_note: u8, /// Highest MIDI note in the zone (inclusive). pub high_note: u8, /// Minimum velocity to trigger (0.0 to 1.0). pub vel_low: f32, /// Maximum velocity to trigger (0.0 to 1.0). pub vel_high: f32, } /// Instrument playback state managed by the GUI thread. pub struct InstrumentPlayback { /// Current instrument configuration (mode, envelope, voices). pub config: InstrumentConfig, /// Loaded sample zones with decoded audio data. pub zone_buffers: Vec, /// Fixed-size voice pool. pub voices: Vec, /// Monotonic counter incremented on each note-on for voice-stealing. pub note_counter: u64, /// Whether instrument mode is engaged (MIDI input active). pub active: bool, /// Host sample rate for envelope timing. pub sample_rate: f32, } impl InstrumentPlayback { /// Create a new instrument with `max_voices` pre-allocated voices. pub fn new(max_voices: usize) -> Self { let mut voices = Vec::with_capacity(max_voices); for _ in 0..max_voices { voices.push(Voice::new()); } Self { config: InstrumentConfig::default(), zone_buffers: Vec::new(), voices, note_counter: 0, active: false, sample_rate: 44100.0, } } /// Trigger a note: find a matching zone, allocate a voice, and start the attack phase. /// /// Voice allocation prefers a free (inactive) slot. If none is available, the oldest /// active voice (lowest `age`) is stolen. pub fn note_on(&mut self, note: u8, velocity: u8) { if self.zone_buffers.is_empty() { return; } let vel_f = velocity as f32 / 127.0; // Find the first zone matching note + velocity range let zone_idx = self .zone_buffers .iter() .position(|z| { note >= z.low_note && note <= z.high_note && vel_f >= z.vel_low && vel_f <= z.vel_high }); let Some(zone_idx) = zone_idx else { return }; // Allocate voice: free slot or steal oldest let voice_idx = self .voices .iter() .position(|v| !v.active) .unwrap_or_else(|| { self.voices .iter() .enumerate() .min_by_key(|(_, v)| v.age) .map(|(i, _)| i) .unwrap_or(0) }); self.note_counter += 1; let voice = &mut self.voices[voice_idx]; voice.active = true; voice.note = note; voice.velocity = vel_f; voice.age = self.note_counter; voice.position = 0.0; voice.zone_index = zone_idx; voice.envelope_phase = EnvelopePhase::Attack; voice.envelope_level = 0.0; voice.envelope_time = 0.0; voice.release_start_level = 0.0; } /// Release a note: transition all active voices matching this note to the release phase. pub fn note_off(&mut self, note: u8) { for voice in &mut self.voices { if voice.active && voice.note == note && voice.envelope_phase != EnvelopePhase::Release { voice.release_start_level = voice.envelope_level; voice.envelope_phase = EnvelopePhase::Release; voice.envelope_time = 0.0; } } } } /// Advance the ADSR envelope by one sample and return the new level. fn step_envelope(voice: &mut Voice, env: &AdsrEnvelope, dt: f32) -> f32 { voice.envelope_time += dt; match voice.envelope_phase { EnvelopePhase::Attack => { if env.attack <= 0.0 { voice.envelope_level = 1.0; voice.envelope_phase = EnvelopePhase::Decay; voice.envelope_time = 0.0; } else { voice.envelope_level = (voice.envelope_time / env.attack).min(1.0); if voice.envelope_level >= 1.0 { voice.envelope_level = 1.0; voice.envelope_phase = EnvelopePhase::Decay; voice.envelope_time = 0.0; } } } EnvelopePhase::Decay => { if env.decay <= 0.0 { voice.envelope_level = env.sustain; voice.envelope_phase = EnvelopePhase::Sustain; voice.envelope_time = 0.0; } else { let progress = (voice.envelope_time / env.decay).min(1.0); voice.envelope_level = 1.0 + (env.sustain - 1.0) * progress; if progress >= 1.0 { voice.envelope_level = env.sustain; voice.envelope_phase = EnvelopePhase::Sustain; voice.envelope_time = 0.0; } } } EnvelopePhase::Sustain => { voice.envelope_level = env.sustain; } EnvelopePhase::Release => { if env.release <= 0.0 { voice.envelope_level = 0.0; voice.envelope_phase = EnvelopePhase::Idle; voice.active = false; } else { let progress = (voice.envelope_time / env.release).min(1.0); voice.envelope_level = voice.release_start_level * (1.0 - progress); if progress >= 1.0 { voice.envelope_level = 0.0; voice.envelope_phase = EnvelopePhase::Idle; voice.active = false; } } } EnvelopePhase::Idle => { voice.envelope_level = 0.0; } } voice.envelope_level } /// Render all active voices into an f32 mix buffer (additive). /// /// The buffer is **not** zeroed — samples are summed into whatever is already there. /// `channels` is the output channel count, `device_sr` the output sample rate. pub fn render_voices( inst: &mut InstrumentPlayback, buf: &mut [f32], channels: usize, device_sr: u32, ) { if !inst.active || inst.zone_buffers.is_empty() || channels == 0 || device_sr == 0 { return; } let num_frames = buf.len() / channels; let dt = 1.0 / device_sr as f32; let envelope = inst.config.envelope; for voice in &mut inst.voices { if !voice.active { continue; } if voice.zone_index >= inst.zone_buffers.len() { voice.active = false; voice.envelope_phase = EnvelopePhase::Idle; continue; } let zone = &inst.zone_buffers[voice.zone_index]; let total_frames = zone.buffer.data.len() / 2; // stereo interleaved if total_frames == 0 { voice.active = false; voice.envelope_phase = EnvelopePhase::Idle; continue; } let pitch_ratio = 2.0_f64.powf((voice.note as f64 - zone.root_note as f64) / 12.0) * (zone.buffer.sample_rate as f64 / device_sr as f64); for frame in 0..num_frames { let pos_int = voice.position as usize; if pos_int >= total_frames { voice.active = false; voice.envelope_phase = EnvelopePhase::Idle; voice.envelope_level = 0.0; break; } let env_level = step_envelope(voice, &envelope, dt); if !voice.active { break; } let frac = (voice.position - pos_int as f64) as f32; let next = (pos_int + 1).min(total_frames - 1); let l0 = zone.buffer.data[pos_int * 2]; let r0 = zone.buffer.data[pos_int * 2 + 1]; let l1 = zone.buffer.data[next * 2]; let r1 = zone.buffer.data[next * 2 + 1]; let left = l0 + (l1 - l0) * frac; let right = r0 + (r1 - r0) * frac; let gain = env_level * voice.velocity; let base = frame * channels; if channels >= 2 { buf[base] += left * gain; buf[base + 1] += right * gain; } else if channels == 1 { buf[base] += (left + right) * 0.5 * gain; } voice.position += pitch_ratio; } } } #[cfg(test)] mod tests { use super::*; fn make_zone(num_frames: usize, root_note: u8, sample_rate: u32) -> LoadedZone { let mut data = Vec::with_capacity(num_frames * 2); for i in 0..num_frames { let val = (i as f32 + 1.0) / num_frames as f32; data.push(val); // L data.push(val); // R } LoadedZone { buffer: PreviewBuffer { data, channels: 2, sample_rate, }, root_note, low_note: 0, high_note: 127, vel_low: 0.0, vel_high: 1.0, } } fn make_inst(num_frames: usize, root_note: u8) -> InstrumentPlayback { let mut inst = InstrumentPlayback::new(8); inst.zone_buffers.push(make_zone(num_frames, root_note, 44100)); inst.active = true; inst.sample_rate = 44100.0; inst } #[test] fn new_creates_inactive_voices() { let playback = InstrumentPlayback::new(8); assert_eq!(playback.voices.len(), 8); for voice in &playback.voices { assert!(!voice.active); assert_eq!(voice.envelope_phase, EnvelopePhase::Idle); } } #[test] fn new_defaults() { let playback = InstrumentPlayback::new(4); assert!(!playback.active); assert_eq!(playback.zone_buffers.len(), 0); assert_eq!(playback.note_counter, 0); assert!((playback.sample_rate - 44100.0).abs() < f32::EPSILON); } #[test] fn note_on_allocates_voice() { let mut inst = make_inst(1000, 60); inst.note_on(60, 100); let active: Vec<_> = inst.voices.iter().filter(|v| v.active).collect(); assert_eq!(active.len(), 1); assert_eq!(active[0].note, 60); assert_eq!(active[0].envelope_phase, EnvelopePhase::Attack); assert!(active[0].velocity > 0.0); } #[test] fn note_on_no_zones_is_noop() { let mut inst = InstrumentPlayback::new(4); inst.active = true; inst.note_on(60, 100); assert!(inst.voices.iter().all(|v| !v.active)); } #[test] fn note_on_steals_oldest_voice() { let mut inst = make_inst(1000, 60); // Fill all 8 voices for i in 0..8 { inst.note_on(60 + i, 100); } assert_eq!(inst.voices.iter().filter(|v| v.active).count(), 8); // 9th note should steal the oldest (note 60, age=1) inst.note_on(80, 100); let stolen = &inst.voices[0]; // voice 0 was the first allocated assert_eq!(stolen.note, 80); assert_eq!(stolen.age, 9); } #[test] fn note_off_triggers_release() { let mut inst = make_inst(1000, 60); // Set envelope with non-zero sustain so envelope_level > 0 inst.config.envelope.attack = 0.0; inst.config.envelope.sustain = 0.8; inst.note_on(60, 100); // Advance a few samples to get past attack let dt = 1.0 / 44100.0; let voice = &mut inst.voices[0]; for _ in 0..100 { step_envelope(voice, &inst.config.envelope, dt); } inst.note_off(60); let voice = &inst.voices[0]; assert_eq!(voice.envelope_phase, EnvelopePhase::Release); assert!(voice.release_start_level > 0.0); } #[test] fn note_off_wrong_note_is_noop() { let mut inst = make_inst(1000, 60); inst.note_on(60, 100); inst.note_off(61); // different note assert_eq!(inst.voices[0].envelope_phase, EnvelopePhase::Attack); } #[test] fn envelope_attack_ramp() { let env = AdsrEnvelope { attack: 0.01, decay: 0.0, sustain: 1.0, release: 0.0, }; let mut voice = Voice::new(); voice.active = true; voice.envelope_phase = EnvelopePhase::Attack; let dt = 0.001; // 1ms steps for _ in 0..5 { step_envelope(&mut voice, &env, dt); } // At 5ms into 10ms attack, should be ~0.5 assert!((voice.envelope_level - 0.5).abs() < 0.01); assert_eq!(voice.envelope_phase, EnvelopePhase::Attack); // Complete the attack (zero-length decay transitions immediately to sustain) for _ in 0..6 { step_envelope(&mut voice, &env, dt); } assert_eq!(voice.envelope_phase, EnvelopePhase::Sustain); assert!((voice.envelope_level - 1.0).abs() < f32::EPSILON); } #[test] fn envelope_zero_attack_skips() { let env = AdsrEnvelope { attack: 0.0, decay: 0.1, sustain: 0.5, release: 0.0, }; let mut voice = Voice::new(); voice.active = true; voice.envelope_phase = EnvelopePhase::Attack; step_envelope(&mut voice, &env, 0.001); assert_eq!(voice.envelope_level, 1.0); assert_eq!(voice.envelope_phase, EnvelopePhase::Decay); } #[test] fn envelope_release_ramps_to_zero() { let env = AdsrEnvelope { attack: 0.0, decay: 0.0, sustain: 1.0, release: 0.01, }; let mut voice = Voice::new(); voice.active = true; voice.envelope_phase = EnvelopePhase::Release; voice.release_start_level = 0.8; let dt = 0.001; for _ in 0..5 { step_envelope(&mut voice, &env, dt); } // At 5ms into 10ms release from 0.8, should be ~0.4 assert!((voice.envelope_level - 0.4).abs() < 0.05); assert!(voice.active); // Complete release for _ in 0..6 { step_envelope(&mut voice, &env, dt); } assert!(!voice.active); assert_eq!(voice.envelope_phase, EnvelopePhase::Idle); assert_eq!(voice.envelope_level, 0.0); } #[test] fn envelope_zero_release_immediately_deactivates() { let env = AdsrEnvelope { attack: 0.0, decay: 0.0, sustain: 1.0, release: 0.0, }; let mut voice = Voice::new(); voice.active = true; voice.envelope_phase = EnvelopePhase::Release; voice.release_start_level = 1.0; step_envelope(&mut voice, &env, 0.001); assert!(!voice.active); assert_eq!(voice.envelope_phase, EnvelopePhase::Idle); } #[test] fn render_voices_produces_output() { let mut inst = make_inst(1000, 60); inst.config.envelope.attack = 0.0; inst.config.envelope.sustain = 1.0; inst.note_on(60, 127); // max velocity, root pitch let mut buf = vec![0.0f32; 20]; // 10 frames stereo render_voices(&mut inst, &mut buf, 2, 44100); // Should have non-zero audio assert!(buf.iter().any(|&s| s != 0.0)); } #[test] fn render_voices_inactive_produces_silence() { let mut inst = make_inst(1000, 60); inst.active = false; inst.note_on(60, 100); let mut buf = vec![0.0f32; 20]; render_voices(&mut inst, &mut buf, 2, 44100); assert!(buf.iter().all(|&s| s == 0.0)); } #[test] fn render_voices_pitch_ratio_at_root() { // At root note with same sample rate, pitch ratio = 1.0 let mut inst = make_inst(1000, 60); inst.config.envelope.attack = 0.0; inst.config.envelope.sustain = 1.0; inst.note_on(60, 127); let mut buf = vec![0.0f32; 4]; // 2 frames stereo render_voices(&mut inst, &mut buf, 2, 44100); // Position should have advanced by ~2.0 (ratio 1.0, 2 frames) let voice = &inst.voices[0]; assert!((voice.position - 2.0).abs() < 0.01); } #[test] fn render_voices_pitch_ratio_octave_up() { // An octave up (note + 12) should double the pitch ratio let mut inst = make_inst(1000, 60); inst.config.envelope.attack = 0.0; inst.config.envelope.sustain = 1.0; inst.note_on(72, 127); // octave above root let mut buf = vec![0.0f32; 4]; // 2 frames stereo render_voices(&mut inst, &mut buf, 2, 44100); // Position should have advanced by ~4.0 (ratio 2.0, 2 frames) let voice = &inst.voices[0]; assert!((voice.position - 4.0).abs() < 0.01); } #[test] fn render_voices_deactivates_at_buffer_end() { let mut inst = make_inst(5, 60); // only 5 frames of audio inst.config.envelope.attack = 0.0; inst.config.envelope.sustain = 1.0; inst.note_on(60, 127); let mut buf = vec![0.0f32; 40]; // 20 frames — way past buffer render_voices(&mut inst, &mut buf, 2, 44100); assert!(!inst.voices[0].active); } #[test] fn render_voices_sums_additively() { let mut inst = make_inst(1000, 60); inst.config.envelope.attack = 0.0; inst.config.envelope.sustain = 1.0; // Start with pre-filled buffer let mut buf = vec![0.5f32; 4]; // 2 frames stereo inst.note_on(60, 127); render_voices(&mut inst, &mut buf, 2, 44100); // All values should be > 0.5 since voice output is added assert!(buf.iter().all(|&s| s > 0.5)); } }