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::audio::{AudioDecoderOptions, CODEC_ID_NULL_AUDIO},
18 errors::Error as SymphoniaError,
19 formats::{FormatOptions, probe::Hint},
20 io::MediaSourceStream,
21 meta::MetadataOptions,
22};
23
24pub const MASTER_SAMPLE_RATE: u32 = 48_000;
26
27pub const MASTER_CHANNELS: u16 = 2;
29
30#[derive(Debug)]
32pub struct AudioError(String);
33
34impl fmt::Display for AudioError {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 write!(f, "audio error: {}", self.0)
37 }
38}
39
40impl std::error::Error for AudioError {}
41
42impl From<std::io::Error> for AudioError {
43 fn from(err: std::io::Error) -> Self {
44 Self(err.to_string())
45 }
46}
47
48#[derive(Debug, Clone)]
55pub struct AudioClip {
56 sample_rate: u32,
57 channels: u16,
58 pcm: Arc<[f32]>,
59}
60
61impl AudioClip {
62 pub fn from_pcm(pcm: impl Into<Arc<[f32]>>, sample_rate: u32, channels: u16) -> Self {
64 assert!(sample_rate > 0, "sample rate must be positive");
65 assert!(channels > 0, "channels must be positive");
66 Self {
67 sample_rate,
68 channels,
69 pcm: pcm.into(),
70 }
71 }
72
73 pub fn sine(freq: f64, secs: f64, amplitude: f64) -> Self {
75 let sample_rate = MASTER_SAMPLE_RATE;
76 let len = (secs * sample_rate as f64) as usize;
77 let pcm: Vec<f32> = (0..len)
78 .map(|i| (amplitude * (TAU * freq * i as f64 / sample_rate as f64).sin()) as f32)
79 .collect();
80 Self::from_pcm(pcm, sample_rate, 1)
81 }
82
83 #[cfg(feature = "audio-decode")]
94 pub fn from_bytes(bytes: &[u8]) -> Result<Self, AudioError> {
95 decode(bytes)
96 }
97
98 #[cfg(feature = "audio-decode")]
104 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, AudioError> {
105 Self::from_bytes(&std::fs::read(path)?)
106 }
107
108 pub fn sample_rate(&self) -> u32 {
110 self.sample_rate
111 }
112
113 pub fn channels(&self) -> u16 {
115 self.channels
116 }
117
118 pub fn pcm(&self) -> &Arc<[f32]> {
120 &self.pcm
121 }
122
123 pub fn duration_secs(&self) -> f64 {
125 self.frames() as f64 / self.sample_rate as f64
126 }
127
128 fn frames(&self) -> usize {
129 self.pcm.len() / self.channels as usize
130 }
131}
132
133#[cfg(feature = "audio-decode")]
138fn decode(bytes: &[u8]) -> Result<AudioClip, AudioError> {
139 let mss = MediaSourceStream::new(
140 Box::new(Cursor::new(bytes.to_vec())),
142 Default::default(),
143 );
144 let mut format = symphonia::default::get_probe()
145 .probe(
146 &Hint::new(),
147 mss,
148 FormatOptions::default(),
149 MetadataOptions::default(),
150 )
151 .map_err(|err| AudioError(format!("failed to probe audio: {err}")))?;
152 let (track_id, codec_params) = format
153 .tracks()
154 .iter()
155 .find_map(|track| {
156 let params = track.codec_params.as_ref()?.audio()?;
157 (params.codec != CODEC_ID_NULL_AUDIO).then_some((track.id, params))
158 })
159 .ok_or_else(|| AudioError("no decodable audio track".to_string()))?;
160 let mut decoder = symphonia::default::get_codecs()
161 .make_audio_decoder(codec_params, &AudioDecoderOptions::default())
162 .map_err(|err| AudioError(format!("unsupported codec: {err}")))?;
163
164 let mut pcm = Vec::new();
165 let mut spec = None;
166 loop {
167 let packet = match format.next_packet() {
168 Ok(Some(packet)) => packet,
169 Ok(None) => break,
170 Err(SymphoniaError::IoError(_)) => break,
171 Err(err) => return Err(AudioError(format!("failed to read packet: {err}"))),
172 };
173 if packet.track_id != track_id {
174 continue;
175 }
176 let decoded = match decoder.decode(&packet) {
177 Ok(decoded) => decoded,
178 Err(SymphoniaError::DecodeError(_)) => continue,
179 Err(SymphoniaError::IoError(_)) => break,
180 Err(err) => return Err(AudioError(format!("failed to decode packet: {err}"))),
181 };
182 if spec.as_ref().is_some_and(|spec| spec != decoded.spec()) {
183 return Err(AudioError("stream changes format mid-way".to_string()));
184 }
185 spec.get_or_insert_with(|| decoded.spec().clone());
186 let start = pcm.len();
187 pcm.resize(start + decoded.samples_interleaved(), 0.0);
188 decoded.copy_to_slice_interleaved(&mut pcm[start..]);
189 }
190 let spec = spec.ok_or_else(|| AudioError("no audio frames decoded".to_string()))?;
191
192 let src_channels = spec.channels().count();
195 let frames = pcm.len() / src_channels;
196 let mut stereo = vec![0.0; frames * MASTER_CHANNELS as usize];
197 for (frame, out) in stereo
198 .as_chunks_mut::<{ MASTER_CHANNELS as usize }>()
199 .0
200 .iter_mut()
201 .enumerate()
202 {
203 for (channel, slot) in out.iter_mut().enumerate() {
204 let src = frame * src_channels + channel.min(src_channels - 1);
205 *slot = pcm[src];
206 }
207 }
208 let pcm = if spec.rate() != MASTER_SAMPLE_RATE {
209 resample_to_master(stereo, spec.rate())?
210 } else {
211 stereo
212 };
213 Ok(AudioClip {
214 sample_rate: MASTER_SAMPLE_RATE,
215 channels: MASTER_CHANNELS,
216 pcm: pcm.into(),
217 })
218}
219
220#[cfg(feature = "audio-decode")]
223fn resample_to_master(pcm: Vec<f32>, src_rate: u32) -> Result<Vec<f32>, AudioError> {
224 use rubato::audioadapter_buffers::owned::InterleavedOwned;
225 use rubato::{Fft, FixedSync, Resampler};
226
227 let channels = MASTER_CHANNELS as usize;
228 let frames = pcm.len() / channels;
229 let input = InterleavedOwned::<f32>::new_from(pcm, channels, frames)
230 .map_err(|err| AudioError(format!("resampler input: {err}")))?;
231 let mut resampler = Fft::<f32>::new(
232 src_rate as usize,
233 MASTER_SAMPLE_RATE as usize,
234 1024,
235 channels,
236 FixedSync::Both,
237 )
238 .map_err(|err| AudioError(format!("failed to init resampler: {err}")))?;
239 let output = resampler
240 .process_all(&input, frames, None)
241 .map_err(|err| AudioError(format!("resampling failed: {err}")))?;
242 Ok(output.take_data())
243}
244
245#[derive(Debug, Clone)]
253pub struct AudioTrack {
254 clip: AudioClip,
255 gain: f64,
256 fade_in_secs: f64,
257 fade_out_secs: f64,
258 play_range: Option<Range<f64>>,
259}
260
261impl AudioTrack {
262 pub fn new(clip: AudioClip) -> Self {
264 Self {
265 clip,
266 gain: 1.0,
267 fade_in_secs: 0.0,
268 fade_out_secs: 0.0,
269 play_range: None,
270 }
271 }
272
273 pub fn with_gain(mut self, gain: f64) -> Self {
275 self.gain = gain;
276 self
277 }
278
279 pub fn with_fade_in(mut self, secs: f64) -> Self {
281 self.fade_in_secs = secs;
282 self
283 }
284
285 pub fn with_fade_out(mut self, secs: f64) -> Self {
287 self.fade_out_secs = secs;
288 self
289 }
290
291 pub fn with_play_secs(mut self, range: Range<f64>) -> Self {
300 self.play_range = Some(range);
301 self
302 }
303
304 fn source_span(&self) -> (f64, f64) {
307 let duration = self.clip.duration_secs();
308 match &self.play_range {
309 None => (0.0, duration),
310 Some(range) => {
311 let start = range.start.max(0.0);
312 (start, range.end.min(duration).max(start))
313 }
314 }
315 }
316
317 pub(crate) fn play_window_secs(&self) -> f64 {
319 let (start, end) = self.source_span();
320 end - start
321 }
322}
323
324impl AudioTrack {
325 pub(crate) fn sample_at(&self, own: f64) -> [f32; 2] {
331 const SILENCE: [f32; 2] = [0.0; 2];
332 let clip_frames = self.clip.frames();
333 if clip_frames == 0 {
334 return SILENCE;
335 }
336 let (start, end) = self.source_span();
337 let play_len = end - start;
338 if !(0.0..play_len).contains(&own) {
339 return SILENCE;
340 }
341 let mut envelope = self.gain;
342 if self.fade_in_secs > 0.0 && own < self.fade_in_secs {
343 envelope *= own / self.fade_in_secs;
344 }
345 if self.fade_out_secs > 0.0 {
346 envelope *= ((play_len - own) / self.fade_out_secs).min(1.0);
347 }
348 let envelope = envelope as f32;
349 let src_pos = (start + own) * self.clip.sample_rate as f64;
351 let f0 = src_pos.floor() as usize;
352 if f0 >= clip_frames {
353 return SILENCE;
354 }
355 let frac = (src_pos - src_pos.floor()) as f32;
356 let f1 = (f0 + 1).min(clip_frames - 1);
357 let clip_channels = self.clip.channels as usize;
358 let sample = |frame: usize, channel: usize| {
359 self.clip.pcm[frame * clip_channels + channel.min(clip_channels - 1)]
360 };
361 let mut out = SILENCE;
362 for (channel, slot) in out.iter_mut().enumerate() {
363 let value = sample(f0, channel) + (sample(f1, channel) - sample(f0, channel)) * frac;
364 *slot = value * envelope;
365 }
366 out
367 }
368
369 pub(crate) fn mix_span_into(
379 &self,
380 a: f64,
381 b: f64,
382 g_lo: usize,
383 g_hi: usize,
384 sample_rate: f64,
385 pcm: &mut [f32],
386 ) {
387 let (start, end) = self.source_span();
388 let play_len = end - start;
389 let clip_frames = self.clip.frames();
390 if clip_frames == 0 || play_len <= 0.0 {
391 return;
392 }
393 let clip_rate = self.clip.sample_rate as f64;
394 let clip_channels = self.clip.channels as usize;
395 let sample = |frame: usize, channel: usize| {
396 self.clip.pcm[frame * clip_channels + channel.min(clip_channels - 1)]
397 };
398 for g in g_lo..g_hi {
399 let own = a + b * (g as f64 / sample_rate);
400 if !(0.0..play_len).contains(&own) {
403 continue;
404 }
405 let mut envelope = self.gain;
406 if self.fade_in_secs > 0.0 && own < self.fade_in_secs {
407 envelope *= own / self.fade_in_secs;
408 }
409 if self.fade_out_secs > 0.0 {
410 envelope *= ((play_len - own) / self.fade_out_secs).min(1.0);
411 }
412 let envelope = envelope as f32;
413 let src_pos = (start + own) * clip_rate;
414 let f0 = src_pos.floor() as usize;
415 if f0 >= clip_frames {
416 continue;
417 }
418 let frac = (src_pos - src_pos.floor()) as f32;
419 let f1 = (f0 + 1).min(clip_frames - 1);
420 let value = |channel: usize| {
421 sample(f0, channel) + (sample(f1, channel) - sample(f0, channel)) * frac
422 };
423 pcm[g * 2] += value(0) * envelope;
424 pcm[g * 2 + 1] += value(1) * envelope;
425 }
426 }
427}
428
429pub fn write_wav(
431 path: impl AsRef<Path>,
432 pcm: &[f32],
433 sample_rate: u32,
434 channels: u16,
435) -> std::io::Result<()> {
436 let data_len = (pcm.len() * 4) as u32;
437 let mut file = std::fs::File::create(path)?;
438 let write_chunk =
439 |file: &mut std::fs::File, id: &[u8; 4], payload: &[u8]| -> std::io::Result<()> {
440 file.write_all(id)?;
441 file.write_all(&(payload.len() as u32).to_le_bytes())?;
442 file.write_all(payload)
443 };
444 file.write_all(b"RIFF")?;
446 file.write_all(&(4 + 8 + 16 + 8 + data_len).to_le_bytes())?;
447 file.write_all(b"WAVE")?;
448 let mut fmt_chunk = Vec::with_capacity(16);
449 fmt_chunk.extend_from_slice(&3u16.to_le_bytes()); fmt_chunk.extend_from_slice(&channels.to_le_bytes());
451 fmt_chunk.extend_from_slice(&sample_rate.to_le_bytes());
452 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)?;
456 let mut data = Vec::with_capacity(pcm.len() * 4);
457 for sample in pcm {
458 data.extend_from_slice(&sample.to_le_bytes());
459 }
460 write_chunk(&mut file, b"data", &data)?;
461 Ok(())
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 const EPS: f32 = 1e-6;
469
470 fn constant(value: f32, secs: f64) -> AudioClip {
471 let pcm = vec![value; (secs * MASTER_SAMPLE_RATE as f64) as usize * 2];
472 AudioClip::from_pcm(pcm, MASTER_SAMPLE_RATE, 2)
473 }
474
475 #[test]
476 fn fades_ramp_linearly() {
477 let track = AudioTrack::new(constant(1.0, 2.0))
478 .with_fade_in(1.0)
479 .with_fade_out(1.0);
480 for (sec, expected) in [(0.0, 0.0), (0.5, 0.5), (1.0, 1.0), (1.5, 0.5), (2.0, 0.0)] {
481 assert!(
482 (track.sample_at(sec)[0] - expected).abs() < EPS,
483 "at {sec}s"
484 );
485 }
486 }
487
488 #[test]
489 fn gain_and_play_window_modify_the_track() {
490 let track = AudioTrack::new(constant(0.5, 4.0))
491 .with_gain(0.2)
492 .with_play_secs(0.0..1.0);
493 assert!((track.sample_at(0.5)[0] - 0.1).abs() < EPS);
494 assert_eq!(track.sample_at(1.0), [0.0; 2]);
495 }
496
497 #[test]
498 fn play_range_skips_into_the_clip() {
499 let ramp: Vec<f32> = (0..16).flat_map(|i| [i as f32, i as f32]).collect();
503 let clip = AudioClip::from_pcm(ramp, 4, 2);
504 let track = AudioTrack::new(clip).with_play_secs(1.0..3.0);
505 assert!((track.sample_at(0.0)[0] - 4.0).abs() < EPS);
506 assert!((track.sample_at(1.0)[0] - 8.0).abs() < EPS);
507 assert!(track.sample_at(2.0)[0].abs() < EPS);
508 assert!((track.play_window_secs() - 2.0).abs() < f64::from(EPS));
509 }
510
511 #[test]
512 fn play_range_is_clamped_to_the_clip() {
513 let clip = AudioClip::from_pcm(vec![0.5; 8], 4, 1);
514 let track = AudioTrack::new(clip.clone()).with_play_secs(0.0..10.0);
516 assert!((track.play_window_secs() - 2.0).abs() < f64::from(EPS));
517 let track = AudioTrack::new(clip.clone()).with_play_secs(-1.0..2.0);
519 assert!((track.play_window_secs() - 2.0).abs() < f64::from(EPS));
520 let track = AudioTrack::new(clip.clone()).with_play_secs(3.0..1.0);
522 assert_eq!(track.play_window_secs(), 0.0);
523 assert!(track.sample_at(0.0)[0].abs() < EPS);
524 let track = AudioTrack::new(clip).with_play_secs(5.0..9.0);
525 assert_eq!(track.play_window_secs(), 0.0);
526 }
527
528 #[test]
529 fn mono_clip_duplicates_to_stereo() {
530 let track = AudioTrack::new(AudioClip::from_pcm(vec![0.5; 48], 48, 1));
531 assert_eq!(track.sample_at(0.5), [0.5; 2]);
532 }
533
534 #[test]
535 fn clip_is_resampled_by_linear_interpolation() {
536 let clip = AudioClip::from_pcm(vec![0.0, 2.0], 24, 1);
542 let track = AudioTrack::new(clip);
543 assert!((track.sample_at(0.0 / 48.0)[0] - 0.0).abs() < EPS);
544 assert!((track.sample_at(1.0 / 48.0)[0] - 1.0).abs() < EPS);
545 assert!((track.sample_at(2.0 / 48.0)[0] - 2.0).abs() < EPS);
546 assert!((track.sample_at(3.0 / 48.0)[0] - 2.0).abs() < EPS);
547 assert!(track.sample_at(4.0 / 48.0)[0].abs() < EPS);
548 }
549
550 #[test]
551 fn wav_roundtrip_has_a_sound_header() {
552 let path = std::env::temp_dir().join(format!("ranim-wav-test-{}.wav", std::process::id()));
553 let pcm = vec![0.25f32; 480];
554 write_wav(&path, &pcm, MASTER_SAMPLE_RATE, MASTER_CHANNELS).unwrap();
555 let bytes = std::fs::read(&path).unwrap();
556 std::fs::remove_file(&path).unwrap();
557 assert_eq!(&bytes[0..4], b"RIFF");
558 assert_eq!(&bytes[8..12], b"WAVE");
559 assert_eq!(&bytes[12..16], b"fmt ");
560 let channels = u16::from_le_bytes(bytes[22..24].try_into().unwrap());
561 assert_eq!(channels, MASTER_CHANNELS);
562 assert_eq!(&bytes[36..40], b"data");
563 let data_len = u32::from_le_bytes(bytes[40..44].try_into().unwrap());
564 assert_eq!(data_len as usize, pcm.len() * 4);
565 }
566
567 #[test]
568 #[cfg(feature = "audio-decode")]
569 fn from_file_decodes_the_written_wav() {
570 let path =
571 std::env::temp_dir().join(format!("ranim-decode-test-{}.wav", std::process::id()));
572 let pcm = vec![0.5f32; 480 * MASTER_CHANNELS as usize];
573 write_wav(&path, &pcm, MASTER_SAMPLE_RATE, MASTER_CHANNELS).unwrap();
574 let clip = AudioClip::from_file(&path).unwrap();
575 std::fs::remove_file(&path).unwrap();
576 assert_eq!(clip.sample_rate(), MASTER_SAMPLE_RATE);
577 assert_eq!(clip.channels(), MASTER_CHANNELS);
578 assert!((clip.duration_secs() - 480.0 / MASTER_SAMPLE_RATE as f64).abs() < 1e-9);
579 assert!((clip.pcm()[0] - 0.5).abs() < 1e-6);
580 }
581
582 #[test]
583 #[cfg(feature = "audio-decode")]
584 fn decoding_resamples_to_master_rate() {
585 let rate = 24_000u32;
589 let click = rate as usize / 2;
590 let mut pcm = vec![0.0f32; rate as usize * 2];
591 pcm[click * 2] = 1.0;
592 pcm[click * 2 + 1] = 1.0;
593 let path =
594 std::env::temp_dir().join(format!("ranim-resample-test-{}.wav", std::process::id()));
595 write_wav(&path, &pcm, rate, MASTER_CHANNELS).unwrap();
596 let clip = AudioClip::from_file(&path).unwrap();
597 std::fs::remove_file(&path).unwrap();
598 assert_eq!(clip.sample_rate(), MASTER_SAMPLE_RATE);
599 assert_eq!(clip.channels(), MASTER_CHANNELS);
600 let samples = clip.pcm();
601 let peak = samples
602 .as_chunks::<2>()
603 .0
604 .iter()
605 .enumerate()
606 .max_by(|a, b| a.1[0].abs().total_cmp(&b.1[0].abs()))
607 .unwrap()
608 .0;
609 assert!(
610 (peak as i64 - 24_000).abs() < 1_000,
611 "click at frame {peak}, expected ~24000"
612 );
613 assert!(
614 (samples.len() as i64 / 2 - 48_000).abs() < 2_000,
615 "duration {} frames, expected ~48000",
616 samples.len() / 2
617 );
618 }
619
620 #[test]
621 #[cfg(feature = "audio-decode")]
622 fn decoding_duplicates_mono_to_stereo() {
623 let pcm = vec![0.5f32; MASTER_SAMPLE_RATE as usize];
624 let path = std::env::temp_dir().join(format!("ranim-mono-test-{}.wav", std::process::id()));
625 write_wav(&path, &pcm, MASTER_SAMPLE_RATE, 1).unwrap();
626 let clip = AudioClip::from_file(&path).unwrap();
627 std::fs::remove_file(&path).unwrap();
628 assert_eq!(clip.sample_rate(), MASTER_SAMPLE_RATE);
629 assert_eq!(clip.channels(), MASTER_CHANNELS);
630 assert_eq!(clip.pcm().len(), MASTER_SAMPLE_RATE as usize * 2);
631 assert!(clip.pcm().iter().all(|&s| (s - 0.5).abs() < 1e-6));
632 }
633
634 #[test]
635 #[cfg(feature = "audio-decode")]
636 fn decoding_garbage_is_an_error() {
637 assert!(AudioClip::from_bytes(b"not an audio file").is_err());
638 }
639}