Skip to main content

moq_audio/
format.rs

1use std::borrow::Cow;
2
3use crate::AudioError;
4
5/// Raw PCM sample format.
6///
7/// Mirrors the WebCodecs `AudioData.format` enum so callers can pass
8/// microphone or speaker buffers across the FFI boundary unchanged.
9///
10/// Interleaved variants pack samples as `[c0_s0, c1_s0, c0_s1, c1_s1, ...]`.
11/// Planar variants pack as `[c0_s0, c0_s1, ..., c1_s0, c1_s1, ...]`.
12///
13/// See <https://developer.mozilla.org/en-US/docs/Web/API/AudioData/format>.
14#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum AudioFormat {
17	U8,
18	S16,
19	S32,
20	/// Default: matches libopus's native interleaved layout.
21	#[default]
22	F32,
23	U8Planar,
24	S16Planar,
25	S32Planar,
26	F32Planar,
27}
28
29impl AudioFormat {
30	/// Bytes used per single-channel sample.
31	pub fn bytes_per_sample(self) -> usize {
32		match self {
33			Self::U8 | Self::U8Planar => 1,
34			Self::S16 | Self::S16Planar => 2,
35			Self::S32 | Self::S32Planar | Self::F32 | Self::F32Planar => 4,
36		}
37	}
38
39	/// Whether channels are stored planar (each channel contiguous) rather than interleaved.
40	pub fn is_planar(self) -> bool {
41		matches!(
42			self,
43			Self::U8Planar | Self::S16Planar | Self::S32Planar | Self::F32Planar
44		)
45	}
46
47	/// Whether the underlying sample type is floating-point.
48	pub fn is_float(self) -> bool {
49		matches!(self, Self::F32 | Self::F32Planar)
50	}
51
52	/// Convert a raw PCM buffer in this format to interleaved `f32` in `[-1.0, 1.0]`.
53	///
54	/// Returns a [`Cow::Borrowed`] when the input is already interleaved `f32`
55	/// (and the byte buffer is aligned), avoiding an allocation. Otherwise
56	/// returns a [`Cow::Owned`] holding the converted samples.
57	pub fn as_interleaved_f32<'a>(self, data: &'a [u8], channels: u32) -> Result<Cow<'a, [f32]>, AudioError> {
58		let channels = channels as usize;
59		if channels == 0 {
60			return Err(AudioError::Unsupported("channel count must be > 0".into()));
61		}
62
63		let bps = self.bytes_per_sample();
64		if data.len() % (bps * channels) != 0 {
65			return Err(AudioError::Misaligned {
66				got: data.len(),
67				expected: data.len().next_multiple_of(bps * channels),
68			});
69		}
70
71		// Fast path: already in the codec's working format and aligned.
72		if self == Self::F32 {
73			let (head, body, tail) = unsafe { data.align_to::<f32>() };
74			if head.is_empty() && tail.is_empty() {
75				return Ok(Cow::Borrowed(body));
76			}
77		}
78
79		Ok(Cow::Owned(self.to_interleaved_f32_owned(data, channels)))
80	}
81
82	fn to_interleaved_f32_owned(self, data: &[u8], channels: usize) -> Vec<f32> {
83		let total_samples = data.len() / self.bytes_per_sample();
84		let frames = total_samples / channels;
85		let mut out = vec![0.0f32; total_samples];
86
87		match self {
88			Self::F32 => {
89				for (i, chunk) in data.chunks_exact(4).enumerate() {
90					out[i] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
91				}
92			}
93			Self::F32Planar => {
94				for ch in 0..channels {
95					let plane = &data[ch * frames * 4..(ch + 1) * frames * 4];
96					for (frame, chunk) in plane.chunks_exact(4).enumerate() {
97						out[frame * channels + ch] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
98					}
99				}
100			}
101			Self::S16 => {
102				for (i, chunk) in data.chunks_exact(2).enumerate() {
103					let v = i16::from_le_bytes([chunk[0], chunk[1]]);
104					out[i] = (v as f32) / 32768.0;
105				}
106			}
107			Self::S16Planar => {
108				for ch in 0..channels {
109					let plane = &data[ch * frames * 2..(ch + 1) * frames * 2];
110					for (frame, chunk) in plane.chunks_exact(2).enumerate() {
111						let v = i16::from_le_bytes([chunk[0], chunk[1]]);
112						out[frame * channels + ch] = (v as f32) / 32768.0;
113					}
114				}
115			}
116			Self::S32 => {
117				for (i, chunk) in data.chunks_exact(4).enumerate() {
118					let v = i32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
119					out[i] = (v as f32) / (i32::MAX as f32 + 1.0);
120				}
121			}
122			Self::S32Planar => {
123				for ch in 0..channels {
124					let plane = &data[ch * frames * 4..(ch + 1) * frames * 4];
125					for (frame, chunk) in plane.chunks_exact(4).enumerate() {
126						let v = i32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
127						out[frame * channels + ch] = (v as f32) / (i32::MAX as f32 + 1.0);
128					}
129				}
130			}
131			Self::U8 => {
132				for (i, &b) in data.iter().enumerate() {
133					out[i] = (b as f32 - 128.0) / 128.0;
134				}
135			}
136			Self::U8Planar => {
137				for ch in 0..channels {
138					let plane = &data[ch * frames..(ch + 1) * frames];
139					for (frame, &b) in plane.iter().enumerate() {
140						out[frame * channels + ch] = (b as f32 - 128.0) / 128.0;
141					}
142				}
143			}
144		}
145
146		out
147	}
148
149	/// Convert interleaved `f32` PCM to this format's raw byte
150	/// representation. Returns owned bytes; integer formats clamp
151	/// out-of-range samples rather than wrapping.
152	pub fn from_interleaved_f32(self, samples: &[f32], channels: u32) -> Result<Vec<u8>, AudioError> {
153		let channels = channels as usize;
154		if channels == 0 {
155			return Err(AudioError::Unsupported("channel count must be > 0".into()));
156		}
157		if samples.len() % channels != 0 {
158			return Err(AudioError::Misaligned {
159				got: samples.len(),
160				expected: samples.len().next_multiple_of(channels),
161			});
162		}
163
164		let frames = samples.len() / channels;
165		let mut out = vec![0u8; samples.len() * self.bytes_per_sample()];
166
167		match self {
168			Self::F32 => {
169				for (i, &s) in samples.iter().enumerate() {
170					out[i * 4..i * 4 + 4].copy_from_slice(&s.to_le_bytes());
171				}
172			}
173			Self::F32Planar => {
174				for ch in 0..channels {
175					let plane = &mut out[ch * frames * 4..(ch + 1) * frames * 4];
176					for (frame, chunk) in plane.chunks_exact_mut(4).enumerate() {
177						chunk.copy_from_slice(&samples[frame * channels + ch].to_le_bytes());
178					}
179				}
180			}
181			Self::S16 => {
182				for (i, &s) in samples.iter().enumerate() {
183					let v = (s.clamp(-1.0, 1.0) * 32767.0).round() as i16;
184					out[i * 2..i * 2 + 2].copy_from_slice(&v.to_le_bytes());
185				}
186			}
187			Self::S16Planar => {
188				for ch in 0..channels {
189					let plane = &mut out[ch * frames * 2..(ch + 1) * frames * 2];
190					for (frame, chunk) in plane.chunks_exact_mut(2).enumerate() {
191						let v = (samples[frame * channels + ch].clamp(-1.0, 1.0) * 32767.0).round() as i16;
192						chunk.copy_from_slice(&v.to_le_bytes());
193					}
194				}
195			}
196			Self::S32 => {
197				for (i, &s) in samples.iter().enumerate() {
198					let v = (s.clamp(-1.0, 1.0) as f64 * i32::MAX as f64).round() as i32;
199					out[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
200				}
201			}
202			Self::S32Planar => {
203				for ch in 0..channels {
204					let plane = &mut out[ch * frames * 4..(ch + 1) * frames * 4];
205					for (frame, chunk) in plane.chunks_exact_mut(4).enumerate() {
206						let v =
207							(samples[frame * channels + ch].clamp(-1.0, 1.0) as f64 * i32::MAX as f64).round() as i32;
208						chunk.copy_from_slice(&v.to_le_bytes());
209					}
210				}
211			}
212			Self::U8 => {
213				for (i, &s) in samples.iter().enumerate() {
214					out[i] = ((s.clamp(-1.0, 1.0) * 127.0).round() + 128.0) as u8;
215				}
216			}
217			Self::U8Planar => {
218				for ch in 0..channels {
219					let plane = &mut out[ch * frames..(ch + 1) * frames];
220					for (frame, byte) in plane.iter_mut().enumerate() {
221						*byte = ((samples[frame * channels + ch].clamp(-1.0, 1.0) * 127.0).round() + 128.0) as u8;
222					}
223				}
224			}
225		}
226
227		Ok(out)
228	}
229}
230
231#[cfg(test)]
232mod tests {
233	use super::*;
234
235	#[test]
236	fn f32_interleaved_is_borrowed() {
237		let samples: Vec<f32> = vec![0.1, 0.2, 0.3, 0.4];
238		let bytes: Vec<u8> = samples.iter().flat_map(|s| s.to_le_bytes()).collect();
239		let cow = AudioFormat::F32.as_interleaved_f32(&bytes, 2).unwrap();
240		assert!(matches!(cow, Cow::Borrowed(_)));
241		assert_eq!(cow.as_ref(), samples.as_slice());
242	}
243
244	#[test]
245	fn s16_interleaved_is_owned_but_close() {
246		let samples = vec![-1.0_f32, -0.5, 0.0, 0.5, 0.9999];
247		let bytes = AudioFormat::S16.from_interleaved_f32(&samples, 1).unwrap();
248		let cow = AudioFormat::S16.as_interleaved_f32(&bytes, 1).unwrap();
249		assert!(matches!(cow, Cow::Owned(_)));
250		for (a, b) in samples.iter().zip(cow.iter()) {
251			assert!((a - b).abs() < 1.0 / 32767.0, "{a} vs {b}");
252		}
253	}
254
255	#[test]
256	fn planar_to_interleaved_orders_correctly() {
257		let planar: Vec<f32> = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6];
258		let bytes: Vec<u8> = planar.iter().flat_map(|s| s.to_le_bytes()).collect();
259		let cow = AudioFormat::F32Planar.as_interleaved_f32(&bytes, 2).unwrap();
260		assert_eq!(cow.as_ref(), &[0.1, 0.4, 0.2, 0.5, 0.3, 0.6]);
261	}
262
263	#[test]
264	fn s16_clamps_out_of_range() {
265		let samples = vec![2.0_f32, -3.0];
266		let bytes = AudioFormat::S16.from_interleaved_f32(&samples, 1).unwrap();
267		let cow = AudioFormat::S16.as_interleaved_f32(&bytes, 1).unwrap();
268		assert!((cow[0] - 0.99997).abs() < 1e-4);
269		assert!((cow[1] + 1.0).abs() < 1e-4);
270	}
271
272	#[test]
273	fn rejects_misaligned_buffer() {
274		let result = AudioFormat::S16.as_interleaved_f32(&[0u8; 5], 2);
275		assert!(matches!(result, Err(AudioError::Misaligned { .. })));
276	}
277}