Skip to content

Commit fc018e9

Browse files
committed
feat: add const generic mixer, voice stealing, 2nd-order sigma-delta, Q15 biquad & eaf-bake codegen
1 parent eeecb59 commit fc018e9

17 files changed

Lines changed: 592 additions & 91 deletions

File tree

src/bank.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,22 @@ impl<'a> SoundBank<'a> {
9797
}
9898

9999
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
100116
for i in 0..self.effect_count as usize {
101117
let e = self.entry(i)?;
102118
if e.id == id {
@@ -129,6 +145,7 @@ impl BankBuilder {
129145
}
130146
}
131147

148+
#[allow(clippy::too_many_arguments)]
132149
pub fn add_effect(
133150
&mut self,
134151
id: u16,
@@ -166,11 +183,20 @@ impl BankBuilder {
166183
&self,
167184
out: &mut heapless::Vec<u8, { BANK_BUILD_CAP }>,
168185
) -> 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+
169195
out.clear();
170196
out.extend_from_slice(&BANK_MAGIC)
171197
.map_err(|_| AudioError::BankFull)?;
172198
out.push(BANK_VERSION).map_err(|_| AudioError::BankFull)?;
173-
let count = self.entries.len() as u16;
199+
let count = sorted_entries.len() as u16;
174200
out.push((count & 0xFF) as u8)
175201
.map_err(|_| AudioError::BankFull)?;
176202
out.push((count >> 8) as u8)
@@ -181,7 +207,7 @@ impl BankBuilder {
181207
out.push((rate >> 8) as u8)
182208
.map_err(|_| AudioError::BankFull)?;
183209
out.push(0).map_err(|_| AudioError::BankFull)?; // reserved
184-
for e in &self.entries {
210+
for e in &sorted_entries {
185211
out.push((e.id & 0xFF) as u8)
186212
.map_err(|_| AudioError::BankFull)?;
187213
out.push((e.id >> 8) as u8)

src/config.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub const fn crossfade_step_q8(duration_ms: u16, sample_rate_hz: u32) -> u8 {
5959
if samples == 0 {
6060
return 255;
6161
}
62-
let step = (255u32 + samples - 1) / samples;
62+
let step = 255u32.div_ceil(samples);
63+
6364
if step > 255 { 255 } else { step as u8 }
6465
}

src/decode/pcm.rs

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,94 @@
1-
/// Streaming 8-bit PCM (Tier B).
1+
use crate::fixed::lerp_i8;
2+
3+
/// Streaming 8-bit PCM (Tier B) with fractional speed resampling.
24
#[derive(Debug, Clone, Copy)]
35
pub struct Pcm8Stream<'a> {
46
data: &'a [u8],
5-
pos: usize,
7+
phase_q16: u32,
8+
speed_q16: u32,
69
looped: bool,
10+
is_signed: bool,
711
}
812

913
impl<'a> Pcm8Stream<'a> {
1014
pub fn new(data: &'a [u8], flags: u8) -> Self {
15+
Self::with_speed(data, flags, 65536)
16+
}
17+
18+
pub fn with_speed(data: &'a [u8], flags: u8, speed_q16: u32) -> Self {
1119
Self {
1220
data,
13-
pos: 0,
21+
phase_q16: 0,
22+
speed_q16,
1423
looped: flags & crate::tier::flags::LOOP != 0,
24+
is_signed: flags & crate::tier::flags::SIGNED != 0,
1525
}
1626
}
1727

28+
pub fn set_speed_q16(&mut self, speed_q16: u32) {
29+
self.speed_q16 = speed_q16;
30+
}
31+
32+
pub fn speed_q16(&self) -> u32 {
33+
self.speed_q16
34+
}
35+
1836
pub fn reset(&mut self) {
19-
self.pos = 0;
37+
self.phase_q16 = 0;
2038
}
2139

2240
pub fn is_done(&self) -> bool {
23-
!self.looped && self.pos >= self.data.len()
41+
if self.data.is_empty() {
42+
return true;
43+
}
44+
let pos = (self.phase_q16 >> 16) as usize;
45+
!self.looped && pos >= self.data.len()
46+
}
47+
48+
#[inline]
49+
fn get_sample(&self, idx: usize) -> i8 {
50+
if idx >= self.data.len() {
51+
0
52+
} else if self.is_signed {
53+
self.data[idx] as i8
54+
} else {
55+
self.data[idx].wrapping_sub(128) as i8
56+
}
2457
}
25-
}
2658

27-
impl<'a> Pcm8Stream<'a> {
2859
pub fn next_sample(&mut self) -> Option<i8> {
29-
if self.pos >= self.data.len() {
30-
if self.looped && !self.data.is_empty() {
31-
self.pos = 0;
60+
if self.data.is_empty() {
61+
return None;
62+
}
63+
64+
let curr_idx = (self.phase_q16 >> 16) as usize;
65+
66+
if curr_idx >= self.data.len() {
67+
if self.looped {
68+
let len = self.data.len();
69+
let wrap = (curr_idx % len) << 16;
70+
self.phase_q16 = (wrap as u32) | (self.phase_q16 & 0xFFFF);
3271
} else {
3372
return None;
3473
}
3574
}
36-
let s = self.data[self.pos] as i8;
37-
self.pos += 1;
38-
Some(s)
75+
76+
let idx = (self.phase_q16 >> 16) as usize;
77+
let frac = ((self.phase_q16 >> 8) & 0xFF) as u8;
78+
79+
let a = self.get_sample(idx);
80+
let next_idx = if idx + 1 < self.data.len() {
81+
idx + 1
82+
} else if self.looped {
83+
0
84+
} else {
85+
idx
86+
};
87+
let b = self.get_sample(next_idx);
88+
89+
let sample = lerp_i8(a, b, frac);
90+
self.phase_q16 = self.phase_q16.wrapping_add(self.speed_q16);
91+
92+
Some(sample)
3993
}
4094
}

src/dsp.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,66 @@ impl EnvelopeFollower {
384384
self.envelope = 0.0;
385385
}
386386
}
387+
388+
/// Fixed-point Q15 Biquad filter operating without hardware floating-point operations.
389+
#[derive(Debug, Clone)]
390+
pub struct BiquadAudioFilterQ15 {
391+
// Coefficients scaled in Q14 (1.14 fixed point format)
392+
b0: i16,
393+
b1: i16,
394+
b2: i16,
395+
a1: i16,
396+
a2: i16,
397+
x1: i16,
398+
x2: i16,
399+
y1: i16,
400+
y2: i16,
401+
}
402+
403+
impl BiquadAudioFilterQ15 {
404+
pub const fn new(b0: i16, b1: i16, b2: i16, a1: i16, a2: i16) -> Self {
405+
Self {
406+
b0,
407+
b1,
408+
b2,
409+
a1,
410+
a2,
411+
x1: 0,
412+
x2: 0,
413+
y1: 0,
414+
y2: 0,
415+
}
416+
}
417+
418+
pub fn reset(&mut self) {
419+
self.x1 = 0;
420+
self.x2 = 0;
421+
self.y1 = 0;
422+
self.y2 = 0;
423+
}
424+
425+
/// Process a single 16-bit signed PCM sample (`i16`).
426+
pub fn process_sample_i16(&mut self, x: i16) -> i16 {
427+
let acc = (self.b0 as i32 * x as i32)
428+
+ (self.b1 as i32 * self.x1 as i32)
429+
+ (self.b2 as i32 * self.x2 as i32)
430+
- (self.a1 as i32 * self.y1 as i32)
431+
- (self.a2 as i32 * self.y2 as i32);
432+
433+
let y = (acc >> 14).clamp(-32768, 32767) as i16;
434+
435+
self.x2 = self.x1;
436+
self.x1 = x;
437+
self.y2 = self.y1;
438+
self.y1 = y;
439+
440+
y
441+
}
442+
443+
/// Process a single 8-bit signed PCM sample (`i8`).
444+
pub fn process_sample_i8(&mut self, x: i8) -> i8 {
445+
let x16 = (x as i16) << 8;
446+
let y16 = self.process_sample_i16(x16);
447+
(y16 >> 8) as i8
448+
}
449+
}

src/encode/wav.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ pub fn write_mono_u8(path: &str, sample_rate_hz: u32, samples: &[u8]) -> Result<
1313
}
1414

1515
pub fn build_wav_u8(sample_rate_hz: u32, samples: &[u8]) -> Vec<u8> {
16-
let byte_rate = sample_rate_hz * 1;
16+
let byte_rate = sample_rate_hz;
17+
1718
let block_align = 1u16;
1819
let data_len = samples.len() as u32;
1920
let riff_len = 36 + data_len;

0 commit comments

Comments
 (0)