Convert ffmpeg functionality to symphonia and fix some related tests

This commit is contained in:
NGnius (Graham) 2023-01-11 19:15:53 -05:00
parent 08aba11e39
commit 66e7333ff1
9 changed files with 457 additions and 339 deletions

227
Cargo.lock generated
View file

@ -52,6 +52,12 @@ version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
[[package]]
name = "arrayvec"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -70,6 +76,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]] [[package]]
<<<<<<< HEAD
name = "bindgen" name = "bindgen"
version = "0.59.2" version = "0.59.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
@ -89,6 +96,8 @@ dependencies = [
] ]
[[package]] [[package]]
=======
>>>>>>> 6fcb00c (Convert ffmpeg functionality to symphonia and fix some related tests)
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
@ -104,7 +113,6 @@ dependencies = [
"crossbeam", "crossbeam",
"dirs", "dirs",
"env_logger", "env_logger",
"ffmpeg-next",
"glob", "glob",
"indicatif", "indicatif",
"lazy_static 1.4.0", "lazy_static 1.4.0",
@ -126,7 +134,11 @@ dependencies = [
"serde_json", "serde_json",
"strum", "strum",
"strum_macros", "strum_macros",
<<<<<<< HEAD
"tempdir", "tempdir",
=======
"symphonia",
>>>>>>> 6fcb00c (Convert ffmpeg functionality to symphonia and fix some related tests)
"thiserror", "thiserror",
] ]
@ -168,6 +180,21 @@ dependencies = [
] ]
[[package]] [[package]]
<<<<<<< HEAD
=======
name = "byte-tools"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
[[package]]
name = "bytemuck"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f"
[[package]]
>>>>>>> 6fcb00c (Convert ffmpeg functionality to symphonia and fix some related tests)
name = "byteorder" name = "byteorder"
version = "1.4.3" version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
@ -203,15 +230,6 @@ dependencies = [
"jobserver", "jobserver",
] ]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -229,6 +247,7 @@ dependencies = [
] ]
[[package]] [[package]]
<<<<<<< HEAD
name = "clang-sys" name = "clang-sys"
version = "1.4.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
@ -240,6 +259,8 @@ dependencies = [
] ]
[[package]] [[package]]
=======
>>>>>>> 6fcb00c (Convert ffmpeg functionality to symphonia and fix some related tests)
name = "clap" name = "clap"
version = "2.34.0" version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
@ -430,6 +451,15 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "encoding_rs"
version = "0.8.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.8.4" version = "0.8.4"
@ -456,6 +486,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]] [[package]]
<<<<<<< HEAD
name = "ffmpeg-next" name = "ffmpeg-next"
version = "5.1.1" version = "5.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
@ -481,6 +512,8 @@ dependencies = [
] ]
[[package]] [[package]]
=======
>>>>>>> 6fcb00c (Convert ffmpeg functionality to symphonia and fix some related tests)
name = "fftw-src" name = "fftw-src"
version = "0.3.3" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
@ -679,12 +712,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.134" version = "0.2.134"
@ -692,6 +719,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
[[package]] [[package]]
<<<<<<< HEAD
name = "libloading" name = "libloading"
version = "0.7.3" version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
@ -704,6 +732,10 @@ dependencies = [
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
version = "0.24.2" version = "0.24.2"
=======
name = "log"
version = "0.4.16"
>>>>>>> 6fcb00c (Convert ffmpeg functionality to symphonia and fix some related tests)
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14"
dependencies = [ dependencies = [
@ -837,6 +869,7 @@ dependencies = [
] ]
[[package]] [[package]]
<<<<<<< HEAD
name = "nom" name = "nom"
version = "7.1.1" version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
@ -847,6 +880,8 @@ dependencies = [
] ]
[[package]] [[package]]
=======
>>>>>>> 6fcb00c (Convert ffmpeg functionality to symphonia and fix some related tests)
name = "num" name = "num"
version = "0.1.42" version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
@ -944,21 +979,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "output_vt100"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]] [[package]]
name = "pest" name = "pest"
version = "2.4.0" version = "2.4.0"
@ -1267,12 +1287,6 @@ dependencies = [
"smallvec", "smallvec",
] ]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "rustfft" name = "rustfft"
version = "5.1.1" version = "5.1.1"
@ -1394,6 +1408,139 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "symphonia"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7e5f38aa07e792f4eebb0faa93cee088ec82c48222dd332897aae1569d9a4b7"
dependencies = [
"lazy_static 1.4.0",
"symphonia-bundle-flac",
"symphonia-bundle-mp3",
"symphonia-codec-aac",
"symphonia-codec-pcm",
"symphonia-codec-vorbis",
"symphonia-core",
"symphonia-format-ogg",
"symphonia-format-wav",
"symphonia-metadata",
]
[[package]]
name = "symphonia-bundle-flac"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "116e5412f5fb4e5d07efd6628d50d6fcd7a61ebef43d98f5012f3cf763b25d02"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-bundle-mp3"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec4d97c4a61ece4651751dddb393ebecb7579169d9e758ae808fe507a5250790"
dependencies = [
"bitflags",
"lazy_static 1.4.0",
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "symphonia-codec-aac"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd3d7ab37eb9b7df16ddedd7adb7cc382afe708ff078e525a14dc9b05e57558f"
dependencies = [
"lazy_static 1.4.0",
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-codec-pcm"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba1d54738758993546107e3a4be2c1da827f2d4489fcffee0fa47867254e44c7"
dependencies = [
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-codec-vorbis"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a29ed6748078effb35a05064a451493a78038918981dc1a76bdf5a2752d441fa"
dependencies = [
"log",
"symphonia-core",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-core"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa135e97be0f4a666c31dfe5ef4c75435ba3d355fd6a73d2100aa79b14c104c9"
dependencies = [
"arrayvec",
"bitflags",
"bytemuck",
"lazy_static 1.4.0",
"log",
]
[[package]]
name = "symphonia-format-ogg"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7b2357288a79adfec532cfd86049696cfa5c58efeff83bd51687a528f18a519"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-format-wav"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d9fa5e5b420dea6763ba2547887eb1a02a142c676c5b02ed1b113a247101dad"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "symphonia-metadata"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5260599daba18d8fe905ca3eb3b42ba210529a6276886632412cc74984e79b1a"
dependencies = [
"encoding_rs",
"lazy_static 1.4.0",
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-utils-xiph"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a37026c6948ff842e0bf94b4008579cc71ab16ed0ff9ca70a331f60f4f1e1e9"
dependencies = [
"symphonia-core",
"symphonia-metadata",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.101" version = "1.0.101"
@ -1548,12 +1695,6 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" checksum = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "vec_map" name = "vec_map"
version = "0.8.2" version = "0.8.2"

View file

@ -1,5 +1,5 @@
[package] [package]
name = "bliss-audio" name = "bliss-audio-symphonia"
version = "0.6.5" version = "0.6.5"
authors = ["Polochon-street <polochonstreet@gmx.fr>"] authors = ["Polochon-street <polochonstreet@gmx.fr>"]
edition = "2018" edition = "2018"
@ -17,8 +17,6 @@ no-default-features = true
[features] [features]
default = ["bliss-audio-aubio-rs/static"] default = ["bliss-audio-aubio-rs/static"]
# Build ffmpeg instead of using the host's. # Build ffmpeg instead of using the host's.
build-ffmpeg = ["ffmpeg-next/build"]
ffmpeg-static = ["ffmpeg-next/static"]
# Use if you want to build python bindings with maturin. # Use if you want to build python bindings with maturin.
python-bindings = ["bliss-audio-aubio-rs/fftw3"] python-bindings = ["bliss-audio-aubio-rs/fftw3"]
# Enable the benchmarks with `cargo +nightly bench --features=bench` # Enable the benchmarks with `cargo +nightly bench --features=bench`
@ -41,7 +39,8 @@ lazy_static = "1.4.0"
rayon = "1.5.0" rayon = "1.5.0"
crossbeam = "0.8.0" crossbeam = "0.8.0"
noisy_float = "0.2.0" noisy_float = "0.2.0"
ffmpeg-next = "5.1.1" symphonia = { version = "0.5", features = ["mp3", "aac"]}
rubato = { version = "0.12" }
log = "0.4.14" log = "0.4.14"
env_logger = "0.8.3" env_logger = "0.8.3"
thiserror = "1.0.24" thiserror = "1.0.24"

View file

@ -1,4 +1,4 @@
use bliss_audio::Song; use bliss_audio_symphonia::Song;
use std::env; use std::env;
/** /**

View file

@ -1,4 +1,4 @@
use bliss_audio::Song; use bliss_audio_symphonia::Song;
use std::env; use std::env;
/** /**

View file

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use bliss_audio::playlist::{closest_to_first_song, dedup_playlist, euclidean_distance}; use bliss_audio_symphonia::playlist::{closest_to_first_song, dedup_playlist, euclidean_distance};
use bliss_audio::{analyze_paths, Song}; use bliss_audio_symphonia::{analyze_paths, Song};
use clap::{App, Arg}; use clap::{App, Arg};
use glob::glob; use glob::glob;
use std::env; use std::env;

View file

@ -201,7 +201,7 @@ mod tests {
let error = songs[0].to_owned().unwrap_err(); let error = songs[0].to_owned().unwrap_err();
assert_eq!( assert_eq!(
error, error,
BlissError::DecodingError("empty audio file associated to CUE sheet".to_string()) BlissError::DecodingError("while opening format: DecodeError(\"wav: chunk length exceeds parent (list) chunk length\").".to_string())
); );
} }
@ -330,8 +330,7 @@ mod tests {
..Default::default() ..Default::default()
}), }),
Err(BlissError::DecodingError(String::from( Err(BlissError::DecodingError(String::from(
"while opening format for file 'data/not-existing.wav': \ "while opening song: Os { code: 2, kind: NotFound, message: \"No such file or directory\" }.",
ffmpeg::Error(2: No such file or directory).",
))), ))),
]; ];
assert_eq!(expected, songs); assert_eq!(expected, songs);

View file

@ -91,7 +91,7 @@ use thiserror::Error;
pub use song::{Analysis, AnalysisIndex, Song, NUMBER_FEATURES}; pub use song::{Analysis, AnalysisIndex, Song, NUMBER_FEATURES};
const CHANNELS: u16 = 1; //const CHANNELS: u16 = 1;
const SAMPLE_RATE: u32 = 22050; const SAMPLE_RATE: u32 = 22050;
/// Stores the current version of bliss-rs' features. /// Stores the current version of bliss-rs' features.
/// It is bumped every time one or more feature is added, updated or removed, /// It is bumped every time one or more feature is added, updated or removed,
@ -291,27 +291,21 @@ mod tests {
false, false,
PathBuf::from("./data/testcue.cue"), PathBuf::from("./data/testcue.cue"),
Some(String::from( Some(String::from(
"error happened while decoding file while \ "error happened while decoding file while opening song: Os { code: 2, kind: NotFound, message: \"No such file or directory\" }.",
opening format for file './data/not-existing.wav': \
ffmpeg::Error(2: No such file or directory).",
)), )),
), ),
( (
false, false,
PathBuf::from("definitely-not-existing.foo"), PathBuf::from("definitely-not-existing.foo"),
Some(String::from( Some(String::from(
"error happened while decoding file while \ "error happened while decoding file while opening song: Os { code: 2, kind: NotFound, message: \"No such file or directory\" }.",
opening format for file 'definitely-not-existing\
.foo': ffmpeg::Error(2: No such file or directory).",
)), )),
), ),
( (
false, false,
PathBuf::from("not-existing.foo"), PathBuf::from("not-existing.foo"),
Some(String::from( Some(String::from(
"error happened while decoding file \ "error happened while decoding file while opening song: Os { code: 2, kind: NotFound, message: \"No such file or directory\" }.",
while opening format for file 'not-existing.foo': \
ffmpeg::Error(2: No such file or directory).",
)), )),
), ),
(true, PathBuf::from("./data/s16_mono_22_5kHz.flac"), None), (true, PathBuf::from("./data/s16_mono_22_5kHz.flac"), None),

View file

@ -8,7 +8,8 @@
//! a look at Library is instead recommended. //! a look at Library is instead recommended.
extern crate crossbeam; extern crate crossbeam;
extern crate ffmpeg_next as ffmpeg; //extern crate ffmpeg_next as ffmpeg;
extern crate symphonia;
extern crate ndarray; extern crate ndarray;
extern crate ndarray_npy; extern crate ndarray_npy;
@ -21,19 +22,18 @@ use crate::playlist::{closest_to_first_song, dedup_playlist, euclidean_distance,
use crate::temporal::BPMDesc; use crate::temporal::BPMDesc;
use crate::timbral::{SpectralDesc, ZeroCrossingRateDesc}; use crate::timbral::{SpectralDesc, ZeroCrossingRateDesc};
use crate::{BlissError, BlissResult, SAMPLE_RATE}; use crate::{BlissError, BlissResult, SAMPLE_RATE};
use crate::{CHANNELS, FEATURES_VERSION}; use crate::{FEATURES_VERSION};
use ::log::warn; use ::log::warn;
use core::ops::Index; use core::ops::Index;
use crossbeam::thread; use crossbeam::thread;
use ffmpeg_next::codec::threading::{Config, Type as ThreadingType}; use symphonia::core::formats::{SeekMode, SeekTo};
use ffmpeg_next::util::channel_layout::ChannelLayout; use symphonia::core::io::MediaSourceStream;
use ffmpeg_next::util::error::Error; use symphonia::core::probe::Hint;
use ffmpeg_next::util::error::EINVAL; use symphonia::core::errors::Error as SymphoniaError;
use ffmpeg_next::util::format::sample::{Sample, Type}; use symphonia::core::audio::{AudioBuffer, AudioBufferRef, Signal};
use ffmpeg_next::util::frame::audio::Audio; use symphonia::core::meta::{MetadataRevision, StandardTagKey};
use ffmpeg_next::util::log; use rubato::Resampler;
use ffmpeg_next::util::log::level::Level; use rubato::FftFixedIn as ResamplerImpl;
use ffmpeg_next::{media, util};
use ndarray::{arr1, Array1}; use ndarray::{arr1, Array1};
use std::convert::TryInto; use std::convert::TryInto;
use std::fmt; use std::fmt;
@ -133,7 +133,7 @@ pub const NUMBER_FEATURES: usize = AnalysisIndex::COUNT;
/// Only use it if you want to have an in-depth look of what is /// Only use it if you want to have an in-depth look of what is
/// happening behind the scene, or make a distance metric yourself. /// happening behind the scene, or make a distance metric yourself.
/// ///
/// Under the hood, it is just an array of f32 holding different numeric /// Under the hood, it is just an array of f32 holding different nsrcumeric
/// features. /// features.
/// ///
/// For more info on the different features, build the /// For more info on the different features, build the
@ -422,205 +422,123 @@ impl Song {
} }
pub(crate) fn decode(path: &Path) -> BlissResult<InternalSong> { pub(crate) fn decode(path: &Path) -> BlissResult<InternalSong> {
ffmpeg::init().map_err(|e| { let registry = symphonia::default::get_codecs();
BlissError::DecodingError(format!( let probe = symphonia::default::get_probe();
"ffmpeg init error while decoding file '{}': {:?}.",
path.display(),
e
))
})?;
log::set_level(Level::Quiet);
let mut song = InternalSong { let mut song = InternalSong {
path: path.into(), path: path.into(),
..Default::default() ..Default::default()
}; };
let mut ictx = ffmpeg::format::input(&path).map_err(|e| { let song_file = std::fs::File::open(path).map_err(|e| BlissError::DecodingError(format!("while opening song: {:?}.", e)))?;
BlissError::DecodingError(format!( let stream = MediaSourceStream::new(Box::new(song_file), Default::default());
"while opening format for file '{}': {:?}.", let mut hint = Hint::new();
path.display(), if let Some(ext) = path.extension().and_then(|x| x.to_str()) {
e hint.with_extension(ext);
)) }
})?; let mut probed = probe.format(&hint, stream, &Default::default(), &Default::default())
let (mut decoder, stream, expected_sample_number) = { .map_err(|e| BlissError::DecodingError(format!("while opening format: {:?}.", e)))?;
let input = ictx.streams().best(media::Type::Audio).ok_or_else(|| { let format = &mut probed.format;
BlissError::DecodingError(format!( /*let mut format = ffmpeg::format::input(&path)
"No audio stream found for file '{}'.", .map_err(|e| BlissError::DecodingError(format!("while opening format: {:?}.", e)))?;*/
path.display() let (mut codec, stream_id, expected_sample_number) = {
)) let track = format
})?; .tracks()
let mut context = ffmpeg::codec::context::Context::from_parameters(input.parameters()) .into_iter()
.map_err(|e| { .find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL)
BlissError::DecodingError(format!( .ok_or_else(|| BlissError::DecodingError(String::from("No valid audio stream found.")))?;
"Could not load the codec context for file '{}': {:?}", let track_id = track.id;
path.display(), /*let stream = format
e .streams()
)) .find(|s| s.codec().medium() == ffmpeg::media::Type::Audio)
})?; .ok_or_else(|| BlissError::DecodingError(String::from("No audio stream found.")))?;*/
context.set_threading(Config { /*stream.codec().set_threading(Config {
kind: ThreadingType::Frame, kind: ThreadingType::Frame,
count: 0, count: 0,
safe: true, safe: true,
}); });*/
let decoder = context.decoder().audio().map_err(|e| { let mut decoder = registry
BlissError::DecodingError(format!( .make(&track.codec_params, &Default::default())
"when finding decoder for file '{}': {:?}.", .map_err(|e| BlissError::DecodingError(format!("when finding codec: {:?}.", e)))?;
path.display(), /*let codec =
e stream.codec().decoder().audio().map_err(|e| {
)) BlissError::DecodingError(format!("when finding codec: {:?}.", e))
})?; })?;*/
let mut expected_sample_number: usize = 0;
// Add SAMPLE_RATE to have one second margin to avoid reallocating if // decode once to find sample number
// the duration is slightly more than estimated // previously, ffmpeg let us (roughly) calculate this
// TODO>1.0 another way to get the exact number of samples is to decode // symphonia does not make that easy so we just decode twice instead
// everything once, compute the real number of samples from that, while let Ok(packet) = format.next_packet() {
// allocate the array with that number, and decode again. Check if packet.track_id() == track_id {
// what's faster between reallocating, and just have one second if let Ok(buffer) = decoder.decode(&packet) {
// leeway. // errors will only be handled when actually decoding samples
let expected_sample_number = (SAMPLE_RATE as f32 * input.duration() as f32 expected_sample_number += sample_buffer_length(buffer);
/ input.time_base().denominator() as f32)
.ceil()
+ SAMPLE_RATE as f32;
(decoder, input.index(), expected_sample_number)
};
let sample_array: Vec<f32> = Vec::with_capacity(expected_sample_number as usize);
if let Some(title) = ictx.metadata().get("title") {
song.title = match title {
"" => None,
t => Some(t.to_string()),
};
};
if let Some(artist) = ictx.metadata().get("artist") {
song.artist = match artist {
"" => None,
a => Some(a.to_string()),
};
};
if let Some(album) = ictx.metadata().get("album") {
song.album = match album {
"" => None,
a => Some(a.to_string()),
};
};
if let Some(genre) = ictx.metadata().get("genre") {
song.genre = match genre {
"" => None,
g => Some(g.to_string()),
};
};
if let Some(track_number) = ictx.metadata().get("track") {
song.track_number = match track_number {
"" => None,
t => Some(t.to_string()),
};
};
if let Some(album_artist) = ictx.metadata().get("album_artist") {
song.album_artist = match album_artist {
"" => None,
t => Some(t.to_string()),
};
};
let in_channel_layout = {
if decoder.channel_layout() == ChannelLayout::empty() {
ChannelLayout::default(decoder.channels().into())
} else {
decoder.channel_layout()
} }
} // else ignore packet
}
// reset decoding to start of audio
format.seek(SeekMode::Accurate, SeekTo::Time {
time: symphonia::core::units::Time {
seconds: 0,
frac: 0.0,
},
track_id: Some(track_id),
}).map_err(|e| BlissError::DecodingError(format!("while seeking to start: {}", e)))?;
decoder.reset();
(decoder, track_id, expected_sample_number)
}; };
decoder.set_channel_layout(in_channel_layout); let sample_array: Vec<f32> = Vec::with_capacity(expected_sample_number);
// populate song metadata from file's info tags
if let Some(revision) = format.metadata().current() {
// audio format's built-in tags
song.read_tags(revision);
}
if let Some(metadata) = probed.metadata.get() {
if let Some(revision) = metadata.current() {
// audio wrapper's tags
song.read_tags(revision);
}
}
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let in_codec_format = decoder.format();
let in_codec_rate = decoder.rate();
let child = std_thread::spawn(move || { let child = std_thread::spawn(move || {
resample_frame( resample_buffer(
rx, rx,
in_codec_format,
in_channel_layout,
in_codec_rate,
sample_array, sample_array,
) )
}); });
for (s, packet) in ictx.packets() { loop {
if s.index() != stream { let packet = match format.next_packet() {
Err(SymphoniaError::IoError(_)) => break,
Ok(packet) => packet,
Err(e) => return Err(BlissError::DecodingError(format!("while reading packet: {}", e)))
};
if packet.track_id() != stream_id {
continue; continue;
} }
match decoder.send_packet(&packet) { match codec.decode(&packet) {
Ok(_) => (), Ok(decoded) => {
Err(Error::Other { errno: EINVAL }) => { tx.send(convert_sample_buffer(decoded)).map_err(|e| {
return Err(BlissError::DecodingError(format!(
"wrong codec opened for file '{}.",
path.display(),
)))
}
Err(Error::Eof) => {
warn!(
"Premature EOF reached while decoding file '{}'.",
path.display()
);
drop(tx);
song.sample_array = child.join().unwrap()?;
return Ok(song);
}
Err(e) => warn!("error while decoding file '{}': {}", path.display(), e),
};
loop {
let mut decoded = ffmpeg::frame::Audio::empty();
match decoder.receive_frame(&mut decoded) {
Ok(_) => {
tx.send(decoded).map_err(|e| {
BlissError::DecodingError(format!(
"while sending decoded frame to the resampling thread for file '{}': {:?}",
path.display(),
e,
))
})?;
}
Err(_) => break,
}
}
}
// Flush the stream
let packet = ffmpeg::codec::packet::Packet::empty();
match decoder.send_packet(&packet) {
Ok(_) => (),
Err(Error::Other { errno: EINVAL }) => {
return Err(BlissError::DecodingError(format!(
"wrong codec opened for file '{}'.",
path.display()
)))
}
Err(Error::Eof) => {
warn!(
"Premature EOF reached while decoding file '{}'.",
path.display()
);
drop(tx);
song.sample_array = child.join().unwrap()?;
return Ok(song);
}
Err(e) => warn!("error while decoding {}: {}", path.display(), e),
};
loop {
let mut decoded = ffmpeg::frame::Audio::empty();
match decoder.receive_frame(&mut decoded) {
Ok(_) => {
tx.send(decoded).map_err(|e| {
BlissError::DecodingError(format!( BlissError::DecodingError(format!(
"while sending decoded frame to the resampling thread for file '{}': {:?}", "while sending decoded frame to the resampling thread for file '{}': {:?}",
path.display(), path.display(),
e e
)) ))
})?; })?;
},
Err(SymphoniaError::Unsupported(s)) => {
return Err(BlissError::DecodingError(format!("unsupported: {}", s)))
} }
Err(_) => break, Err(SymphoniaError::IoError(e)) => {
} warn!("IO error occured while decoding: {}", e);
drop(tx);
song.sample_array = child.join().unwrap()?;
return Ok(song);
},
Err(e) => warn!("error while decoding {}: {}", path.display(), e),
};
} }
drop(tx); drop(tx);
song.sample_array = child.join().unwrap()?; song.sample_array = child.join().map_err(|_| BlissError::DecodingError(format!("resampler thread panic!")))??;
let duration_seconds = song.sample_array.len() as f32 / SAMPLE_RATE as f32; let duration_seconds = song.sample_array.len() as f32 / SAMPLE_RATE as f32;
song.duration = Duration::from_nanos((duration_seconds * 1e9_f32).round() as u64); song.duration = Duration::from_nanos((duration_seconds * 1e9_f32).round() as u64);
Ok(song) Ok(song)
@ -640,93 +558,163 @@ pub(crate) struct InternalSong {
pub sample_array: Vec<f32>, pub sample_array: Vec<f32>,
} }
fn resample_frame( impl InternalSong {
rx: Receiver<Audio>, #[inline]
in_codec_format: Sample, fn read_tags(&mut self, metadata: &MetadataRevision) {
in_channel_layout: ChannelLayout, for tag in metadata.tags() {
in_rate: u32, if let Some(key) = tag.std_key {
mut sample_array: Vec<f32>, match key {
) -> BlissResult<Vec<f32>> { StandardTagKey::Album => self.album = Some(tag.value.to_string()),
let mut resample_context = ffmpeg::software::resampling::context::Context::get( StandardTagKey::AlbumArtist => self.album_artist = Some(tag.value.to_string()),
in_codec_format, StandardTagKey::TrackTitle => self.title = Some(tag.value.to_string()),
in_channel_layout, StandardTagKey::Artist => self.artist = Some(tag.value.to_string()),
in_rate, StandardTagKey::Genre => self.genre = Some(tag.value.to_string()),
Sample::F32(Type::Packed), StandardTagKey::TrackNumber => self.track_number = Some(tag.value.to_string()),
ffmpeg::util::channel_layout::ChannelLayout::MONO, _ => {},
SAMPLE_RATE,
)
.map_err(|e| {
BlissError::DecodingError(format!(
"while trying to allocate resampling context: {:?}",
e
))
})?;
let mut resampled = ffmpeg::frame::Audio::empty();
let mut something_happened = false;
for decoded in rx.iter() {
if in_codec_format != decoded.format()
|| (in_channel_layout != decoded.channel_layout())
// If the decoded layout is empty, it means we forced the
// "in_channel_layout" to something default, not that
// the format is wrong.
&& (decoded.channel_layout() != ChannelLayout::empty())
|| in_rate != decoded.rate()
{
warn!("received decoded packet with wrong format; file might be corrupted.");
continue;
} }
something_happened = true;
resampled = ffmpeg::frame::Audio::empty();
resample_context
.run(&decoded, &mut resampled)
.map_err(|e| {
BlissError::DecodingError(format!("while trying to resample song: {:?}", e))
})?;
push_to_sample_array(&resampled, &mut sample_array);
} }
if !something_happened {
return Ok(sample_array);
} }
// TODO when ffmpeg-next will be active again: shouldn't we allocate
// `resampled` again?
loop {
match resample_context.flush(&mut resampled).map_err(|e| {
BlissError::DecodingError(format!("while trying to resample song: {:?}", e))
})? {
Some(_) => {
push_to_sample_array(&resampled, &mut sample_array);
} }
None => {
if resampled.samples() == 0 {
break;
}
push_to_sample_array(&resampled, &mut sample_array);
}
};
}
Ok(sample_array)
} }
fn push_to_sample_array(frame: &ffmpeg::frame::Audio, sample_array: &mut Vec<f32>) { fn convert_sample_buffer(buffer_in: AudioBufferRef) -> AudioBuffer<f32> {
if frame.samples() == 0 { match buffer_in {
return; AudioBufferRef::F32(buf) => buf.into_owned(),
AudioBufferRef::F64(buf) => {
let mut buffer_out = buf.make_equivalent::<f32>();
// symphonia expects frames to already exist (but since we know we're going to replace them right away, rendering doesn't have to do anything)
buffer_out.render(Some(buf.frames()), |_, _| Ok(())).unwrap_or(());
buf.convert(&mut buffer_out);
buffer_out
},
AudioBufferRef::S16(buf) => {
let mut buffer_out = buf.make_equivalent::<f32>();
// symphonia expects frames to already exist
buffer_out.render(Some(buf.frames()), |_, _| Ok(())).unwrap_or(());
buf.convert(&mut buffer_out);
buffer_out
},
AudioBufferRef::S24(buf) => {
let mut buffer_out = buf.make_equivalent::<f32>();
// symphonia expects frames to already exist
buffer_out.render(Some(buf.frames()), |_, _| Ok(())).unwrap_or(());
buf.convert(&mut buffer_out);
buffer_out
},
AudioBufferRef::S32(buf) => {
let mut buffer_out = buf.make_equivalent::<f32>();
// symphonia expects frames to already exist
buffer_out.render(Some(buf.frames()), |_, _| Ok(())).unwrap_or(());
buf.convert(&mut buffer_out);
buffer_out
},
AudioBufferRef::S8(buf) => {
let mut buffer_out = buf.make_equivalent::<f32>();
// symphonia expects frames to already exist
buffer_out.render(Some(buf.frames()), |_, _| Ok(())).unwrap_or(());
buf.convert(&mut buffer_out);
buffer_out
},
AudioBufferRef::U16(buf) => {
let mut buffer_out = buf.make_equivalent::<f32>();
// symphonia expects frames to already exist
buffer_out.render(Some(buf.frames()), |_, _| Ok(())).unwrap_or(());
buf.convert(&mut buffer_out);
buffer_out
},
AudioBufferRef::U24(buf) => {
let mut buffer_out = buf.make_equivalent::<f32>();
// symphonia expects frames to already exist
buffer_out.render(Some(buf.frames()), |_, _| Ok(())).unwrap_or(());
buf.convert(&mut buffer_out);
buffer_out
},
AudioBufferRef::U32(buf) => {
let mut buffer_out = buf.make_equivalent::<f32>();
// symphonia expects frames to already exist
buffer_out.render(Some(buf.frames()), |_, _| Ok(())).unwrap_or(());
buf.convert(&mut buffer_out);
buffer_out
},
AudioBufferRef::U8(buf) => {
let mut buffer_out = buf.make_equivalent::<f32>();
// symphonia expects frames to already exist
buffer_out.render(Some(buf.frames()), |_, _| Ok(())).unwrap_or(());
buf.convert(&mut buffer_out);
buffer_out
},
} }
// Account for the padding }
let actual_size = util::format::sample::Buffer::size(
Sample::F32(Type::Packed),
CHANNELS, fn sample_buffer_length(buffer_in: AudioBufferRef) -> usize {
frame.samples(), match buffer_in {
false, AudioBufferRef::F32(buf) => buf.frames(),
); AudioBufferRef::F64(buf) => buf.frames(),
let f32_frame: Vec<f32> = frame.data(0)[..actual_size] AudioBufferRef::S16(buf) => buf.frames(),
.chunks_exact(4) AudioBufferRef::S24(buf) => buf.frames(),
.map(|x| { AudioBufferRef::S32(buf) => buf.frames(),
let mut a: [u8; 4] = [0; 4]; AudioBufferRef::S8(buf) => buf.frames(),
a.copy_from_slice(x); AudioBufferRef::U16(buf) => buf.frames(),
f32::from_le_bytes(a) AudioBufferRef::U24(buf) => buf.frames(),
}) AudioBufferRef::U32(buf) => buf.frames(),
.collect(); AudioBufferRef::U8(buf) => buf.frames(),
sample_array.extend_from_slice(&f32_frame); }
}
fn resample_buffer(
rx: Receiver<AudioBuffer<f32>>,
mut sample_array: Vec<f32>,
) -> BlissResult<Vec<f32>> {
let mut resample_buffer = Vec::<f32>::new();
let mut resampler_cache = std::collections::HashMap::<(u32, usize), ResamplerImpl<f32>>::new();
let mut wave_out_buffer = [Vec::<f32>::new()];
for decoded in rx.iter() {
//dbg!(decoded.frames());
if decoded.planes().planes().is_empty() || decoded.frames() < 5 {
// buffers that are too small cause resampler to panic
// due to chunk rounding down to 0
continue;
}
resample_buffer.clear();
// do resampling ourselves since symphonia doesn't
let frame_count = decoded.frames();
let in_sample_rate = decoded.spec().rate;
let audio_planes = decoded.planes();
let planes = audio_planes.planes();
let planes_len = planes.len() as f32;
for i in 0..frame_count {
// average samples into Mono track
let mut avg_sample = 0.0;
for plane in planes {
avg_sample += plane[i] / planes_len;
}
resample_buffer.push(avg_sample);
}
// build resampler
let cache_key = (in_sample_rate, resample_buffer.len());
let resampler = if let Some(resampler) = resampler_cache.get_mut(&cache_key) {
resampler
} else {
let new_resampler = ResamplerImpl::new(
in_sample_rate as _,
SAMPLE_RATE as _,
resample_buffer.len(),
8,
1
).map_err(|e| BlissError::DecodingError(format!("Resampler init failure: {}", e)))?;
resampler_cache.insert(cache_key, new_resampler);
resampler_cache.get_mut(&cache_key).unwrap()
};
// resample
resampler.process_into_buffer(
&[resample_buffer.as_slice()],
&mut wave_out_buffer,
None,
).map_err(|e| BlissError::DecodingError(format!("Resampler processing error: {}", e)))?;
sample_array.append(&mut wave_out_buffer[0]);
}
Ok(sample_array)
} }
#[cfg(test)] #[cfg(test)]
@ -929,14 +917,12 @@ mod tests {
assert_eq!( assert_eq!(
Song::decode(Path::new("nonexistent")).unwrap_err(), Song::decode(Path::new("nonexistent")).unwrap_err(),
BlissError::DecodingError(String::from( BlissError::DecodingError(String::from(
"while opening format for file 'nonexistent': ffmpeg::Error(2: No such file or directory)." "while opening song: Os { code: 2, kind: NotFound, message: \"No such file or directory\" }."
)), )),
); );
assert_eq!( assert_eq!(
Song::decode(Path::new("data/picture.png")).unwrap_err(), Song::decode(Path::new("data/picture.png")).unwrap_err(),
BlissError::DecodingError(String::from( BlissError::DecodingError(String::from("while opening format: Unsupported(\"core (probe): no suitable format reader found\").")),
"No audio stream found for file 'data/picture.png'."
)),
); );
} }

View file

@ -3,7 +3,6 @@ use ndarray::{arr1, s, Array, Array1, Array2};
use rustfft::num_complex::Complex; use rustfft::num_complex::Complex;
use rustfft::num_traits::Zero; use rustfft::num_traits::Zero;
use rustfft::FftPlanner; use rustfft::FftPlanner;
extern crate ffmpeg_next as ffmpeg;
use log::warn; use log::warn;
use std::f32::consts::PI; use std::f32::consts::PI;