1use std::sync::{Arc, Mutex};
20
21use unlit_ecs::sync;
22use unlit_ecs::{Entity, World, bundle};
23
24use crate::audio::{AudioChunk, AudioFillOrder, AudioFormat, GlobalAudioGain, OnAudioFill};
25
26#[derive(Debug, thiserror::Error)]
28#[non_exhaustive]
29pub enum RecordingError {
30 #[error("cannot write a {0:?} recording; use .flac or .wav")]
32 UnsupportedFormat(String),
33 #[error("the file name {0:?} has no extension")]
35 MissingExtension(String),
36 #[error("the encoder configuration is invalid: {0}")]
38 Verify(#[from] flacenc::error::VerifyError),
39 #[error("the samples could not be encoded: {0}")]
41 Encode(#[from] flacenc::error::EncodeError),
42 #[error("the FLAC stream could not be written: {0}")]
44 Write(#[from] flacenc::error::OutputError<flacenc::bitsink::ByteSink>),
45}
46
47#[derive(Clone, Debug)]
53pub struct RecordingHandle {
54 entity: Entity,
55 state: Arc<Mutex<RecordingState>>,
56}
57
58#[derive(Debug)]
60struct RecordingState {
61 format: Option<AudioFormat>,
64 samples: Vec<f32>,
65 max_samples: usize,
67 max_seconds: f32,
69}
70
71#[derive(Clone, Debug, PartialEq)]
73pub struct RecordedAudio {
74 pub format: AudioFormat,
76 pub samples: Vec<f32>,
78}
79
80impl RecordedAudio {
81 #[must_use]
83 pub fn frame_count(&self) -> usize {
84 self.samples.len() / usize::from(self.format.channels).max(1)
85 }
86
87 #[must_use]
89 pub fn seconds(&self) -> f64 {
90 self.frame_count() as f64 / f64::from(self.format.sample_rate.max(1))
91 }
92}
93
94pub fn start_recording(world: &mut World, max_seconds: f32) -> RecordingHandle {
106 let state = Arc::new(Mutex::new(RecordingState {
107 format: None,
108 samples: Vec::new(),
109 max_samples: usize::MAX,
112 max_seconds: max_seconds.max(0.0),
113 }));
114 let recorder = Arc::clone(&state);
115 let entity = world.root_scope_mut().spawn(bundle![OnAudioFill::new(
116 AudioFillOrder(i32::MAX),
117 move |world: &World, _entity: Entity, chunk: &mut AudioChunk| {
118 let gain = {
123 let mut rows = world.root_scope().query::<&GlobalAudioGain>();
124 rows.iter().next().map(|audio_gain| audio_gain.0)
125 }
126 .unwrap_or(1.0);
127 let mut state = sync::lock(&recorder);
128 if state.format.is_none() {
129 let format = AudioFormat {
130 sample_rate: chunk.sample_rate,
131 channels: chunk.channels,
132 };
133 let frames =
134 (f64::from(format.sample_rate) * f64::from(state.max_seconds)) as usize;
135 state.max_samples = frames.saturating_mul(usize::from(format.channels).max(1));
136 state.format = Some(format);
137 }
138 let room = state.max_samples.saturating_sub(state.samples.len());
139 state.samples.extend(
140 chunk
141 .out
142 .iter()
143 .take(room)
144 .map(|sample| (sample * gain).clamp(-1.0, 1.0)),
145 );
146 },
147 ),]);
148 RecordingHandle { entity, state }
149}
150
151const BITS_PER_SAMPLE: usize = i16::BITS as usize;
154
155fn quantize(sample: f32) -> i16 {
157 (sample.clamp(-1.0, 1.0) * f32::from(i16::MAX)).round() as i16
158}
159
160pub(crate) fn write_wav(format: AudioFormat, samples: &[f32]) -> Vec<u8> {
165 let channels = format.channels.max(1);
166 let sample_rate = format.sample_rate.max(1);
167 let block_align = channels * 2;
168 let data_len = (samples.len() * 2) as u32;
169 let mut bytes = Vec::with_capacity(44 + data_len as usize);
170 bytes.extend_from_slice(b"RIFF");
171 bytes.extend_from_slice(&(36 + data_len).to_le_bytes());
172 bytes.extend_from_slice(b"WAVE");
173 bytes.extend_from_slice(b"fmt ");
174 bytes.extend_from_slice(&16u32.to_le_bytes());
175 bytes.extend_from_slice(&1u16.to_le_bytes());
176 bytes.extend_from_slice(&channels.to_le_bytes());
177 bytes.extend_from_slice(&sample_rate.to_le_bytes());
178 bytes.extend_from_slice(&(sample_rate * u32::from(block_align)).to_le_bytes());
179 bytes.extend_from_slice(&block_align.to_le_bytes());
180 bytes.extend_from_slice(&16u16.to_le_bytes());
181 bytes.extend_from_slice(b"data");
182 bytes.extend_from_slice(&data_len.to_le_bytes());
183 for &sample in samples {
184 bytes.extend_from_slice(&quantize(sample).to_le_bytes());
185 }
186 bytes
187}
188
189pub(crate) fn write_flac(format: AudioFormat, samples: &[f32]) -> Result<Vec<u8>, RecordingError> {
196 use flacenc::component::BitRepr;
197 use flacenc::error::Verify;
198
199 let channels = usize::from(format.channels.max(1));
200 let quantized: Vec<i32> = samples
201 .iter()
202 .map(|sample| i32::from(quantize(*sample)))
203 .collect();
204 let config = flacenc::config::Encoder::default();
205 let block_size = config.block_size;
206 let verified = config
207 .into_verified()
208 .map_err(|(_, error)| RecordingError::Verify(error))?;
209 let source = flacenc::source::MemSource::from_samples(
210 &quantized,
211 channels,
212 BITS_PER_SAMPLE,
213 format.sample_rate.max(1) as usize,
214 );
215 let mut stream = flacenc::encode_with_fixed_block_size(&verified, source, block_size)?;
216 stream
222 .stream_info_mut()
223 .set_block_sizes(block_size, block_size)?;
224 let mut sink = flacenc::bitsink::ByteSink::new();
225 stream.write(&mut sink)?;
226 Ok(sink.into_inner())
227}
228
229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
236pub enum RecordingFormat {
237 Wav,
239 Flac,
241}
242
243impl RecordingFormat {
244 pub const DEFAULT: Self = Self::Flac;
249
250 pub fn from_name(name: &str) -> Result<Self, RecordingError> {
257 match name.to_ascii_lowercase().as_str() {
258 "wav" | "wave" => Ok(Self::Wav),
259 "flac" => Ok(Self::Flac),
260 other => Err(RecordingError::UnsupportedFormat(other.to_owned())),
261 }
262 }
263
264 pub fn of(file_name: &str) -> Result<Self, RecordingError> {
271 unlit_ecs::naming::extension_of(file_name).map_or_else(
272 || Err(RecordingError::MissingExtension(file_name.to_owned())),
273 Self::from_name,
274 )
275 }
276
277 #[must_use]
280 pub fn mime_type(self) -> &'static str {
281 match self {
282 Self::Wav => "audio/wav",
283 Self::Flac => "audio/flac",
284 }
285 }
286
287 pub fn encode(self, format: AudioFormat, samples: &[f32]) -> Result<Vec<u8>, RecordingError> {
293 match self {
294 Self::Wav => Ok(write_wav(format, samples)),
295 Self::Flac => write_flac(format, samples),
296 }
297 }
298}
299
300impl RecordingHandle {
301 #[cfg_attr(
307 all(not(test), any(not(feature = "mcp"), target_arch = "wasm32")),
308 expect(dead_code, reason = "only the MCP tools ask for it")
309 )]
310 #[must_use]
311 pub(crate) fn format(&self) -> Option<AudioFormat> {
312 sync::lock(&self.state).format
313 }
314
315 #[must_use]
317 pub fn frame_count(&self) -> usize {
318 let state = sync::lock(&self.state);
319 let channels = state
320 .format
321 .map_or(1, |format| usize::from(format.channels).max(1));
322 state.samples.len() / channels
323 }
324
325 #[must_use]
327 pub fn seconds(&self) -> f64 {
328 let state = sync::lock(&self.state);
329 match state.format {
330 Some(format) => {
331 let channels = usize::from(format.channels).max(1);
332 (state.samples.len() / channels) as f64 / f64::from(format.sample_rate.max(1))
333 }
334 None => 0.0,
335 }
336 }
337
338 #[must_use]
343 pub fn is_full(&self) -> bool {
344 let state = sync::lock(&self.state);
345 state.format.is_some() && state.samples.len() >= state.max_samples
346 }
347
348 #[must_use]
353 pub fn stop(self, world: &mut World) -> Option<RecordedAudio> {
354 world.root_scope_mut().despawn(self.entity).unwrap();
355 let mut state = sync::lock(&self.state);
356 let format = state.format?;
357 Some(RecordedAudio {
358 format,
359 samples: std::mem::take(&mut state.samples),
360 })
361 }
362
363 pub fn cancel(self, world: &mut World) {
365 world.root_scope_mut().despawn(self.entity).unwrap();
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::audio::dispatch_audio_fill;
373
374 fn constant_source(world: &mut World, level: f32) -> Entity {
375 world.root_scope_mut().spawn(bundle![OnAudioFill::new(
376 AudioFillOrder(0),
377 move |_world, _entity, chunk: &mut AudioChunk| {
378 chunk.out.fill(level);
379 },
380 ),])
381 }
382
383 #[test]
384 fn a_recording_captures_the_mixed_samples() {
385 let mut world = World::new();
386 constant_source(&mut world, 0.75);
387 world.root_scope_mut().spawn(bundle![GlobalAudioGain(2.0)]);
388 let format = AudioFormat {
389 sample_rate: 8,
390 channels: 1,
391 };
392 let handle = start_recording(&mut world, 1.0);
393 assert_eq!(handle.format(), None, "nothing fills it yet");
394 let mut out = [0.0f32; 4];
395 dispatch_audio_fill(&world, &mut out, format.sample_rate, format.channels);
396 assert_eq!(
397 out, [1.0f32; 4],
398 "the dispatcher scales the mix once, after every source ran"
399 );
400 assert_eq!(handle.frame_count(), 4);
401 assert!(!handle.is_full());
402 assert_eq!(handle.format(), Some(format));
403 let recorded = handle.stop(&mut world).expect("the recording saw a fill");
404 assert_eq!(recorded.samples, vec![1.0; 4]);
405 assert_eq!(recorded.frame_count(), 4);
406 assert_eq!(recorded.seconds(), 0.5);
407 }
408
409 #[test]
410 fn a_recording_keeps_what_the_output_receives() {
411 let mut world = World::new();
416 constant_source(&mut world, 0.2);
417 world.root_scope_mut().spawn(bundle![GlobalAudioGain(3.0)]);
418 let format = AudioFormat {
419 sample_rate: 8,
420 channels: 1,
421 };
422 let handle = start_recording(&mut world, 1.0);
423 let mut out = [0.0f32; 4];
424 dispatch_audio_fill(&world, &mut out, format.sample_rate, format.channels);
425 assert_eq!(out, [0.6f32; 4], "0.2 scaled by 3, below the clamp");
426 let recorded = handle.stop(&mut world).expect("the recording saw a fill");
427 assert_eq!(recorded.samples, out);
428 }
429
430 #[test]
431 fn a_recording_stops_at_its_limit() {
432 let mut world = World::new();
433 constant_source(&mut world, 0.0);
434 let format = AudioFormat {
435 sample_rate: 4,
436 channels: 1,
437 };
438 let handle = start_recording(&mut world, 0.5);
439 let mut out = [0.0f32; 8];
440 dispatch_audio_fill(&world, &mut out, format.sample_rate, format.channels);
441 assert_eq!(handle.frame_count(), 2);
442 assert!(handle.is_full());
443 handle.cancel(&mut world);
444 }
445
446 #[test]
447 fn a_full_recording_keeps_nothing_from_later_fills() {
448 let mut world = World::new();
449 constant_source(&mut world, 0.5);
450 world.root_scope_mut().spawn(bundle![GlobalAudioGain(2.0)]);
451 let format = AudioFormat {
452 sample_rate: 4,
453 channels: 2,
454 };
455 let handle = start_recording(&mut world, 0.5);
456 let mut out = [0.0f32; 8];
457 dispatch_audio_fill(&world, &mut out, format.sample_rate, format.channels);
458 assert_eq!(handle.frame_count(), 2);
460 assert!(handle.is_full());
461 out.fill(1.0);
462 dispatch_audio_fill(&world, &mut out, format.sample_rate, format.channels);
463 let recorded = handle.stop(&mut world).expect("the recording saw a fill");
464 assert_eq!(recorded.samples, vec![1.0; 4]);
465 }
466
467 #[test]
468 fn a_recording_nobody_filled_hands_back_nothing() {
469 let mut world = World::new();
470 let handle = start_recording(&mut world, 1.0);
471 assert!(!handle.is_full());
472 assert_eq!(handle.seconds(), 0.0);
473 assert_eq!(handle.stop(&mut world), None);
474 }
475
476 #[test]
477 fn a_wav_file_describes_its_samples() {
478 let format = AudioFormat {
479 sample_rate: 8000,
480 channels: 2,
481 };
482 let bytes = write_wav(format, &[0.0, -1.0, 1.0, 2.0]);
483 assert_eq!(&bytes[0..4], b"RIFF");
484 assert_eq!(&bytes[8..12], b"WAVE");
485 assert_eq!(&bytes[12..16], b"fmt ");
486 assert_eq!(&bytes[36..40], b"data");
487 assert_eq!(bytes.len(), 44 + 8);
488 assert_eq!(
489 u32::from_le_bytes(bytes[4..8].try_into().unwrap()),
490 44 + 8 - 8
491 );
492 assert_eq!(u16::from_le_bytes(bytes[20..22].try_into().unwrap()), 1);
493 assert_eq!(u16::from_le_bytes(bytes[22..24].try_into().unwrap()), 2);
494 assert_eq!(u32::from_le_bytes(bytes[24..28].try_into().unwrap()), 8000);
495 assert_eq!(u32::from_le_bytes(bytes[40..44].try_into().unwrap()), 8);
496 assert_eq!(i16::from_le_bytes(bytes[44..46].try_into().unwrap()), 0);
497 assert_eq!(
498 i16::from_le_bytes(bytes[46..48].try_into().unwrap()),
499 -i16::MAX
500 );
501 assert_eq!(
502 i16::from_le_bytes(bytes[50..52].try_into().unwrap()),
503 i16::MAX
504 );
505 }
506
507 #[test]
508 fn a_flac_stream_decodes_back_to_the_quantized_samples() {
509 let format = AudioFormat {
510 sample_rate: 8000,
511 channels: 2,
512 };
513 let samples = [0.0, -1.0, 1.0, 2.0];
514 let bytes = write_flac(format, &samples).expect("the stream encodes");
515 assert_eq!(&bytes[0..4], b"fLaC");
516 let mut reader =
517 claxon::FlacReader::new(bytes.as_slice()).expect("the stream is decodable");
518 let info = reader.streaminfo();
519 assert_eq!(info.channels, 2);
520 assert_eq!(info.sample_rate, 8000);
521 assert_eq!(
522 info.bits_per_sample, BITS_PER_SAMPLE as u32,
523 "the stream holds 16-bit samples"
524 );
525 assert_eq!(info.min_block_size, info.max_block_size);
529 let decoded: Vec<i16> = reader
530 .samples()
531 .map(|sample| sample.expect("a decodable sample") as i16)
532 .collect();
533 assert_eq!(decoded, vec![0, -i16::MAX, i16::MAX, i16::MAX]);
534 }
535
536 #[test]
537 fn an_extension_or_name_names_the_recording_format() {
538 assert_eq!(
539 RecordingFormat::of("take.flac").unwrap(),
540 RecordingFormat::Flac
541 );
542 assert_eq!(
543 RecordingFormat::of("take.wav").unwrap(),
544 RecordingFormat::Wav
545 );
546 assert_eq!(
547 RecordingFormat::of("TAKE.FLAC").unwrap(),
548 RecordingFormat::Flac
549 );
550 assert_eq!(
551 RecordingFormat::from_name("wave").unwrap(),
552 RecordingFormat::Wav
553 );
554 assert_eq!(
555 RecordingFormat::from_name("flac").unwrap(),
556 RecordingFormat::Flac
557 );
558 assert!(RecordingFormat::of("take").is_err());
559 assert!(RecordingFormat::of("take.mp3").is_err());
560 }
561
562 #[test]
563 fn both_formats_hold_the_same_samples() {
564 let format = AudioFormat {
565 sample_rate: 8000,
566 channels: 1,
567 };
568 let samples = [0.0, -1.0, 1.0, 2.0];
569 let wav = RecordingFormat::Wav
570 .encode(format, &samples)
571 .expect("the WAV encodes");
572 let flac = RecordingFormat::Flac
573 .encode(format, &samples)
574 .expect("the stream encodes");
575 let mut reader = claxon::FlacReader::new(flac.as_slice()).expect("the stream is decodable");
576 let decoded: Vec<i16> = reader
577 .samples()
578 .map(|sample| sample.expect("a decodable sample") as i16)
579 .collect();
580 let pcm: Vec<i16> = wav[44..]
581 .as_chunks::<2>()
582 .0
583 .iter()
584 .map(|sample| i16::from_le_bytes(*sample))
585 .collect();
586 assert_eq!(decoded, pcm);
587 }
588}