Skip to main content

aether_sampler/
voice.rs

1//! Sampler voice — one playing note.
2
3use crate::instrument::{ArticulationType, SampleZone};
4
5/// The phase of a voice's lifecycle.
6#[derive(Debug, Clone, PartialEq)]
7pub enum VoicePhase {
8    /// Key is held — playing forward.
9    Attack,
10    /// Sustain loop active.
11    Sustain,
12    /// Key released — playing release tail.
13    Release,
14    /// Voice is done — can be recycled.
15    Done,
16}
17
18/// ADSR envelope state.
19#[derive(Debug, Clone)]
20pub struct EnvelopeState {
21    pub level: f32,
22    pub phase: EnvPhase,
23}
24
25#[derive(Debug, Clone, PartialEq)]
26pub enum EnvPhase { Attack, Decay, Sustain, Release, Done }
27
28impl EnvelopeState {
29    pub fn new() -> Self {
30        Self { level: 0.0, phase: EnvPhase::Attack }
31    }
32}
33
34impl Default for EnvelopeState {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl EnvelopeState {
41    pub fn tick(&mut self, attack_rate: f32, decay_rate: f32, sustain: f32, release_rate: f32) {
42        match self.phase {
43            EnvPhase::Attack => {
44                self.level += attack_rate;
45                if self.level >= 1.0 { self.level = 1.0; self.phase = EnvPhase::Decay; }
46            }
47            EnvPhase::Decay => {
48                self.level -= decay_rate;
49                if self.level <= sustain { self.level = sustain; self.phase = EnvPhase::Sustain; }
50            }
51            EnvPhase::Sustain => {}
52            EnvPhase::Release => {
53                self.level -= release_rate;
54                if self.level <= 0.0 { self.level = 0.0; self.phase = EnvPhase::Done; }
55            }
56            EnvPhase::Done => {}
57        }
58    }
59
60    pub fn release(&mut self) {
61        if self.phase != EnvPhase::Done {
62            self.phase = EnvPhase::Release;
63        }
64    }
65
66    pub fn is_done(&self) -> bool { self.phase == EnvPhase::Done }
67}
68
69/// One active voice in the sampler.
70pub struct SamplerVoice {
71    /// MIDI note number.
72    pub note: u8,
73    /// MIDI channel.
74    pub channel: u8,
75    /// Velocity (0.0–1.0).
76    pub velocity: f32,
77    /// Current playback position in frames (sub-sample precision).
78    pub position: f64,
79    /// Playback speed ratio (accounts for pitch shifting).
80    pub pitch_ratio: f64,
81    /// Volume multiplier from zone + velocity.
82    pub volume: f32,
83    /// Current lifecycle phase.
84    pub phase: VoicePhase,
85    /// ADSR envelope.
86    pub envelope: EnvelopeState,
87    /// Zone id this voice is playing.
88    pub zone_id: String,
89    /// Articulation type (cached from zone).
90    pub articulation: ArticulationType,
91    /// Loop start frame (if sustain loop).
92    pub loop_start: usize,
93    /// Loop end frame (if sustain loop).
94    pub loop_end: usize,
95    /// Whether the key is still held.
96    pub key_held: bool,
97}
98
99impl SamplerVoice {
100    pub fn new(
101        note: u8,
102        channel: u8,
103        velocity: f32,
104        pitch_ratio: f64,
105        volume: f32,
106        zone: &SampleZone,
107    ) -> Self {
108        let (loop_start, loop_end) = match &zone.articulation {
109            ArticulationType::SustainLoop { loop_start, loop_end } => (*loop_start, *loop_end),
110            _ => (0, 0),
111        };
112        Self {
113            note,
114            channel,
115            velocity,
116            position: 0.0,
117            pitch_ratio,
118            volume,
119            phase: VoicePhase::Attack,
120            envelope: EnvelopeState::new(),
121            zone_id: zone.id.clone(),
122            articulation: zone.articulation.clone(),
123            loop_start,
124            loop_end,
125            key_held: true,
126        }
127    }
128
129    /// Signal key release.
130    pub fn release(&mut self) {
131        self.key_held = false;
132        self.envelope.release();
133        if self.phase == VoicePhase::Sustain || self.phase == VoicePhase::Attack {
134            self.phase = VoicePhase::Release;
135        }
136    }
137
138    /// Is this voice finished?
139    pub fn is_done(&self) -> bool {
140        self.phase == VoicePhase::Done || self.envelope.is_done()
141    }
142
143    /// Advance position by one sample, handling loop points.
144    /// Returns the current frame position for sample lookup.
145    pub fn advance(&mut self, buffer_frames: usize) -> f64 {
146        let pos = self.position;
147        self.position += self.pitch_ratio;
148
149        match &self.articulation {
150            ArticulationType::SustainLoop { loop_start, loop_end } => {
151                if self.key_held && self.position >= *loop_end as f64 {
152                    self.position = *loop_start as f64 + (self.position - *loop_end as f64);
153                    self.phase = VoicePhase::Sustain;
154                } else if !self.key_held && self.position >= buffer_frames as f64 {
155                    self.phase = VoicePhase::Done;
156                }
157            }
158            ArticulationType::OneShot => {
159                if self.position >= buffer_frames as f64 {
160                    self.phase = VoicePhase::Done;
161                }
162            }
163            ArticulationType::SustainRelease => {
164                if self.position >= buffer_frames as f64 {
165                    self.phase = VoicePhase::Done;
166                }
167            }
168        }
169
170        pos
171    }
172}