falcon_library/audio/mod.rs
1//! Audio decoding: the one path playback, seeking, durations, waveforms
2//! and the tempo all read audio through.
3//!
4//! [`open`] returns an [`AudioSource`], a stream of interleaved `f32`
5//! blocks that can seek. The main backend is symphonia (pure Rust,
6//! MPL-2.0) with its MP3, AAC, ALAC, FLAC, Vorbis, PCM and ADPCM decoders
7//! in MP4/M4A, Ogg, WAV, FLAC, MP3 and ADTS files, plus the pure-Rust
8//! `opus-decoder` crate for Opus (in Ogg or M4A, which Eagle's Chromium
9//! player also plays). A file that doesn't decode returns an
10//! [`AudioError`] and callers fall back (the viewer shows the reason, the
11//! waveform is flat, no duration is stored). When an LGPL FFmpeg is
12//! installed (`falcon-ffmpeg`, loaded at run time), [`open`] tries it after
13//! symphonia, which covers the soundtracks of video files.
14
15mod adts;
16mod edit;
17mod ffmpeg;
18mod native;
19mod opus;
20
21use std::fmt;
22use std::path::Path;
23
24pub use ffmpeg::FfmpegSource;
25pub use native::SymphoniaSource;
26pub(crate) use native::open_format;
27
28/// A decoded audio stream.
29pub trait AudioSource: Send {
30 /// Channels in each block's frames.
31 fn channels(&self) -> u16;
32 /// Frames per second.
33 fn sample_rate(&self) -> u32;
34 /// The stream's length in seconds, when the file says.
35 fn duration(&self) -> Option<f64>;
36 /// The next block of samples, or `None` at the end.
37 fn next_block(&mut self) -> Option<Block<'_>>;
38 /// Moves so the next block starts at `secs` (clamped to the stream)
39 /// and returns the time it starts at.
40 fn seek(&mut self, secs: f64) -> Result<f64, AudioError>;
41 /// Where the next block starts, in seconds.
42 fn position(&self) -> f64;
43 /// Which decoder backs the stream, for messages and tests.
44 fn backend(&self) -> &'static str;
45}
46
47/// Decoded audio: interleaved samples in -1..=1, never empty.
48#[derive(Debug, Clone, Copy)]
49pub struct Block<'a> {
50 pub samples: &'a [f32],
51 pub channels: u16,
52 pub rate: u32,
53}
54
55impl Block<'_> {
56 /// The samples one frame at a time.
57 pub fn frames(&self) -> std::slice::Chunks<'_, f32> {
58 self.samples.chunks(usize::from(self.channels.max(1)))
59 }
60}
61
62/// Why a file can't be decoded.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum AudioError {
65 /// The file couldn't be read.
66 Io(String),
67 /// No backend recognises the container.
68 UnknownFormat,
69 /// The container holds no audio track.
70 NoAudioTrack,
71 /// The track's codec has no decoder here.
72 UnsupportedCodec,
73 /// The stream couldn't be moved to the asked time.
74 Seek(String),
75}
76
77impl fmt::Display for AudioError {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 match self {
80 AudioError::Io(e) => f.write_str(e),
81 AudioError::UnknownFormat => f.write_str("unsupported audio format"),
82 AudioError::NoAudioTrack => f.write_str("no audio track"),
83 AudioError::UnsupportedCodec => f.write_str("unsupported audio codec"),
84 AudioError::Seek(e) => write!(f, "seek failed: {e}"),
85 }
86 }
87}
88
89impl std::error::Error for AudioError {}
90
91/// Opens an audio file (`ext` is a hint; the contents decide). Backends
92/// are tried in turn; the first error is kept when none can.
93pub fn open(path: &Path, ext: &str) -> Result<Box<dyn AudioSource>, AudioError> {
94 match SymphoniaSource::open(path, ext) {
95 Ok(s) => Ok(Box::new(s)),
96 Err(AudioError::Io(e)) => Err(AudioError::Io(e)),
97 Err(e) => FfmpegSource::open(path).map(|s| Box::new(s) as Box<dyn AudioSource>).map_err(|_| e),
98 }
99}
100
101/// Audio formats that decode, by extension, with the codecs each may hold.
102pub const DECODED: [(&str, &str); 6] = [
103 ("mp3", "MP3"),
104 ("wav", "PCM, IEEE float, A-law, ยต-law, IMA and MS ADPCM"),
105 ("flac", "FLAC"),
106 ("ogg", "Vorbis, Opus, FLAC"),
107 ("aac", "AAC-LC (ADTS)"),
108 ("m4a", "AAC-LC, ALAC, FLAC, Opus"),
109];
110
111#[cfg(test)]
112mod tests;