1#[cfg(feature = "audio-decode")]
12use std::io::Cursor;
13use std::ops::Range;
14use std::{f64::consts::TAU, fmt, io::Write, path::Path, sync::Arc};
15#[cfg(feature = "audio-decode")]
16use symphonia::core::{
17 codecs::{CODEC_TYPE_NULL, DecoderOptions},
18 errors::Error as SymphoniaError,
19 formats::FormatOptions,
20 io::MediaSourceStream,
21 meta::MetadataOptions,
22 probe::Hint,
23};
24
25pub const MASTER_SAMPLE_RATE: u32 = 48_000;
27
28pub const MASTER_CHANNELS: u16 = 2;
30
31#[derive(Debug)]
33pub struct AudioError(String);
34
35impl fmt::Display for AudioError {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 write!(f, "audio error: {}", self.0)
38 }
39}
40
41impl std::error::Error for AudioError {}
42
43impl From<std::io::Error> for AudioError {
44 fn from(err: std::io::Error) -> Self {
45 Self(err.to_string())
46 }
47}
48
49#[derive(Debug, Clone)]
56pub struct AudioClip {
57 sample_rate: u32,
58 channels: u16,
59 pcm: Arc<[f32]>,
60}
61
62impl AudioClip {
63 pub fn from_pcm(pcm: impl Into<Arc<[f32]>>, sample_rate: u32, channels: u16) -> Self {
65 assert!(sample_rate > 0, "sample rate must be positive");
66 assert!(channels > 0, "channels must be positive");
67 Self {
68 sample_rate,
69 channels,
70 pcm: pcm.into(),
71 }
72 }
73
74 pub fn sine(freq: f64, secs: f64, amplitude: f64) -> Self {
76 let sample_rate = MASTER_SAMPLE_RATE;
77 let len = (secs * sample_rate as f64) as usize;
78 let pcm: Vec<f32> = (0..len)
79 .map(|i| (amplitude * (TAU * freq * i as f64 / sample_rate as f64).sin()) as f32)
80 .collect();
81 Self::from_pcm(pcm, sample_rate, 1)
82 }
83
84 #[cfg(feature = "audio-decode")]
95 pub fn from_bytes(bytes: &[u8]) -> Result<Self, AudioError> {
96 decode(bytes)
97 }
98
99 #[cfg(feature = "audio-decode")]
105 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, AudioError> {
106 Self::from_bytes(&std::fs::read(path)?)
107 }
108
109 pub fn sample_rate(&self) -> u32 {
111 self.sample_rate
112 }
113
114 pub fn channels(&self) -> u16 {
116 self.channels
117 }
118
119 pub fn pcm(&self) -> &Arc<[f32]> {
121 &self.pcm
122 }
123
124 pub fn duration_secs(&self) -> f64 {
126 self.frames() as f64 / self.sample_rate as f64
127 }
128
129 fn frames(&self) -> usize {
130 self.pcm.len() / self.channels as usize
131 }
132}
133
134#[cfg(feature = "audio-decode")]
139fn decode(bytes: &[u8]) -> Result<AudioClip, AudioError> {
140 use symphonia::core::audio::SampleBuffer;
141 use symphonia::core::units::Duration;
142
143 let mss = MediaSourceStream::new(
144 Box::new(Cursor::new(bytes.to_vec())),
146 Default::default(),
147 );
148 let mut probed = symphonia::default::get_probe()
149 .format(
150 &Hint::new(),
151 mss,
152 &FormatOptions::default(),
153 &MetadataOptions::default(),
154 )
155 .map_err(|err| AudioError(format!("failed to probe audio: {err}")))?;
156 let format = &mut probed.format;
157 let track = format
158 .tracks()
159 .iter()
160 .find(|track| track.codec_params.codec != CODEC_TYPE_NULL)
161 .ok_or_else(|| AudioError("no decodable audio track".to_string()))?;
162 let track_id = track.id;
163 let mut decoder = symphonia::default::get_codecs()
164 .make(&track.codec_params, &DecoderOptions::default())
165 .map_err(|err| AudioError(format!("unsupported codec: {err}")))?;
166
167 let mut pcm = Vec::new();
168 let mut spec = None;
169 loop {
170 let packet = match format.next_packet() {
171 Ok(packet) => packet,
172 Err(SymphoniaError::IoError(_)) => break,
174 Err(err) => return Err(AudioError(format!("failed to read packet: {err}"))),
175 };
176 if packet.track_id() != track_id {
177 continue;
178 }
179 let decoded = match decoder.decode(&packet) {
180 Ok(decoded) => decoded,
181 Err(SymphoniaError::DecodeError(_)) => continue,
182 Err(SymphoniaError::IoError(_)) => break,
183 Err(err) => return Err(AudioError(format!("failed to decode packet: {err}"))),
184 };
185 if spec.unwrap_or(*decoded.spec()) != *decoded.spec() {
186 return Err(AudioError("stream changes format mid-way".to_string()));
187 }
188 spec = Some(*decoded.spec());
189 let mut buffer = SampleBuffer::<f32>::new(decoded.capacity() as Duration, *decoded.spec());
190 buffer.copy_interleaved_ref(decoded);
191 pcm.extend_from_slice(buffer.samples());
192 }
193 let spec = spec.ok_or_else(|| AudioError("no audio frames decoded".to_string()))?;
194
195 let src_channels = spec.channels.count();
198 let frames = pcm.len() / src_channels;
199 let mut stereo = vec![0.0; frames * MASTER_CHANNELS as usize];
200 for (frame, out) in stereo
201 .as_chunks_mut::<{ MASTER_CHANNELS as usize }>()
202 .0
203 .iter_mut()
204 .enumerate()
205 {
206 for (channel, slot) in out.iter_mut().enumerate() {
207 let src = frame * src_channels + channel.min(src_channels - 1);
208 *slot = pcm[src];
209 }
210 }
211 let pcm = if spec.rate != MASTER_SAMPLE_RATE {
212 resample_to_master(stereo, spec.rate)?
213 } else {
214 stereo
215 };
216 Ok(AudioClip {
217 sample_rate: MASTER_SAMPLE_RATE,
218 channels: MASTER_CHANNELS,
219 pcm: pcm.into(),
220 })
221}
222
223#[cfg(feature = "audio-decode")]
226fn resample_to_master(pcm: Vec<f32>, src_rate: u32) -> Result<Vec<f32>, AudioError> {
227 use rubato::audioadapter_buffers::owned::InterleavedOwned;
228 use rubato::{Fft, FixedSync, Resampler};
229
230 let channels = MASTER_CHANNELS as usize;
231 let frames = pcm.len() / channels;
232 let input = InterleavedOwned::<f32>::new_from(pcm, channels, frames)
233 .map_err(|err| AudioError(format!("resampler input: {err}")))?;
234 let mut resampler = Fft::<f32>::new(
235 src_rate as usize,
236 MASTER_SAMPLE_RATE as usize,
237 1024,
238 channels,
239 FixedSync::Both,
240 )
241 .map_err(|err| AudioError(format!("failed to init resampler: {err}")))?;
242 let output = resampler
243 .process_all(&input, frames, None)
244 .map_err(|err| AudioError(format!("resampling failed: {err}")))?;
245 Ok(output.take_data())
246}
247
248#[derive(Debug, Clone)]
256pub struct AudioTrack {
257 clip: AudioClip,
258 gain: f64,
259 fade_in_secs: f64,
260 fade_out_secs: f64,
261 play_range: Option<Range<f64>>,
262}
263
264impl AudioTrack {
265 pub fn new(clip: AudioClip) -> Self {
267 Self {
268 clip,
269 gain: 1.0,
270 fade_in_secs: 0.0,
271 fade_out_secs: 0.0,
272 play_range: None,
273 }
274 }
275
276 pub fn with_gain(mut self, gain: f64) -> Self {
278 self.gain = gain;
279 self
280 }
281
282 pub fn with_fade_in(mut self, secs: f64) -> Self {
284 self.fade_in_secs = secs;
285 self
286 }
287
288 pub fn with_fade_out(mut self, secs: f64) -> Self {
290 self.fade_out_secs = secs;
291 self
292 }
293
294 pub fn with_play_secs(mut self, range: Range<f64>) -> Self {
303 self.play_range = Some(range);
304 self
305 }
306
307 fn source_span(&self) -> (f64, f64) {
310 let duration = self.clip.duration_secs();
311 match &self.play_range {
312 None => (0.0, duration),
313 Some(range) => {
314 let start = range.start.max(0.0);
315 (start, range.end.min(duration).max(start))
316 }
317 }
318 }
319
320 pub(crate) fn play_window_secs(&self) -> f64 {
322 let (start, end) = self.source_span();
323 end - start
324 }
325}
326
327impl AudioTrack {
328 pub(crate) fn sample_at(&self, own: f64) -> [f32; 2] {
334 const SILENCE: [f32; 2] = [0.0; 2];
335 let clip_frames = self.clip.frames();
336 if clip_frames == 0 {
337 return SILENCE;
338 }
339 let (start, end) = self.source_span();
340 let play_len = end - start;
341 if !(0.0..play_len).contains(&own) {
342 return SILENCE;
343 }
344 let mut envelope = self.gain;
345 if self.fade_in_secs > 0.0 && own < self.fade_in_secs {
346 envelope *= own / self.fade_in_secs;
347 }
348 if self.fade_out_secs > 0.0 {
349 envelope *= ((play_len - own) / self.fade_out_secs).min(1.0);
350 }
351 let envelope = envelope as f32;
352 let src_pos = (start + own) * self.clip.sample_rate as f64;
354 let f0 = src_pos.floor() as usize;
355 if f0 >= clip_frames {
356 return SILENCE;
357 }
358 let frac = (src_pos - src_pos.floor()) as f32;
359 let f1 = (f0 + 1).min(clip_frames - 1);
360 let clip_channels = self.clip.channels as usize;
361 let sample = |frame: usize, channel: usize| {
362 self.clip.pcm[frame * clip_channels + channel.min(clip_channels - 1)]
363 };
364 let mut out = SILENCE;
365 for (channel, slot) in out.iter_mut().enumerate() {
366 let value = sample(f0, channel) + (sample(f1, channel) - sample(f0, channel)) * frac;
367 *slot = value * envelope;
368 }
369 out
370 }
371
372 pub(crate) fn mix_span_into(
382 &self,
383 a: f64,
384 b: f64,
385 g_lo: usize,
386 g_hi: usize,
387 sample_rate: f64,
388 pcm: &mut [f32],
389 ) {
390 let (start, end) = self.source_span();
391 let play_len = end - start;
392 let clip_frames = self.clip.frames();
393 if clip_frames == 0 || play_len <= 0.0 {
394 return;
395 }
396 let clip_rate = self.clip.sample_rate as f64;
397 let clip_channels = self.clip.channels as usize;
398 let sample = |frame: usize, channel: usize| {
399 self.clip.pcm[frame * clip_channels + channel.min(clip_channels - 1)]
400 };
401 for g in g_lo..g_hi {
402 let own = a + b * (g as f64 / sample_rate);
403 if !(0.0..play_len).contains(&own) {
406 continue;
407 }
408 let mut envelope = self.gain;
409 if self.fade_in_secs > 0.0 && own < self.fade_in_secs {
410 envelope *= own / self.fade_in_secs;
411 }
412 if self.fade_out_secs > 0.0 {
413 envelope *= ((play_len - own) / self.fade_out_secs).min(1.0);
414 }
415 let envelope = envelope as f32;
416 let src_pos = (start + own) * clip_rate;
417 let f0 = src_pos.floor() as usize;
418 if f0 >= clip_frames {
419 continue;
420 }
421 let frac = (src_pos - src_pos.floor()) as f32;
422 let f1 = (f0 + 1).min(clip_frames - 1);
423 let value = |channel: usize| {
424 sample(f0, channel) + (sample(f1, channel) - sample(f0, channel)) * frac
425 };
426 pcm[g * 2] += value(0) * envelope;
427 pcm[g * 2 + 1] += value(1) * envelope;
428 }
429 }
430}
431
432pub fn write_wav(
434 path: impl AsRef<Path>,
435 pcm: &[f32],
436 sample_rate: u32,
437 channels: u16,
438) -> std::io::Result<()> {
439 let data_len = (pcm.len() * 4) as u32;
440 let mut file = std::fs::File::create(path)?;
441 let write_chunk =
442 |file: &mut std::fs::File, id: &[u8; 4], payload: &[u8]| -> std::io::Result<()> {
443 file.write_all(id)?;
444 file.write_all(&(payload.len() as u32).to_le_bytes())?;
445 file.write_all(payload)
446 };
447 file.write_all(b"RIFF")?;
449 file.write_all(&(4 + 8 + 16 + 8 + data_len).to_le_bytes())?;
450 file.write_all(b"WAVE")?;
451 let mut fmt_chunk = Vec::with_capacity(16);
452 fmt_chunk.extend_from_slice(&3u16.to_le_bytes()); fmt_chunk.extend_from_slice(&channels.to_le_bytes());
454 fmt_chunk.extend_from_slice(&sample_rate.to_le_bytes());
455 fmt_chunk.extend_from_slice(&(sample_rate * channels as u32 * 4).to_le_bytes()); fmt_chunk.extend_from_slice(&(channels * 4).to_le_bytes()); fmt_chunk.extend_from_slice(&32u16.to_le_bytes()); write_chunk(&mut file, b"fmt ", &fmt_chunk)?;
459 let mut data = Vec::with_capacity(pcm.len() * 4);
460 for sample in pcm {
461 data.extend_from_slice(&sample.to_le_bytes());
462 }
463 write_chunk(&mut file, b"data", &data)?;
464 Ok(())
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 const EPS: f32 = 1e-6;
472
473 fn constant(value: f32, secs: f64) -> AudioClip {
474 let pcm = vec![value; (secs * MASTER_SAMPLE_RATE as f64) as usize * 2];
475 AudioClip::from_pcm(pcm, MASTER_SAMPLE_RATE, 2)
476 }
477
478 fn sample_of(buf: &[f32], sec: f64) -> f32 {
479 buf[(sec * MASTER_SAMPLE_RATE as f64) as usize * 2]
480 }
481
482 fn sampled(track: &AudioTrack, sec: f64) -> [f32; 2] {
484 track.sample_at(sec)
485 }
486
487 fn summed(tracks: &[AudioTrack], sec: f64) -> [f32; 2] {
489 let mut acc = [0.0_f32; 2];
490 for track in tracks {
491 let [l, r] = track.sample_at(sec);
492 acc[0] += l;
493 acc[1] += r;
494 }
495 acc
496 }
497
498 fn mixed(tracks: &[AudioTrack], total_secs: f64) -> Vec<f32> {
501 let out_frames = (total_secs * MASTER_SAMPLE_RATE as f64).ceil() as usize;
502 let mut out = vec![0.0; out_frames * MASTER_CHANNELS as usize];
503 for frame in 0..out_frames {
504 let t = frame as f64 / MASTER_SAMPLE_RATE as f64;
505 let [l, r] = summed(tracks, t);
506 out[frame * 2] = l;
507 out[frame * 2 + 1] = r;
508 }
509 out
510 }
511
512 #[test]
513 fn overlapping_tracks_sum() {
514 let buf = mixed(
515 &[
516 AudioTrack::new(constant(0.25, 2.0)),
517 AudioTrack::new(constant(0.25, 1.0)),
518 ],
519 3.0,
520 );
521 assert!((sample_of(&buf, 0.5) - 0.5).abs() < EPS);
522 assert!((sample_of(&buf, 1.5) - 0.25).abs() < EPS);
523 assert!(sample_of(&buf, 2.5).abs() < EPS);
524 }
525
526 #[test]
527 fn fades_ramp_linearly() {
528 let buf = mixed(
529 &[AudioTrack::new(constant(1.0, 2.0))
530 .with_fade_in(1.0)
531 .with_fade_out(1.0)],
532 2.0,
533 );
534 assert!(sample_of(&buf, 0.0).abs() < EPS);
535 assert!((sample_of(&buf, 0.5) - 0.5).abs() < EPS);
536 assert!((sample_of(&buf, 1.0) - 1.0).abs() < EPS);
537 assert!((sample_of(&buf, 1.5) - 0.5).abs() < EPS);
538 assert!(sample_of(&buf, 1.999).abs() < 2e-3);
539 }
540
541 #[test]
542 fn gain_and_play_window_modify_the_track() {
543 let buf = mixed(&[AudioTrack::new(constant(0.5, 1.0)).with_gain(0.2)], 1.0);
544 assert!((sample_of(&buf, 0.5) - 0.1).abs() < EPS);
545
546 let buf = mixed(
547 &[AudioTrack::new(constant(0.5, 4.0)).with_play_secs(0.0..1.0)],
548 3.0,
549 );
550 assert!((sample_of(&buf, 0.6) - 0.5).abs() < EPS);
551 assert!(sample_of(&buf, 1.6).abs() < EPS);
552 }
553
554 #[test]
555 fn play_range_skips_into_the_clip() {
556 let ramp: Vec<f32> = (0..16).flat_map(|i| [i as f32, i as f32]).collect();
560 let clip = AudioClip::from_pcm(ramp, 4, 2);
561 let track = AudioTrack::new(clip).with_play_secs(1.0..3.0);
562 assert!((sampled(&track, 0.0)[0] - 4.0).abs() < EPS);
563 assert!((sampled(&track, 1.0)[0] - 8.0).abs() < EPS);
564 assert!(sampled(&track, 2.0)[0].abs() < EPS);
565 assert!((track.play_window_secs() - 2.0).abs() < f64::from(EPS));
566 }
567
568 #[test]
569 fn play_range_is_clamped_to_the_clip() {
570 let clip = AudioClip::from_pcm(vec![0.5; 8], 4, 1);
571 let track = AudioTrack::new(clip.clone()).with_play_secs(0.0..10.0);
573 assert!((track.play_window_secs() - 2.0).abs() < f64::from(EPS));
574 let track = AudioTrack::new(clip.clone()).with_play_secs(-1.0..2.0);
576 assert!((track.play_window_secs() - 2.0).abs() < f64::from(EPS));
577 let track = AudioTrack::new(clip.clone()).with_play_secs(3.0..1.0);
579 assert_eq!(track.play_window_secs(), 0.0);
580 assert!(sampled(&track, 0.0)[0].abs() < EPS);
581 let track = AudioTrack::new(clip).with_play_secs(5.0..9.0);
582 assert_eq!(track.play_window_secs(), 0.0);
583 }
584
585 #[test]
586 fn track_is_trimmed_to_buffer_end() {
587 let buf = mixed(&[AudioTrack::new(constant(0.5, 10.0))], 1.0);
588 assert_eq!(buf.len(), MASTER_SAMPLE_RATE as usize * 2);
589 assert!((sample_of(&buf, 0.99) - 0.5).abs() < EPS);
590 }
591
592 #[test]
593 fn mono_clip_duplicates_to_stereo() {
594 let clip = AudioClip::from_pcm(vec![0.5; 48], 48, 1);
595 let buf = mixed(&[AudioTrack::new(clip)], 1.0);
596 assert!((buf[0] - 0.5).abs() < EPS && (buf[1] - 0.5).abs() < EPS);
597 }
598
599 #[test]
600 fn clip_is_resampled_by_linear_interpolation() {
601 let clip = AudioClip::from_pcm(vec![0.0, 2.0], 24, 1);
607 let track = AudioTrack::new(clip);
608 assert!((sampled(&track, 0.0 / 48.0)[0] - 0.0).abs() < EPS);
609 assert!((sampled(&track, 1.0 / 48.0)[0] - 1.0).abs() < EPS);
610 assert!((sampled(&track, 2.0 / 48.0)[0] - 2.0).abs() < EPS);
611 assert!((sampled(&track, 3.0 / 48.0)[0] - 2.0).abs() < EPS);
612 assert!(sampled(&track, 4.0 / 48.0)[0].abs() < EPS);
613 }
614
615 #[test]
616 fn wav_roundtrip_has_a_sound_header() {
617 let path = std::env::temp_dir().join(format!("ranim-wav-test-{}.wav", std::process::id()));
618 let pcm = vec![0.25f32; 480];
619 write_wav(&path, &pcm, MASTER_SAMPLE_RATE, MASTER_CHANNELS).unwrap();
620 let bytes = std::fs::read(&path).unwrap();
621 std::fs::remove_file(&path).unwrap();
622 assert_eq!(&bytes[0..4], b"RIFF");
623 assert_eq!(&bytes[8..12], b"WAVE");
624 assert_eq!(&bytes[12..16], b"fmt ");
625 let channels = u16::from_le_bytes(bytes[22..24].try_into().unwrap());
626 assert_eq!(channels, MASTER_CHANNELS);
627 assert_eq!(&bytes[36..40], b"data");
628 let data_len = u32::from_le_bytes(bytes[40..44].try_into().unwrap());
629 assert_eq!(data_len as usize, pcm.len() * 4);
630 }
631
632 #[test]
633 #[cfg(feature = "audio-decode")]
634 fn from_file_decodes_the_written_wav() {
635 let path =
636 std::env::temp_dir().join(format!("ranim-decode-test-{}.wav", std::process::id()));
637 let pcm = vec![0.5f32; 480 * MASTER_CHANNELS as usize];
638 write_wav(&path, &pcm, MASTER_SAMPLE_RATE, MASTER_CHANNELS).unwrap();
639 let clip = AudioClip::from_file(&path).unwrap();
640 std::fs::remove_file(&path).unwrap();
641 assert_eq!(clip.sample_rate(), MASTER_SAMPLE_RATE);
642 assert_eq!(clip.channels(), MASTER_CHANNELS);
643 assert!((clip.duration_secs() - 480.0 / MASTER_SAMPLE_RATE as f64).abs() < 1e-9);
644 assert!((clip.pcm()[0] - 0.5).abs() < 1e-6);
645 }
646
647 #[test]
648 #[cfg(feature = "audio-decode")]
649 fn decoding_resamples_to_master_rate() {
650 let rate = 24_000u32;
654 let click = rate as usize / 2;
655 let mut pcm = vec![0.0f32; rate as usize * 2];
656 pcm[click * 2] = 1.0;
657 pcm[click * 2 + 1] = 1.0;
658 let path =
659 std::env::temp_dir().join(format!("ranim-resample-test-{}.wav", std::process::id()));
660 write_wav(&path, &pcm, rate, MASTER_CHANNELS).unwrap();
661 let clip = AudioClip::from_file(&path).unwrap();
662 std::fs::remove_file(&path).unwrap();
663 assert_eq!(clip.sample_rate(), MASTER_SAMPLE_RATE);
664 assert_eq!(clip.channels(), MASTER_CHANNELS);
665 let samples = clip.pcm();
666 let peak = samples
667 .as_chunks::<2>()
668 .0
669 .iter()
670 .enumerate()
671 .max_by(|a, b| a.1[0].abs().total_cmp(&b.1[0].abs()))
672 .unwrap()
673 .0;
674 assert!(
675 (peak as i64 - 24_000).abs() < 1_000,
676 "click at frame {peak}, expected ~24000"
677 );
678 assert!(
679 (samples.len() as i64 / 2 - 48_000).abs() < 2_000,
680 "duration {} frames, expected ~48000",
681 samples.len() / 2
682 );
683 }
684
685 #[test]
686 #[cfg(feature = "audio-decode")]
687 fn decoding_duplicates_mono_to_stereo() {
688 let pcm = vec![0.5f32; MASTER_SAMPLE_RATE as usize];
689 let path = std::env::temp_dir().join(format!("ranim-mono-test-{}.wav", std::process::id()));
690 write_wav(&path, &pcm, MASTER_SAMPLE_RATE, 1).unwrap();
691 let clip = AudioClip::from_file(&path).unwrap();
692 std::fs::remove_file(&path).unwrap();
693 assert_eq!(clip.sample_rate(), MASTER_SAMPLE_RATE);
694 assert_eq!(clip.channels(), MASTER_CHANNELS);
695 assert_eq!(clip.pcm().len(), MASTER_SAMPLE_RATE as usize * 2);
696 assert!(clip.pcm().iter().all(|&s| (s - 0.5).abs() < 1e-6));
697 }
698
699 #[test]
700 #[cfg(feature = "audio-decode")]
701 fn decoding_garbage_is_an_error() {
702 assert!(AudioClip::from_bytes(b"not an audio file").is_err());
703 }
704}