Skip to main content

embedded_audio/
output.rs

1#[cfg(feature = "fm")]
2pub mod fm;
3
4#[cfg(feature = "fm")]
5pub use fm::{FmMapper, FmTick};
6
7use crate::fixed::clamp_sample;
8
9/// How PCM is converted to a PWM compare value.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum DutyMode {
12    /// Mid-scale duty plus linear scaling from PCM.
13    #[default]
14    Linear,
15    /// First-order sigma-delta noise shaping before duty mapping.
16    SigmaDelta,
17    /// Second-order MASH/error-diffusion noise shaping for higher frequency attenuation.
18    SigmaDelta2ndOrder,
19}
20
21/// First-order sigma-delta modulator (PCM → single-bit decision → duty).
22#[derive(Debug, Clone, Copy, Default)]
23pub struct SigmaDelta {
24    integrator: i32,
25}
26
27impl SigmaDelta {
28    pub const fn new() -> Self {
29        Self { integrator: 0 }
30    }
31
32    pub fn reset(&mut self) {
33        self.integrator = 0;
34    }
35
36    /// One modulator step; returns -127..=127 shaped PCM for duty mapping.
37    pub fn shape(&mut self, pcm: i8) -> i8 {
38        let input = (pcm as i32) << 8;
39        self.integrator += input;
40        let high = self.integrator >= 0;
41        if high {
42            self.integrator -= 65536;
43        }
44        if high { 127 } else { -127 }
45    }
46}
47
48/// Second-order sigma-delta modulator (MASH 1-1 / dual error integrator).
49#[derive(Debug, Clone, Copy, Default)]
50pub struct SigmaDelta2ndOrder {
51    e1: i32,
52    e2: i32,
53}
54
55impl SigmaDelta2ndOrder {
56    pub const fn new() -> Self {
57        Self { e1: 0, e2: 0 }
58    }
59
60    pub fn reset(&mut self) {
61        self.e1 = 0;
62        self.e2 = 0;
63    }
64
65    pub fn shape(&mut self, pcm: i8) -> i8 {
66        let input = (pcm as i32) << 8;
67        self.e1 += input;
68        let y1 = if self.e1 >= 0 { 32767 } else { -32768 };
69        self.e1 -= y1;
70
71        self.e2 += self.e1;
72        let y2 = if self.e2 >= 0 { 32767 } else { -32768 };
73        self.e2 -= y2;
74
75        let out = (y1 + y2) >> 9;
76        out.clamp(-127, 127) as i8
77    }
78}
79
80/// Map shaped PCM to PWM duty in `1..period-1`.
81#[inline]
82pub fn pcm_to_duty(pcm: i8, period: u16) -> u16 {
83    let mid = period as i32 / 2;
84    let swing = mid - 1;
85    let offset = (pcm as i32 * swing) / 127;
86    (mid + offset).clamp(1, period as i32 - 1) as u16
87}
88
89/// Full output path: optional ΣΔ then duty.
90pub struct PwmMapper {
91    pub mode: DutyMode,
92    pub sigma_delta: SigmaDelta,
93    pub sigma_delta_2nd: SigmaDelta2ndOrder,
94}
95
96impl PwmMapper {
97    pub const fn new(mode: DutyMode) -> Self {
98        Self {
99            mode,
100            sigma_delta: SigmaDelta::new(),
101            sigma_delta_2nd: SigmaDelta2ndOrder::new(),
102        }
103    }
104
105    pub fn map(&mut self, pcm: i8, period: u16) -> u16 {
106        let shaped = match self.mode {
107            DutyMode::Linear => pcm,
108            DutyMode::SigmaDelta => self.sigma_delta.shape(pcm),
109            DutyMode::SigmaDelta2ndOrder => self.sigma_delta_2nd.shape(pcm),
110        };
111        pcm_to_duty(shaped, period)
112    }
113
114    pub fn reset(&mut self) {
115        self.sigma_delta.reset();
116        self.sigma_delta_2nd.reset();
117    }
118}
119
120/// Soft limiter before mix bus.
121#[inline]
122pub fn limit_bus(sum: i32) -> i8 {
123    clamp_sample(sum)
124}
125
126/// Convert signed 8-bit PCM (-128..=127) to signed 16-bit PCM (-32768..=32767).
127#[inline]
128pub fn pcm_to_i16(pcm: i8) -> i16 {
129    (pcm as i16) << 8
130}
131
132/// Convert signed 8-bit PCM (-128..=127) to signed 32-bit PCM (24-bit aligned).
133#[inline]
134pub fn pcm_to_i32(pcm: i8) -> i32 {
135    (pcm as i32) << 24
136}
137
138/// Convert signed 8-bit PCM (-128..=127) to unsigned 8-bit DAC sample (0..=255).
139#[inline]
140pub fn pcm_to_dac_u8(pcm: i8) -> u8 {
141    (pcm as i16 + 128).clamp(0, 255) as u8
142}
143
144/// Convert signed 8-bit PCM (-128..=127) to unsigned 12-bit DAC sample (0..=4095, e.g. STM32 DAC1).
145#[inline]
146pub fn pcm_to_dac_u12(pcm: i8) -> u16 {
147    ((pcm as i32 + 128) * 4095 / 255).clamp(0, 4095) as u16
148}
149
150/// Convert signed 8-bit PCM (-128..=127) to unsigned 16-bit DAC sample (0..=65535).
151#[inline]
152pub fn pcm_to_dac_u16(pcm: i8) -> u16 {
153    ((pcm as i32 + 128) << 8).clamp(0, 65535) as u16
154}