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    pub fn tick(&mut self, attack_rate: f32, decay_rate: f32, sustain: f32, release_rate: f32) {
34        match self.phase {
35            EnvPhase::Attack => {
36                self.level += attack_rate;
37                if self.level >= 1.0 { self.level = 1.0; self.phase = EnvPhase::Decay; }
38            }
39            EnvPhase::Decay => {
40                self.level -= decay_rate;
41                if self.level <= sustain { self.level = sustain; self.phase = EnvPhase::Sustain; }
42            }
43            EnvPhase::Sustain => {}
44            EnvPhase::Release => {
45                self.level -= release_rate;
46                if self.level <= 0.0 { self.level = 0.0; self.phase = EnvPhase::Done; }
47            }
48            EnvPhase::Done => {}
49        }
50    }
51
52    pub fn release(&mut self) {
53        if self.phase != EnvPhase::Done {
54            self.phase = EnvPhase::Release;
55        }
56    }
57
58    pub fn is_done(&self) -> bool { self.phase == EnvPhase::Done }
59}
60
61/// One active voice in the sampler.
62pub struct SamplerVoice {
63    /// MIDI note number.
64    pub note: u8,
65    /// MIDI channel.
66    pub channel: u8,
67    /// Velocity (0.0–1.0).
68    pub velocity: f32,
69    /// Current playback position in frames (sub-sample precision).
70    pub position: f64,
71    /// Playback speed ratio (accounts for pitch shifting).
72    pub pitch_ratio: f64,
73    /// Volume multiplier from zone + velocity.
74    pub volume: f32,
75    /// Current lifecycle phase.
76    pub phase: VoicePhase,
77    /// ADSR envelope.
78    pub envelope: EnvelopeState,
79    /// Zone id this voice is playing.
80    pub zone_id: String,
81    /// Articulation type (cached from zone).
82    pub articulation: ArticulationType,
83    /// Loop start frame (if sustain loop).
84    pub loop_start: usize,
85    /// Loop end frame (if sustain loop).
86    pub loop_end: usize,
87    /// Whether the key is still held.
88    pub key_held: bool,
89}
90
91impl SamplerVoice {
92    pub fn new(
93        note: u8,
94        channel: u8,
95        velocity: f32,
96        pitch_ratio: f64,
97        volume: f32,
98        zone: &SampleZone,
99    ) -> Self {
100        let (loop_start, loop_end) = match &zone.articulation {
101            ArticulationType::SustainLoop { loop_start, loop_end } => (*loop_start, *loop_end),
102            _ => (0, 0),
103        };
104        Self {
105            note,
106            channel,
107            velocity,
108            position: 0.0,
109            pitch_ratio,
110            volume,
111            phase: VoicePhase::Attack,
112            envelope: EnvelopeState::new(),
113            zone_id: zone.id.clone(),
114            articulation: zone.articulation.clone(),
115            loop_start,
116            loop_end,
117            key_held: true,
118        }
119    }
120
121    /// Signal key release.
122    pub fn release(&mut self) {
123        self.key_held = false;
124        self.envelope.release();
125        if self.phase == VoicePhase::Sustain || self.phase == VoicePhase::Attack {
126            self.phase = VoicePhase::Release;
127        }
128    }
129
130    /// Is this voice finished?
131    pub fn is_done(&self) -> bool {
132        self.phase == VoicePhase::Done || self.envelope.is_done()
133    }
134
135    /// Advance position by one sample, handling loop points.
136    /// Returns the current frame position for sample lookup.
137    pub fn advance(&mut self, buffer_frames: usize) -> f64 {
138        let pos = self.position;
139        self.position += self.pitch_ratio;
140
141        match &self.articulation {
142            ArticulationType::SustainLoop { loop_start, loop_end } => {
143                if self.key_held && self.position >= *loop_end as f64 {
144                    self.position = *loop_start as f64 + (self.position - *loop_end as f64);
145                    self.phase = VoicePhase::Sustain;
146                } else if !self.key_held && self.position >= buffer_frames as f64 {
147                    self.phase = VoicePhase::Done;
148                }
149            }
150            ArticulationType::OneShot => {
151                if self.position >= buffer_frames as f64 {
152                    self.phase = VoicePhase::Done;
153                }
154            }
155            ArticulationType::SustainRelease => {
156                if self.position >= buffer_frames as f64 {
157                    self.phase = VoicePhase::Done;
158                }
159            }
160        }
161
162        pos
163    }
164}