ferravex_engine/
audio.rs

1//! Original PCM16 WAV decoder and bounded stereo spatial mixer.
2//! The mixer writes PCM; a platform audio device supplies its output callback.
3use std::sync::Arc;
4/// Maximum decoded interleaved-stereo storage: 64 MiB, also bounded to 10 minutes.
5pub const MAX_CLIP_FRAMES: usize = 8 * 1024 * 1024;
6const MAX_WAV_BYTES: usize = 64 * 1024 * 1024;
7#[derive(Clone, Debug)]
8pub struct Clip {
9    pub sample_rate: u32,
10    pub samples: Arc<[[f32; 2]]>,
11}
12impl Clip {
13    /// Uncompressed PCM16 mono/stereo, 8–96 kHz. Rejects malformed chunk boundaries.
14    pub fn wav(bytes: &[u8]) -> Option<Self> {
15        if bytes.len() < 12
16            || bytes.len() > MAX_WAV_BYTES
17            || &bytes[..4] != b"RIFF"
18            || &bytes[8..12] != b"WAVE"
19        {
20            return None;
21        }
22        let declared = u32::from_le_bytes(bytes[4..8].try_into().ok()?);
23        let end = usize::try_from(declared).ok()?.checked_add(8)?;
24        if end != bytes.len() {
25            return None;
26        }
27        let mut at = 12_usize;
28        let mut format = None;
29        let mut pcm = None;
30        while at < end {
31            let header_end = at.checked_add(8)?;
32            let header = bytes.get(at..header_end)?;
33            let len = u32::from_le_bytes(header[4..8].try_into().ok()?) as usize;
34            at = header_end;
35            let chunk_end = at.checked_add(len)?;
36            let chunk = bytes.get(at..chunk_end)?;
37            match &header[..4] {
38                b"fmt " if format.is_none() && len >= 16 => {
39                    let u16at = |i| u16::from_le_bytes(chunk[i..i + 2].try_into().unwrap());
40                    let u32at = |i| u32::from_le_bytes(chunk[i..i + 4].try_into().unwrap());
41                    let channels = u16at(2);
42                    let rate = u32at(4);
43                    let stride = u16at(12);
44                    if u16at(0) != 1
45                        || !(1..=2).contains(&channels)
46                        || !(8000..=96000).contains(&rate)
47                        || u16at(14) != 16
48                        || stride != channels * 2
49                        || u32at(8) != rate * u32::from(stride)
50                    {
51                        return None;
52                    }
53                    format = Some((usize::from(channels), rate));
54                }
55                b"data" if pcm.is_none() => pcm = Some(chunk),
56                b"fmt " | b"data" => return None,
57                _ => {}
58            }
59            at = chunk_end.checked_add(len % 2)?;
60            if at > end {
61                return None;
62            }
63        }
64        let (channels, sample_rate) = format?;
65        let pcm = pcm?;
66        let frames = pcm.len() / (channels * 2);
67        if !pcm.len().is_multiple_of(channels * 2) || frames > frame_limit(sample_rate) {
68            return None;
69        }
70        let samples: Vec<_> = pcm
71            .chunks_exact(channels * 2)
72            .map(|frame| {
73                let left = f32::from(i16::from_le_bytes([frame[0], frame[1]])) / 32768.0;
74                let right = if channels == 1 {
75                    left
76                } else {
77                    f32::from(i16::from_le_bytes([frame[2], frame[3]])) / 32768.0
78                };
79                [left, right]
80            })
81            .collect();
82        Some(Self {
83            sample_rate,
84            samples: samples.into(),
85        })
86    }
87    pub fn tone(sample_rate: u32, hz: f32, seconds: f32) -> Option<Self> {
88        if !(8000..=96000).contains(&sample_rate)
89            || !hz.is_finite()
90            || hz <= 0.0
91            || hz > sample_rate as f32 * 0.5
92            || !seconds.is_finite()
93            || !(0.0..=10.0).contains(&seconds)
94        {
95            return None;
96        }
97        let n = (sample_rate as f32 * seconds) as usize;
98        let samples: Vec<_> = (0..n)
99            .map(|i| {
100                let envelope = 1.0 - i as f32 / n.max(1) as f32;
101                // Wide phase intermediates prevent loss of precision on long, high-frequency tones.
102                let phase =
103                    i as f64 * f64::from(hz) * std::f64::consts::TAU / f64::from(sample_rate);
104                let v = phase.sin() as f32 * envelope * 0.3;
105                [v, v]
106            })
107            .collect();
108        Some(Self {
109            sample_rate,
110            samples: samples.into(),
111        })
112    }
113}
114fn frame_limit(rate: u32) -> usize {
115    (rate as usize * 600).min(MAX_CLIP_FRAMES)
116}
117struct Voice {
118    clip: Clip,
119    cursor: f64,
120    position: [f32; 3],
121    gain: f32,
122    looping: bool,
123}
124pub struct Mixer {
125    sample_rate: u32,
126    voices: Vec<Voice>,
127    limit: usize,
128    pub listener: [f32; 3],
129    pub right: [f32; 3],
130}
131impl Mixer {
132    pub fn new(sample_rate: u32, limit: usize) -> Option<Self> {
133        if !(8000..=96000).contains(&sample_rate) || !(1..=128).contains(&limit) {
134            return None;
135        }
136        Some(Self {
137            sample_rate,
138            voices: Vec::with_capacity(limit),
139            limit,
140            listener: [0.0; 3],
141            right: [1.0, 0.0, 0.0],
142        })
143    }
144    /// Validates manually constructed clips outside the audio callback.
145    /// Samples must be finite and within [-1,1]; playback never grows the voice allocation.
146    pub fn play(&mut self, clip: Clip, position: [f32; 3], gain: f32, looping: bool) -> bool {
147        if self.voices.len() >= self.limit
148            || clip.samples.is_empty()
149            || !(8000..=96000).contains(&clip.sample_rate)
150            || clip.samples.len() > frame_limit(clip.sample_rate)
151            || !position.iter().all(|v| v.is_finite())
152            || !gain.is_finite()
153            || !(0.0..=4.0).contains(&gain)
154            || clip
155                .samples
156                .iter()
157                .flatten()
158                .any(|v| !v.is_finite() || v.abs() > 1.0)
159        {
160            return false;
161        }
162        self.voices.push(Voice {
163            clip,
164            cursor: 0.0,
165            position,
166            gain,
167            looping,
168        });
169        true
170    }
171    pub fn active_voices(&self) -> usize {
172        self.voices.len()
173    }
174    /// No allocations; finished voices may release their clip Arc. Invalid listener poses mute
175    /// playback while advancing voices. An invalid right vector falls back to centered panning.
176    pub fn render(&mut self, out: &mut [[f32; 2]]) {
177        out.fill([0.0; 2]);
178        let right = self.right.map(f64::from);
179        let right_len = right.iter().map(|v| v * v).sum::<f64>().sqrt();
180        let listener_valid = self.listener.iter().all(|v| v.is_finite());
181        for voice in &mut self.voices {
182            let delta = std::array::from_fn::<_, 3, _>(|i| {
183                f64::from(voice.position[i]) - f64::from(self.listener[i])
184            });
185            let distance_squared = delta.iter().map(|v| v * v).sum::<f64>();
186            let distance = distance_squared.sqrt();
187            let pan = if distance > 1.0e-5 && right_len > 1.0e-5 && right_len.is_finite() {
188                (delta.iter().zip(right).map(|(a, b)| a * b).sum::<f64>() / (distance * right_len))
189                    .clamp(-1.0, 1.0)
190            } else {
191                0.0
192            };
193            let gain = if listener_valid {
194                f64::from(voice.gain) / (1.0 + distance_squared * 0.05)
195            } else {
196                0.0
197            };
198            let gains = [
199                (((1.0 - pan) * 0.5).sqrt() * gain) as f32,
200                (((1.0 + pan) * 0.5).sqrt() * gain) as f32,
201            ];
202            let step = f64::from(voice.clip.sample_rate) / f64::from(self.sample_rate);
203            for sample in out.iter_mut() {
204                let len = voice.clip.samples.len();
205                if voice.cursor >= len as f64 {
206                    if voice.looping {
207                        voice.cursor %= len as f64;
208                    } else {
209                        break;
210                    }
211                }
212                let index = voice.cursor as usize;
213                let next = if index + 1 < len {
214                    index + 1
215                } else if voice.looping {
216                    0
217                } else {
218                    index
219                };
220                let fraction = (voice.cursor - index as f64) as f32;
221                for channel in 0..2 {
222                    let a = voice.clip.samples[index][channel];
223                    let b = voice.clip.samples[next][channel];
224                    sample[channel] += (a + (b - a) * fraction) * gains[channel];
225                }
226                voice.cursor += step;
227            }
228        }
229        self.voices
230            .retain(|v| v.looping || v.cursor < v.clip.samples.len() as f64);
231        for sample in out {
232            for v in sample {
233                *v = if v.is_finite() {
234                    v.clamp(-1.0, 1.0)
235                } else {
236                    0.0
237                };
238            }
239        }
240    }
241}
242pub fn encode_wav(sample_rate: u32, samples: &[[f32; 2]]) -> Option<Vec<u8>> {
243    if !(8000..=96000).contains(&sample_rate) || samples.len() > frame_limit(sample_rate) {
244        return None;
245    }
246    let len = u32::try_from(samples.len().checked_mul(4)?).ok()?;
247    let mut out = Vec::with_capacity(len as usize + 44);
248    out.extend(b"RIFF");
249    out.extend((36 + len).to_le_bytes());
250    out.extend(b"WAVEfmt ");
251    out.extend(16_u32.to_le_bytes());
252    out.extend(1_u16.to_le_bytes());
253    out.extend(2_u16.to_le_bytes());
254    out.extend(sample_rate.to_le_bytes());
255    out.extend((sample_rate * 4).to_le_bytes());
256    out.extend(4_u16.to_le_bytes());
257    out.extend(16_u16.to_le_bytes());
258    out.extend(b"data");
259    out.extend(len.to_le_bytes());
260    for frame in samples {
261        for sample in frame {
262            let v = if sample.is_finite() {
263                sample.clamp(-1.0, 1.0)
264            } else {
265                0.0
266            };
267            let scale = if v < 0.0 { 32768.0 } else { 32767.0 };
268            out.extend(((v * scale).round() as i16).to_le_bytes());
269        }
270    }
271    Some(out)
272}
273#[cfg(test)]
274mod tests {
275    use super::*;
276    #[test]
277    fn spatial_pan_voice_limits_and_resampling() {
278        let clip = Clip {
279            sample_rate: 8000,
280            samples: vec![[0.5; 2]; 8].into(),
281        };
282        let mut m = Mixer::new(16000, 1).unwrap();
283        let capacity = m.voices.capacity();
284        assert!(m.play(clip.clone(), [2.0, 0.0, 0.0], 1.0, false));
285        assert!(!m.play(clip, [0.0; 3], 1.0, true));
286        let mut out = [[0.0; 2]; 32];
287        m.render(&mut out);
288        assert!(out[0][1] > 0.3);
289        assert_eq!(out[0][0], 0.0);
290        assert_eq!(out[16], [0.0; 2]);
291        assert_eq!(m.active_voices(), 0);
292        assert_eq!(m.voices.capacity(), capacity);
293    }
294    #[test]
295    fn wav_roundtrip_and_truncation() {
296        let samples = [[0.25, -0.5]; 100];
297        let bytes = encode_wav(48000, &samples).unwrap();
298        let clip = Clip::wav(&bytes).unwrap();
299        assert_eq!(clip.sample_rate, 48000);
300        assert!((clip.samples[0][0] - 0.25).abs() < 0.0001);
301        for i in 0..bytes.len() {
302            assert!(Clip::wav(&bytes[..i]).is_none());
303        }
304    }
305    #[test]
306    fn invalid_clip_and_listener_do_not_poison_other_voices() {
307        let mut m = Mixer::new(8000, 3).unwrap();
308        assert!(!m.play(
309            Clip {
310                sample_rate: 8000,
311                samples: vec![[f32::NAN; 2]].into()
312            },
313            [0.0; 3],
314            1.0,
315            false
316        ));
317        let clip = Clip {
318            sample_rate: 8000,
319            samples: vec![[0.5; 2]; 4].into(),
320        };
321        assert!(m.play(clip.clone(), [0.0; 3], 1.0, false));
322        m.listener = [f32::NAN; 3];
323        let mut out = [[1.0; 2]; 8];
324        m.render(&mut out);
325        assert_eq!(out, [[0.0; 2]; 8]);
326        assert_eq!(m.active_voices(), 0);
327        m.listener = [-f32::MAX; 3];
328        m.right = [f32::MAX; 3];
329        assert!(m.play(clip, [f32::MAX; 3], 1.0, false));
330        m.render(&mut out);
331        assert!(out.iter().flatten().all(|v| v.is_finite()));
332    }
333    #[test]
334    fn looping_and_block_partition_preserve_sample_time() {
335        let clip = Clip {
336            sample_rate: 8000,
337            samples: vec![[0.2; 2], [0.6; 2]].into(),
338        };
339        let mut a = Mixer::new(12000, 1).unwrap();
340        let mut b = Mixer::new(12000, 1).unwrap();
341        a.play(clip.clone(), [0.0; 3], 1.0, true);
342        b.play(clip, [0.0; 3], 1.0, true);
343        let mut whole = [[0.0; 2]; 30];
344        a.render(&mut whole);
345        let mut pieces = [[0.0; 2]; 30];
346        for part in pieces.chunks_mut(3) {
347            b.render(part);
348        }
349        assert_eq!(whole, pieces);
350    }
351    #[test]
352    fn mono_pcm_is_duplicated_and_chunks_can_arrive_out_of_order() {
353        let mut bytes = encode_wav(8000, &[[1.0, -1.0]]).unwrap();
354        bytes[22..24].copy_from_slice(&1_u16.to_le_bytes());
355        bytes[28..32].copy_from_slice(&16000_u32.to_le_bytes());
356        bytes[32..34].copy_from_slice(&2_u16.to_le_bytes());
357        let mut reordered = bytes[..12].to_vec();
358        reordered.extend_from_slice(&bytes[36..]);
359        reordered.extend_from_slice(&bytes[12..36]);
360        let clip = Clip::wav(&reordered).unwrap();
361        assert_eq!(clip.samples.len(), 2);
362        assert_eq!(clip.samples[0][0], clip.samples[0][1]);
363        assert_eq!(clip.samples[1], [-1.0; 2]);
364    }
365    #[test]
366    fn wav_rejects_huge_lengths_duplicate_chunks_and_missing_padding() {
367        let bytes = encode_wav(8000, &[[1.0, -1.0]]).unwrap();
368        let decoded = Clip::wav(&bytes).unwrap();
369        assert_eq!(decoded.samples[0][1], -1.0);
370        let mut huge = bytes.clone();
371        huge[4..8].copy_from_slice(&u32::MAX.to_le_bytes());
372        assert!(Clip::wav(&huge).is_none());
373        let mut duplicate = bytes.clone();
374        duplicate.extend_from_slice(&bytes[36..]);
375        let declared = (duplicate.len() - 8) as u32;
376        duplicate[4..8].copy_from_slice(&declared.to_le_bytes());
377        assert!(Clip::wav(&duplicate).is_none());
378        let mut odd = bytes;
379        odd.extend(b"JUNK");
380        odd.extend(1_u32.to_le_bytes());
381        odd.push(0);
382        let declared = (odd.len() - 8) as u32;
383        odd[4..8].copy_from_slice(&declared.to_le_bytes());
384        assert!(Clip::wav(&odd).is_none());
385        odd.push(0);
386        let declared = (odd.len() - 8) as u32;
387        odd[4..8].copy_from_slice(&declared.to_le_bytes());
388        assert!(Clip::wav(&odd).is_some());
389    }
390}