Skip to main content

embedded_audio/
engine.rs

1use crate::bank::{EffectEntry, SoundBank};
2use crate::config::{AudioConfig, crossfade_step_q8};
3use crate::envelope::AdsrSpec;
4use crate::error::AudioError;
5use crate::fixed::{apply_gain_q8, mix_crossfade};
6use crate::output::{
7    DutyMode, PwmMapper, limit_bus, pcm_to_dac_u8, pcm_to_dac_u12, pcm_to_dac_u16, pcm_to_i16,
8    pcm_to_i32,
9};
10
11use crate::voice::Voice;
12
13#[cfg(feature = "fm")]
14use crate::output::{FmMapper, FmTick};
15
16/// Policy for dynamic voice allocation when triggering new effects.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum VoiceStealingPolicy {
19    /// Steal the lowest-priority voice if all voices are active and new priority is higher.
20    #[default]
21    LowestPriorityOldest,
22    /// Only allocate if a voice channel is completely free.
23    FreeChannelOnly,
24}
25
26/// N-voice mixer with bank playback, dynamic voice allocation, and duty-modulated PWM output.
27pub struct AudioEngine<'a, const N: usize = 2> {
28    bank: Option<SoundBank<'a>>,
29    config: AudioConfig,
30    voices: [Voice<'a>; N],
31    mapper: PwmMapper,
32    crossfade_t_q8: u8,
33    crossfade_step_q8: u8,
34    crossfade_active: bool,
35    stealing_policy: VoiceStealingPolicy,
36    #[cfg(feature = "fm")]
37    fm_mapper: FmMapper,
38}
39
40impl<'a> AudioEngine<'a, 2> {
41    /// Create a standard 2-voice audio engine.
42    pub fn new(config: AudioConfig) -> Self {
43        Self::with_voice_count(config)
44    }
45
46    pub fn from_sample_rate(sample_rate_hz: u32, pwm_period: u16, duty_mode: DutyMode) -> Self {
47        Self::new(AudioConfig::new(sample_rate_hz, pwm_period, duty_mode))
48    }
49
50    #[cfg(feature = "fm")]
51    pub fn new_markham() -> Self {
52        use crate::profile::markham;
53        Self::new(AudioConfig::new(
54            markham::CONTROL_TICK_HZ,
55            0,
56            DutyMode::Linear,
57        ))
58    }
59}
60
61impl<'a, const N: usize> AudioEngine<'a, N> {
62    /// Create an audio engine with generic voice count N.
63    pub fn with_voice_count(config: AudioConfig) -> Self {
64        let rate = config.sample_rate_hz;
65        Self {
66            bank: None,
67            config,
68            voices: [Voice::silent(rate); N],
69            mapper: PwmMapper::new(config.duty_mode),
70            crossfade_t_q8: 0,
71            crossfade_step_q8: 0,
72            crossfade_active: false,
73            stealing_policy: VoiceStealingPolicy::default(),
74            #[cfg(feature = "fm")]
75            fm_mapper: FmMapper::markham(),
76        }
77    }
78
79    pub fn config(&self) -> AudioConfig {
80        self.config
81    }
82
83    pub fn set_stealing_policy(&mut self, policy: VoiceStealingPolicy) {
84        self.stealing_policy = policy;
85    }
86
87    pub fn stealing_policy(&self) -> VoiceStealingPolicy {
88        self.stealing_policy
89    }
90
91    pub fn voice_count(&self) -> usize {
92        N
93    }
94
95    pub fn active_voice_count(&self) -> usize {
96        self.voices.iter().filter(|v| v.is_audible()).count()
97    }
98
99    pub fn voice(&self, idx: usize) -> Option<&Voice<'a>> {
100        self.voices.get(idx)
101    }
102
103    pub fn voice_mut(&mut self, idx: usize) -> Option<&mut Voice<'a>> {
104        self.voices.get_mut(idx)
105    }
106
107    pub fn set_bank(&mut self, bank: SoundBank<'a>) {
108        let rate = bank.sample_rate_hz;
109        self.config.sample_rate_hz = rate;
110        self.voices = [Voice::silent(rate); N];
111        self.bank = Some(bank);
112    }
113
114    pub fn set_master_gain_q8(&mut self, gain: u8) {
115        self.config.master_gain_q8 = gain;
116    }
117
118    pub fn stop_all(&mut self) {
119        for v in &mut self.voices {
120            v.stop_immediate();
121        }
122        self.crossfade_active = false;
123        self.crossfade_step_q8 = 0;
124        self.mapper.reset();
125    }
126
127    pub fn is_playing(&self) -> bool {
128        self.voices.iter().any(|v| v.is_audible())
129    }
130
131    /// Dynamically allocate a voice channel based on priority and stealing policy.
132    pub fn allocate_voice(&mut self, priority: u8) -> Option<usize> {
133        if N == 0 {
134            return None;
135        }
136        // 1. Look for an inaudible / idle voice
137        for (i, v) in self.voices.iter().enumerate() {
138            if !v.is_audible() {
139                return Some(i);
140            }
141        }
142        // 2. Check stealing policy
143        if self.stealing_policy == VoiceStealingPolicy::LowestPriorityOldest {
144            let mut lowest_idx = None;
145            let mut lowest_prio = priority;
146            for (i, v) in self.voices.iter().enumerate() {
147                if v.priority < lowest_prio {
148                    lowest_prio = v.priority;
149                    lowest_idx = Some(i);
150                }
151            }
152            return lowest_idx;
153        }
154        None
155    }
156
157    /// Play effect on an automatically allocated voice channel.
158    pub fn play(&mut self, effect_id: u16, adsr: AdsrSpec) -> Result<usize, AudioError> {
159        self.play_with_priority(effect_id, adsr, 128)
160    }
161
162    /// Play with custom priority on an allocated voice channel.
163    pub fn play_with_priority(
164        &mut self,
165        effect_id: u16,
166        adsr: AdsrSpec,
167        priority: u8,
168    ) -> Result<usize, AudioError> {
169        let idx = self.allocate_voice(priority).ok_or(AudioError::VoiceBusy)?;
170        let bank = self.bank.ok_or(AudioError::NoBank)?;
171        let entry = bank.find_by_id(effect_id)?;
172        self.start_on_voice(idx, &bank, entry, adsr, priority)?;
173        Ok(idx)
174    }
175
176    /// Play custom wavetable on an automatically allocated voice channel.
177    pub fn play_wavetable(
178        &mut self,
179        table: &'a [u8],
180        freq_hz: u32,
181        adsr: AdsrSpec,
182    ) -> Result<usize, AudioError> {
183        self.play_wavetable_with_priority(table, freq_hz, adsr, 128)
184    }
185
186    /// Play custom wavetable with custom priority on an allocated voice channel.
187    pub fn play_wavetable_with_priority(
188        &mut self,
189        table: &'a [u8],
190        freq_hz: u32,
191        adsr: AdsrSpec,
192        priority: u8,
193    ) -> Result<usize, AudioError> {
194        if table.len() < 256 {
195            return Err(AudioError::InvalidPayload);
196        }
197        let idx = self.allocate_voice(priority).ok_or(AudioError::VoiceBusy)?;
198        let rate = self.config.sample_rate_hz;
199        let mut voice = Voice::silent(rate);
200        voice.set_gain_q8(255);
201        voice.priority = priority;
202        voice.source.start_wavetable(table, freq_hz, rate);
203        voice.trigger_adsr(adsr);
204        self.voices[idx] = voice;
205        Ok(idx)
206    }
207
208    /// Crossfade voice 0 → `effect_id` on voice 1 over `duration_ms`.
209    pub fn crossfade_to(
210        &mut self,
211        effect_id: u16,
212        duration_ms: u16,
213        adsr: AdsrSpec,
214    ) -> Result<(), AudioError> {
215        if N < 2 {
216            return Err(AudioError::VoiceBusy);
217        }
218        let bank = self.bank.ok_or(AudioError::NoBank)?;
219        let entry = bank.find_by_id(effect_id)?;
220        self.voices[0].release();
221        self.start_on_voice(1, &bank, entry, adsr, 128)?;
222        self.crossfade_active = true;
223        self.crossfade_t_q8 = 0;
224        self.crossfade_step_q8 = crossfade_step_q8(duration_ms, self.config.sample_rate_hz);
225        Ok(())
226    }
227
228    fn advance_crossfade(&mut self) {
229        if !self.crossfade_active || N < 2 {
230            return;
231        }
232        let t = self.crossfade_t_q8.saturating_add(self.crossfade_step_q8);
233        self.crossfade_t_q8 = t;
234        if t == 255 {
235            self.voices[0].stop_immediate();
236            self.voices[0].source = self.voices[1].source;
237            self.voices[0].set_gain_q8(self.voices[1].gain_q8());
238            self.voices[0].priority = self.voices[1].priority;
239            self.voices[1].stop_immediate();
240            self.crossfade_active = false;
241            self.crossfade_t_q8 = 0;
242        }
243    }
244
245    pub fn start_on_voice(
246        &mut self,
247        idx: usize,
248        bank: &SoundBank<'a>,
249        entry: EffectEntry,
250        adsr: AdsrSpec,
251        priority: u8,
252    ) -> Result<(), AudioError> {
253        if idx >= N {
254            return Err(AudioError::VoiceBusy);
255        }
256        let payload = bank.payload(&entry)?;
257        let rate = bank.sample_rate_hz;
258        let mut voice = Voice::silent(rate);
259        voice.set_gain_q8(entry.default_gain_q8);
260        voice.priority = priority;
261        if !voice.source.start_from_entry(
262            entry.kind,
263            entry.flags,
264            entry.param0,
265            entry.param1,
266            payload,
267            rate,
268        ) {
269            return Err(AudioError::InvalidEffectKind);
270        }
271        voice.trigger_adsr(adsr);
272        self.voices[idx] = voice;
273        Ok(())
274    }
275
276    fn tick_mixed_pcm(&mut self) -> i8 {
277        self.advance_crossfade();
278        for v in &mut self.voices {
279            v.tick_envelope();
280        }
281
282        if N == 0 {
283            return 0;
284        }
285
286        if self.crossfade_active && N >= 2 {
287            let sa = self.voices[0].next_sample();
288            let sb = self.voices[1].next_sample();
289            let mixed = match (sa, sb) {
290                (None, None) => 0,
291                (Some(s), None) => s as i32,
292                (None, Some(s)) => s as i32,
293                (Some(sa), Some(sb)) => mix_crossfade(sa, sb, self.crossfade_t_q8) as i32,
294            };
295            return apply_gain_q8(limit_bus(mixed), self.config.master_gain_q8);
296        }
297
298        let mut sum: i32 = 0;
299        let mut active_count: i32 = 0;
300        for v in &mut self.voices {
301            if let Some(s) = v.next_sample() {
302                sum += s as i32;
303                active_count += 1;
304            }
305        }
306
307        let mixed = if active_count > 1 {
308            sum / active_count
309        } else {
310            sum
311        };
312
313        apply_gain_q8(limit_bus(mixed), self.config.master_gain_q8)
314    }
315
316    /// Mixed PCM sample after envelopes (before PWM mapping). Useful for WAV preview.
317    pub fn tick_pcm(&mut self) -> i8 {
318        self.tick_mixed_pcm()
319    }
320
321    #[cfg(feature = "dsp")]
322    /// Mixed PCM sample tick normalized to floating-point range `[-1.0, 1.0]`.
323    pub fn tick_pcm_f32(&mut self) -> f32 {
324        self.tick_mixed_pcm() as f32 / 128.0
325    }
326
327    #[cfg(feature = "dsp")]
328    /// Fill a buffer with consecutive normalized floating-point PCM samples.
329    pub fn fill_pcm_f32_buffer(&mut self, out: &mut [f32]) -> usize {
330        for sample in out.iter_mut() {
331            *sample = self.tick_pcm_f32();
332        }
333        out.len()
334    }
335
336    /// One audio sample tick → PWM duty compare value.
337    pub fn tick(&mut self) -> u16 {
338        let pcm = self.tick_mixed_pcm();
339        self.mapper.map(pcm, self.config.pwm_period)
340    }
341
342    /// Fill a DMA buffer with consecutive duty values (one per sample tick).
343    ///
344    /// Returns how many slots were written (`out.len()`).
345    pub fn fill_duty_buffer(&mut self, out: &mut [u16]) -> usize {
346        let period = self.config.pwm_period;
347        for duty in out.iter_mut() {
348            let pcm = self.tick_mixed_pcm();
349            *duty = self.mapper.map(pcm, period);
350        }
351        out.len()
352    }
353
354    /// Fill a DMA buffer with consecutive signed 8-bit PCM samples (-128..=127).
355    pub fn fill_pcm_i8_buffer(&mut self, out: &mut [i8]) -> usize {
356        for slot in out.iter_mut() {
357            *slot = self.tick_mixed_pcm();
358        }
359        out.len()
360    }
361
362    /// Fill a DMA buffer with consecutive signed 16-bit PCM samples (-32768..=32767).
363    pub fn fill_pcm_i16_buffer(&mut self, out: &mut [i16]) -> usize {
364        for slot in out.iter_mut() {
365            let pcm = self.tick_mixed_pcm();
366            *slot = pcm_to_i16(pcm);
367        }
368        out.len()
369    }
370
371    /// Fill a DMA buffer with consecutive signed 32-bit PCM samples (24-bit aligned).
372    pub fn fill_pcm_i32_buffer(&mut self, out: &mut [i32]) -> usize {
373        for slot in out.iter_mut() {
374            let pcm = self.tick_mixed_pcm();
375            *slot = pcm_to_i32(pcm);
376        }
377        out.len()
378    }
379
380    /// Fill a DMA buffer with consecutive unsigned 8-bit DAC values (0..=255).
381    pub fn fill_dac_u8_buffer(&mut self, out: &mut [u8]) -> usize {
382        for slot in out.iter_mut() {
383            let pcm = self.tick_mixed_pcm();
384            *slot = pcm_to_dac_u8(pcm);
385        }
386        out.len()
387    }
388
389    /// Fill a DMA buffer with consecutive unsigned 12-bit DAC values (0..=4095, e.g. STM32 DAC1).
390    pub fn fill_dac_u12_buffer(&mut self, out: &mut [u16]) -> usize {
391        for slot in out.iter_mut() {
392            let pcm = self.tick_mixed_pcm();
393            *slot = pcm_to_dac_u12(pcm);
394        }
395        out.len()
396    }
397
398    /// Fill a DMA buffer with consecutive unsigned 16-bit DAC values (0..=65535).
399    pub fn fill_dac_u16_buffer(&mut self, out: &mut [u16]) -> usize {
400        for slot in out.iter_mut() {
401            let pcm = self.tick_mixed_pcm();
402            *slot = pcm_to_dac_u16(pcm);
403        }
404        out.len()
405    }
406
407    /// Fill an interleaved stereo 16-bit PCM buffer (duplicating mono mixed sample to L and R channels).
408    pub fn fill_stereo_i16_buffer(&mut self, out: &mut [i16]) -> usize {
409        let mono_count = out.len() / 2;
410        for i in 0..mono_count {
411            let pcm = self.tick_mixed_pcm();
412            let sample = pcm_to_i16(pcm);
413            out[i * 2] = sample;
414            out[i * 2 + 1] = sample;
415        }
416        mono_count * 2
417    }
418
419    /// Fill an interleaved stereo 32-bit PCM buffer (for 24-bit / 32-bit SAI / I2S DMA peripherals).
420    pub fn fill_stereo_i32_buffer(&mut self, out: &mut [i32]) -> usize {
421        let mono_count = out.len() / 2;
422        for i in 0..mono_count {
423            let pcm = self.tick_mixed_pcm();
424            let sample = pcm_to_i32(pcm);
425            out[i * 2] = sample;
426            out[i * 2 + 1] = sample;
427        }
428        mono_count * 2
429    }
430
431    /// Generic buffer filling using a custom sample mapping closure.
432    pub fn fill_custom_buffer<T>(
433        &mut self,
434        out: &mut [T],
435        mut map_fn: impl FnMut(i8) -> T,
436    ) -> usize {
437        for slot in out.iter_mut() {
438            let pcm = self.tick_mixed_pcm();
439            *slot = map_fn(pcm);
440        }
441        out.len()
442    }
443
444    /// Alias for [`Self::tick`].
445    #[inline]
446    pub fn tick_pwm(&mut self) -> u16 {
447        self.tick()
448    }
449
450    /// Tier A tone without a bank.
451    pub fn play_tone(&mut self, freq_hz: u32, duration_ms: u16, waveform: crate::synth::Waveform) {
452        let rate = self.config.sample_rate_hz;
453        if N > 0 {
454            self.voices[0] = Voice::silent(rate);
455            self.voices[0]
456                .source
457                .start_tone(freq_hz, duration_ms, waveform, rate);
458            self.voices[0].trigger_adsr(AdsrSpec::click());
459        }
460    }
461
462    #[cfg(feature = "fm")]
463    pub fn set_fm_mapper(&mut self, mapper: FmMapper) {
464        self.fm_mapper = mapper;
465    }
466
467    #[cfg(feature = "fm")]
468    #[allow(clippy::collapsible_if)]
469    /// FM buzzer backend (optional; not used for duty-modulation products).
470    pub fn tick_fm(&mut self) -> FmTick {
471        let pcm = self.tick_mixed_pcm();
472        let rate = self.config.sample_rate_hz;
473
474        if !self.crossfade_active && N > 0 {
475            if let Some(hz) = self.voices[0].source.carrier_hz(rate) {
476                return FmMapper::from_carrier(hz, self.voices[0].source.is_active());
477            }
478        }
479
480        self.fm_mapper.map_pcm(pcm)
481    }
482}
483
484impl Default for AudioEngine<'static, 2> {
485    fn default() -> Self {
486        Self::new(AudioConfig::default())
487    }
488}