use crate::error::AudioError;
use crate::tier::EffectKind;
pub const BANK_MAGIC: [u8; 4] = *b"EAFX";
pub const BANK_VERSION: u8 = 2;
pub const HEADER_SIZE: usize = 10;
pub const ENTRY_SIZE: usize = 16;
pub const BANK_BUILD_CAP: usize = 65_536;
const MAX_EFFECTS: usize = 64;
#[derive(Debug, Clone, Copy)]
pub struct SoundBank<'a> {
data: &'a [u8],
pub sample_rate_hz: u32,
effect_count: u16,
}
#[derive(Debug, Clone, Copy)]
pub struct EffectEntry {
pub id: u16,
pub kind: EffectKind,
pub flags: u8,
pub default_gain_q8: u8,
pub param0: u16,
pub param1: u16,
pub offset: u32,
pub len: u32,
}
impl EffectEntry {
pub fn payload<'a>(&self, bank: &'a SoundBank<'_>) -> Result<&'a [u8], AudioError> {
bank.payload(self)
}
}
impl<'a> SoundBank<'a> {
pub fn parse(data: &'a [u8]) -> Result<Self, AudioError> {
if data.len() < HEADER_SIZE {
return Err(AudioError::TruncatedBank);
}
if data[0..4] != BANK_MAGIC {
return Err(AudioError::InvalidBankMagic);
}
if data[4] != BANK_VERSION {
return Err(AudioError::UnsupportedBankVersion);
}
let effect_count = u16::from_le_bytes([data[5], data[6]]);
let sample_rate_hz = u16::from_le_bytes([data[7], data[8]]) as u32;
let sample_rate_hz = if sample_rate_hz == 0 {
crate::config::DEFAULT_SAMPLE_RATE_HZ
} else {
sample_rate_hz
};
let needed = HEADER_SIZE + effect_count as usize * ENTRY_SIZE;
if data.len() < needed {
return Err(AudioError::TruncatedBank);
}
Ok(Self {
data,
sample_rate_hz,
effect_count,
})
}
pub const fn effect_count(&self) -> u16 {
self.effect_count
}
pub fn entry(&self, index: usize) -> Result<EffectEntry, AudioError> {
if index >= self.effect_count as usize {
return Err(AudioError::EffectNotFound);
}
let off = HEADER_SIZE + index * ENTRY_SIZE;
let slice = &self.data[off..off + ENTRY_SIZE];
let id = u16::from_le_bytes([slice[0], slice[1]]);
let kind = EffectKind::from_u8(slice[2]).ok_or(AudioError::InvalidEffectKind)?;
let flags = slice[3];
let default_gain_q8 = slice[4];
let param0 = u16::from_le_bytes([slice[6], slice[7]]);
let param1 = u16::from_le_bytes([slice[8], slice[9]]);
let offset = u32::from_le_bytes([slice[10], slice[11], slice[12], slice[13]]);
let len = u16::from_le_bytes([slice[14], slice[15]]) as u32;
Ok(EffectEntry {
id,
kind,
flags,
default_gain_q8,
param0,
param1,
offset,
len,
})
}
pub fn find_by_id(&self, id: u16) -> Result<EffectEntry, AudioError> {
let mut low = 0i32;
let mut high = self.effect_count as i32 - 1;
while low <= high {
let mid = (low + high) / 2;
let entry = self.entry(mid as usize)?;
if entry.id == id {
return Ok(entry);
} else if entry.id < id {
low = mid + 1;
} else {
high = mid - 1;
}
}
for i in 0..self.effect_count as usize {
let e = self.entry(i)?;
if e.id == id {
return Ok(e);
}
}
Err(AudioError::EffectNotFound)
}
pub fn payload(&self, entry: &EffectEntry) -> Result<&'a [u8], AudioError> {
let start = entry.offset as usize;
let end = start + entry.len as usize;
self.data.get(start..end).ok_or(AudioError::TruncatedBank)
}
}
pub struct BankBuilder {
sample_rate_hz: u32,
entries: heapless::Vec<EffectEntry, MAX_EFFECTS>,
payload: heapless::Vec<u8, { BANK_BUILD_CAP }>,
}
impl BankBuilder {
pub fn new(sample_rate_hz: u32) -> Self {
Self {
sample_rate_hz,
entries: heapless::Vec::new(),
payload: heapless::Vec::new(),
}
}
#[allow(clippy::too_many_arguments)]
pub fn add_effect(
&mut self,
id: u16,
kind: EffectKind,
flags: u8,
default_gain_q8: u8,
param0: u16,
param1: u16,
bytes: &[u8],
) -> Result<(), AudioError> {
if self.entries.len() >= MAX_EFFECTS {
return Err(AudioError::BankFull);
}
let offset =
(HEADER_SIZE + (self.entries.len() + 1) * ENTRY_SIZE + self.payload.len()) as u32;
self.payload
.extend_from_slice(bytes)
.map_err(|_| AudioError::BankFull)?;
self.entries
.push(EffectEntry {
id,
kind,
flags,
default_gain_q8,
param0,
param1,
offset,
len: bytes.len() as u32,
})
.map_err(|_| AudioError::BankFull)?;
Ok(())
}
pub fn finish(
&self,
out: &mut heapless::Vec<u8, { BANK_BUILD_CAP }>,
) -> Result<(), AudioError> {
let mut sorted_entries = self.entries.clone();
for i in 0..sorted_entries.len() {
for j in (i + 1)..sorted_entries.len() {
if sorted_entries[i].id > sorted_entries[j].id {
sorted_entries.swap(i, j);
}
}
}
out.clear();
out.extend_from_slice(&BANK_MAGIC)
.map_err(|_| AudioError::BankFull)?;
out.push(BANK_VERSION).map_err(|_| AudioError::BankFull)?;
let count = sorted_entries.len() as u16;
out.push((count & 0xFF) as u8)
.map_err(|_| AudioError::BankFull)?;
out.push((count >> 8) as u8)
.map_err(|_| AudioError::BankFull)?;
let rate = self.sample_rate_hz.min(u16::MAX as u32) as u16;
out.push((rate & 0xFF) as u8)
.map_err(|_| AudioError::BankFull)?;
out.push((rate >> 8) as u8)
.map_err(|_| AudioError::BankFull)?;
out.push(0).map_err(|_| AudioError::BankFull)?; for e in &sorted_entries {
out.push((e.id & 0xFF) as u8)
.map_err(|_| AudioError::BankFull)?;
out.push((e.id >> 8) as u8)
.map_err(|_| AudioError::BankFull)?;
out.push(e.kind as u8).map_err(|_| AudioError::BankFull)?;
out.push(e.flags).map_err(|_| AudioError::BankFull)?;
out.push(e.default_gain_q8)
.map_err(|_| AudioError::BankFull)?;
out.push(0).map_err(|_| AudioError::BankFull)?;
out.push((e.param0 & 0xFF) as u8)
.map_err(|_| AudioError::BankFull)?;
out.push((e.param0 >> 8) as u8)
.map_err(|_| AudioError::BankFull)?;
out.push((e.param1 & 0xFF) as u8)
.map_err(|_| AudioError::BankFull)?;
out.push((e.param1 >> 8) as u8)
.map_err(|_| AudioError::BankFull)?;
out.push((e.offset & 0xFF) as u8)
.map_err(|_| AudioError::BankFull)?;
out.push(((e.offset >> 8) & 0xFF) as u8)
.map_err(|_| AudioError::BankFull)?;
out.push(((e.offset >> 16) & 0xFF) as u8)
.map_err(|_| AudioError::BankFull)?;
out.push(((e.offset >> 24) & 0xFF) as u8)
.map_err(|_| AudioError::BankFull)?;
out.push((e.len & 0xFF) as u8)
.map_err(|_| AudioError::BankFull)?;
out.push(((e.len >> 8) & 0xFF) as u8)
.map_err(|_| AudioError::BankFull)?;
}
out.extend_from_slice(&self.payload)
.map_err(|_| AudioError::BankFull)?;
Ok(())
}
}