Skip to main content

moq_audio/
format.rs

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