Skip to main content

embedded_audio/
bank.rs

1use crate::error::AudioError;
2use crate::tier::EffectKind;
3
4pub const BANK_MAGIC: [u8; 4] = *b"EAFX";
5pub const BANK_VERSION: u8 = 2;
6pub const HEADER_SIZE: usize = 10;
7pub const ENTRY_SIZE: usize = 16;
8/// Maximum bank blob size for [`BankBuilder`] (tune per product flash budget).
9pub const BANK_BUILD_CAP: usize = 65_536;
10
11const MAX_EFFECTS: usize = 64;
12
13/// Parsed view over a contiguous effect bank blob in flash or ROM.
14#[derive(Debug, Clone, Copy)]
15pub struct SoundBank<'a> {
16    data: &'a [u8],
17    pub sample_rate_hz: u32,
18    effect_count: u16,
19}
20
21/// Directory entry describing one effect payload.
22#[derive(Debug, Clone, Copy)]
23pub struct EffectEntry {
24    pub id: u16,
25    pub kind: EffectKind,
26    pub flags: u8,
27    pub default_gain_q8: u8,
28    pub param0: u16,
29    pub param1: u16,
30    pub offset: u32,
31    pub len: u32,
32}
33
34impl EffectEntry {
35    pub fn payload<'a>(&self, bank: &'a SoundBank<'_>) -> Result<&'a [u8], AudioError> {
36        bank.payload(self)
37    }
38}
39
40impl<'a> SoundBank<'a> {
41    pub fn parse(data: &'a [u8]) -> Result<Self, AudioError> {
42        if data.len() < HEADER_SIZE {
43            return Err(AudioError::TruncatedBank);
44        }
45        if data[0..4] != BANK_MAGIC {
46            return Err(AudioError::InvalidBankMagic);
47        }
48        if data[4] != BANK_VERSION {
49            return Err(AudioError::UnsupportedBankVersion);
50        }
51        let effect_count = u16::from_le_bytes([data[5], data[6]]);
52        let sample_rate_hz = u16::from_le_bytes([data[7], data[8]]) as u32;
53        let sample_rate_hz = if sample_rate_hz == 0 {
54            crate::config::DEFAULT_SAMPLE_RATE_HZ
55        } else {
56            sample_rate_hz
57        };
58        let needed = HEADER_SIZE + effect_count as usize * ENTRY_SIZE;
59        if data.len() < needed {
60            return Err(AudioError::TruncatedBank);
61        }
62        Ok(Self {
63            data,
64            sample_rate_hz,
65            effect_count,
66        })
67    }
68
69    pub const fn effect_count(&self) -> u16 {
70        self.effect_count
71    }
72
73    pub fn entry(&self, index: usize) -> Result<EffectEntry, AudioError> {
74        if index >= self.effect_count as usize {
75            return Err(AudioError::EffectNotFound);
76        }
77        let off = HEADER_SIZE + index * ENTRY_SIZE;
78        let slice = &self.data[off..off + ENTRY_SIZE];
79        let id = u16::from_le_bytes([slice[0], slice[1]]);
80        let kind = EffectKind::from_u8(slice[2]).ok_or(AudioError::InvalidEffectKind)?;
81        let flags = slice[3];
82        let default_gain_q8 = slice[4];
83        let param0 = u16::from_le_bytes([slice[6], slice[7]]);
84        let param1 = u16::from_le_bytes([slice[8], slice[9]]);
85        let offset = u32::from_le_bytes([slice[10], slice[11], slice[12], slice[13]]);
86        let len = u16::from_le_bytes([slice[14], slice[15]]) as u32;
87        Ok(EffectEntry {
88            id,
89            kind,
90            flags,
91            default_gain_q8,
92            param0,
93            param1,
94            offset,
95            len,
96        })
97    }
98
99    pub fn find_by_id(&self, id: u16) -> Result<EffectEntry, AudioError> {
100        let mut low = 0i32;
101        let mut high = self.effect_count as i32 - 1;
102
103        while low <= high {
104            let mid = (low + high) / 2;
105            let entry = self.entry(mid as usize)?;
106            if entry.id == id {
107                return Ok(entry);
108            } else if entry.id < id {
109                low = mid + 1;
110            } else {
111                high = mid - 1;
112            }
113        }
114
115        // Fallback linear scan in case bank entries were not sorted
116        for i in 0..self.effect_count as usize {
117            let e = self.entry(i)?;
118            if e.id == id {
119                return Ok(e);
120            }
121        }
122        Err(AudioError::EffectNotFound)
123    }
124
125    pub fn payload(&self, entry: &EffectEntry) -> Result<&'a [u8], AudioError> {
126        let start = entry.offset as usize;
127        let end = start + entry.len as usize;
128        self.data.get(start..end).ok_or(AudioError::TruncatedBank)
129    }
130}
131
132/// Build an effect bank blob (host tooling / tests).
133pub struct BankBuilder {
134    sample_rate_hz: u32,
135    entries: heapless::Vec<EffectEntry, MAX_EFFECTS>,
136    payload: heapless::Vec<u8, { BANK_BUILD_CAP }>,
137}
138
139impl BankBuilder {
140    pub fn new(sample_rate_hz: u32) -> Self {
141        Self {
142            sample_rate_hz,
143            entries: heapless::Vec::new(),
144            payload: heapless::Vec::new(),
145        }
146    }
147
148    #[allow(clippy::too_many_arguments)]
149    pub fn add_effect(
150        &mut self,
151        id: u16,
152        kind: EffectKind,
153        flags: u8,
154        default_gain_q8: u8,
155        param0: u16,
156        param1: u16,
157        bytes: &[u8],
158    ) -> Result<(), AudioError> {
159        if self.entries.len() >= MAX_EFFECTS {
160            return Err(AudioError::BankFull);
161        }
162        let offset =
163            (HEADER_SIZE + (self.entries.len() + 1) * ENTRY_SIZE + self.payload.len()) as u32;
164        self.payload
165            .extend_from_slice(bytes)
166            .map_err(|_| AudioError::BankFull)?;
167        self.entries
168            .push(EffectEntry {
169                id,
170                kind,
171                flags,
172                default_gain_q8,
173                param0,
174                param1,
175                offset,
176                len: bytes.len() as u32,
177            })
178            .map_err(|_| AudioError::BankFull)?;
179        Ok(())
180    }
181
182    pub fn finish(
183        &self,
184        out: &mut heapless::Vec<u8, { BANK_BUILD_CAP }>,
185    ) -> Result<(), AudioError> {
186        let mut sorted_entries = self.entries.clone();
187        for i in 0..sorted_entries.len() {
188            for j in (i + 1)..sorted_entries.len() {
189                if sorted_entries[i].id > sorted_entries[j].id {
190                    sorted_entries.swap(i, j);
191                }
192            }
193        }
194
195        out.clear();
196        out.extend_from_slice(&BANK_MAGIC)
197            .map_err(|_| AudioError::BankFull)?;
198        out.push(BANK_VERSION).map_err(|_| AudioError::BankFull)?;
199        let count = sorted_entries.len() as u16;
200        out.push((count & 0xFF) as u8)
201            .map_err(|_| AudioError::BankFull)?;
202        out.push((count >> 8) as u8)
203            .map_err(|_| AudioError::BankFull)?;
204        let rate = self.sample_rate_hz.min(u16::MAX as u32) as u16;
205        out.push((rate & 0xFF) as u8)
206            .map_err(|_| AudioError::BankFull)?;
207        out.push((rate >> 8) as u8)
208            .map_err(|_| AudioError::BankFull)?;
209        out.push(0).map_err(|_| AudioError::BankFull)?; // reserved
210        for e in &sorted_entries {
211            out.push((e.id & 0xFF) as u8)
212                .map_err(|_| AudioError::BankFull)?;
213            out.push((e.id >> 8) as u8)
214                .map_err(|_| AudioError::BankFull)?;
215            out.push(e.kind as u8).map_err(|_| AudioError::BankFull)?;
216            out.push(e.flags).map_err(|_| AudioError::BankFull)?;
217            out.push(e.default_gain_q8)
218                .map_err(|_| AudioError::BankFull)?;
219            out.push(0).map_err(|_| AudioError::BankFull)?;
220            out.push((e.param0 & 0xFF) as u8)
221                .map_err(|_| AudioError::BankFull)?;
222            out.push((e.param0 >> 8) as u8)
223                .map_err(|_| AudioError::BankFull)?;
224            out.push((e.param1 & 0xFF) as u8)
225                .map_err(|_| AudioError::BankFull)?;
226            out.push((e.param1 >> 8) as u8)
227                .map_err(|_| AudioError::BankFull)?;
228            out.push((e.offset & 0xFF) as u8)
229                .map_err(|_| AudioError::BankFull)?;
230            out.push(((e.offset >> 8) & 0xFF) as u8)
231                .map_err(|_| AudioError::BankFull)?;
232            out.push(((e.offset >> 16) & 0xFF) as u8)
233                .map_err(|_| AudioError::BankFull)?;
234            out.push(((e.offset >> 24) & 0xFF) as u8)
235                .map_err(|_| AudioError::BankFull)?;
236            out.push((e.len & 0xFF) as u8)
237                .map_err(|_| AudioError::BankFull)?;
238            out.push(((e.len >> 8) & 0xFF) as u8)
239                .map_err(|_| AudioError::BankFull)?;
240        }
241        out.extend_from_slice(&self.payload)
242            .map_err(|_| AudioError::BankFull)?;
243        Ok(())
244    }
245}