167 lines
6.4 KiB
Rust
167 lines
6.4 KiB
Rust
//! What counts as audio, and what the player can actually decode.
|
|
//!
|
|
//! These were seven separate hand-kept lists — the library scanner, the share
|
|
//! index, the row pruner, the loudness pass, and three in the frontend — and
|
|
//! they disagreed. The scanner indexed `.aiff`, the pruner did not recognise it
|
|
//! and DELETED the rows on the next startup. The share index knew `.ape`, the
|
|
//! scanner did not. Every list named `.opus`, which nothing here can decode.
|
|
//!
|
|
//! One table now, with a distinction the old lists could not express:
|
|
//!
|
|
//! - [`PLAYABLE`] — the decoder can turn this into sound. Derived from the
|
|
//! codecs symphonia is actually built with (`all-codecs` + `all-formats`:
|
|
//! aac, adpcm, alac, flac, mp1/2/3, pcm, vorbis; caf, isomp4, mkv, ogg,
|
|
//! aiff, wav) and verified against real files by
|
|
//! `cargo run -p player --example probe_formats`.
|
|
//! - [`RECOGNISED`] — audio we can index, share, download and show, but cannot
|
|
//! play. Naming these is the point: a file the app can see and refuses to
|
|
//! play with a clear reason is honest, whereas leaving it out of the tables
|
|
//! makes it invisible, and claiming it plays makes the player fail at the
|
|
//! moment the user presses play.
|
|
//!
|
|
//! Adding a codec means adding a decoder, not editing a list.
|
|
|
|
/// Extensions the player can decode. Verified, not assumed.
|
|
pub const PLAYABLE: &[&str] = &[
|
|
// FLAC
|
|
"flac",
|
|
// MPEG audio — the bundle provides layers I, II and III
|
|
"mp3", "mp2", "mp1",
|
|
// ISO/MP4 container: AAC and ALAC live here
|
|
"m4a", "m4b", "mp4", "m4r",
|
|
// Raw AAC (ADTS)
|
|
"aac",
|
|
// Ogg container, Vorbis inside
|
|
"ogg", "oga",
|
|
// RIFF: PCM and ADPCM
|
|
"wav", "wave",
|
|
// AIFF / AIFF-C, also via the RIFF reader
|
|
"aiff", "aif", "aifc",
|
|
// Core Audio Format
|
|
"caf",
|
|
// Matroska — FLAC/Vorbis/AAC in a different wrapper
|
|
"mka", "webm",
|
|
];
|
|
|
|
/// Audio we recognise but cannot decode. Indexed and shared; never played.
|
|
///
|
|
/// `wv` earns its place here more than any of them. A WavPack file that
|
|
/// carries the original RIFF header in its wrapper is probed as raw PCM, so
|
|
/// the decoder does not fail — it reads compressed bytes as samples and emits
|
|
/// NaN into the mixer. Refusing it by extension is the only thing that stops
|
|
/// that, because the decoder itself reports success.
|
|
pub const RECOGNISED: &[&str] = &[
|
|
"opus", // no Opus decoder is built in
|
|
"wma", "ape", "wv", "mpc", "tta", "shn", "dsf", "dff", "spx", "ra", "au", "amr",
|
|
];
|
|
|
|
/// Is this an audio file at all — playable or merely recognised?
|
|
///
|
|
/// The scanner, the share index and the row pruner must all answer this the
|
|
/// same way, or a file is indexed by one and deleted by another.
|
|
pub fn is_audio_ext(ext: &str) -> bool {
|
|
let e = ext.to_ascii_lowercase();
|
|
PLAYABLE.contains(&e.as_str()) || RECOGNISED.contains(&e.as_str())
|
|
}
|
|
|
|
/// Can the player turn this into sound?
|
|
pub fn is_playable_ext(ext: &str) -> bool {
|
|
PLAYABLE.contains(&ext.to_ascii_lowercase().as_str())
|
|
}
|
|
|
|
/// Why this file will not play, in words that can go on screen.
|
|
pub fn unplayable_reason(ext: &str) -> Option<&'static str> {
|
|
match ext.to_ascii_lowercase().as_str() {
|
|
"opus" => Some("Opus isn't supported yet — this build has no Opus decoder"),
|
|
"wv" => Some("WavPack isn't supported yet"),
|
|
"ape" => Some("Monkey's Audio (APE) isn't supported yet"),
|
|
"wma" => Some("Windows Media Audio isn't supported yet"),
|
|
"mpc" => Some("Musepack isn't supported yet"),
|
|
"tta" => Some("True Audio isn't supported yet"),
|
|
"shn" => Some("Shorten isn't supported yet"),
|
|
"dsf" | "dff" => Some("DSD isn't supported yet"),
|
|
e if RECOGNISED.contains(&e) => Some("this format isn't supported yet"),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn ext_of(path: &std::path::Path) -> Option<String> {
|
|
path.extension()
|
|
.and_then(|e| e.to_str())
|
|
.map(|e| e.to_ascii_lowercase())
|
|
}
|
|
|
|
pub fn is_audio_path(path: &std::path::Path) -> bool {
|
|
ext_of(path).is_some_and(|e| is_audio_ext(&e))
|
|
}
|
|
|
|
pub fn is_playable_path(path: &std::path::Path) -> bool {
|
|
ext_of(path).is_some_and(|e| is_playable_ext(&e))
|
|
}
|
|
|
|
/// Why this path will not play. `None` means "go ahead and try".
|
|
pub fn unplayable_path_reason(path: &std::path::Path) -> Option<&'static str> {
|
|
unplayable_reason(&ext_of(path)?)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::path::Path;
|
|
|
|
/// The bug the single table exists to prevent: the scanner indexed a
|
|
/// format the pruner did not recognise, so importing an AIFF album put it
|
|
/// in the library and the next startup silently deleted it.
|
|
#[test]
|
|
fn everything_indexed_survives_the_pruner() {
|
|
for ext in PLAYABLE.iter().chain(RECOGNISED.iter()) {
|
|
assert!(is_audio_ext(ext), "{ext} indexed but not recognised as audio");
|
|
}
|
|
// The extensions that were actually in the divergence.
|
|
for ext in ["aiff", "aif", "ape", "wv", "opus"] {
|
|
assert!(is_audio_ext(ext), "{ext} must be recognised");
|
|
}
|
|
}
|
|
|
|
/// A format cannot be both — every entry has one answer.
|
|
#[test]
|
|
fn no_extension_is_in_both_tables() {
|
|
for e in PLAYABLE {
|
|
assert!(!RECOGNISED.contains(e), "{e} is in both tables");
|
|
}
|
|
}
|
|
|
|
/// Recognised-but-not-playable must carry a reason, or the UI has nothing
|
|
/// to say when it refuses.
|
|
#[test]
|
|
fn every_unplayable_format_can_explain_itself() {
|
|
for e in RECOGNISED {
|
|
assert!(unplayable_reason(e).is_some(), "{e} has no reason");
|
|
}
|
|
for e in PLAYABLE {
|
|
assert!(unplayable_reason(e).is_none(), "{e} plays but claims it cannot");
|
|
}
|
|
}
|
|
|
|
/// WavPack is the dangerous one: the decoder does NOT fail on it, so only
|
|
/// an extension check keeps NaN out of the mixer.
|
|
#[test]
|
|
fn wavpack_is_refused_before_the_decoder_sees_it() {
|
|
assert!(!is_playable_ext("wv"));
|
|
assert!(is_audio_ext("wv"));
|
|
assert_eq!(
|
|
unplayable_path_reason(Path::new("C:\\m\\a.WV")),
|
|
Some("WavPack isn't supported yet"),
|
|
"case must not decide whether audio plays"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn case_and_paths() {
|
|
assert!(is_playable_ext("FLAC"));
|
|
assert!(is_playable_path(Path::new("C:\\m\\01 - Song.Mp3")));
|
|
assert!(!is_audio_path(Path::new("C:\\m\\cover.jpg")));
|
|
assert!(!is_audio_path(Path::new("C:\\m\\noext")));
|
|
}
|
|
}
|