Some review comments

This commit is contained in:
Polochon-street 2021-06-13 14:07:17 +02:00
parent 78651c17c7
commit 33520acbc3
13 changed files with 179 additions and 130 deletions

View file

@ -1,5 +1,10 @@
# Changelog
## bliss 0.3.0
* Changed `Song.path` from `String` to `PathBuf`.
* Made `Song` metadata (artist, album, etc) `Option`s.
* Added a `BlissResult` error type.
## bliss 0.2.6
* Fixed an allocation bug in Song::decode that potentially caused segfaults.

2
Cargo.lock generated
View file

@ -75,7 +75,7 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "bliss-audio"
version = "0.2.6"
version = "0.3.0"
dependencies = [
"bliss-audio-aubio-rs",
"crossbeam",

View file

@ -1,6 +1,6 @@
[package]
name = "bliss-audio"
version = "0.2.6"
version = "0.3.0"
authors = ["Polochon-street <polochonstreet@gmx.fr>"]
edition = "2018"
license = "GPL-3.0-only"
@ -19,9 +19,12 @@ no-default-features = true
# https://github.com/zmwangx/rust-ffmpeg/pull/60
# or https://github.com/zmwangx/rust-ffmpeg/pull/62 is in
default = ["bliss-audio-aubio-rs/static", "build-ffmpeg"]
# Build ffmpeg instead of using the host's.
build-ffmpeg = ["ffmpeg-next/build"]
bench = []
# Use if you want to build python bindings with maturin.
python-bindings = ["bliss-audio-aubio-rs/fftw3"]
# Enable the benchmarks with `cargo +nightly bench --features=bench`
bench = []
[dependencies]
ripemd160 = "0.9.0"

BIN
data/no_tags.flac Normal file

Binary file not shown.

View file

@ -17,7 +17,7 @@ fn main() -> Result<(), String> {
let song2 = Song::new(&second_path).map_err(|x| x.to_string())?;
println!(
"d({}, {}) = {}",
"d({:?}, {:?}) = {}",
song1.path,
song2.path,
song1.distance(&song2)

View file

@ -7,7 +7,7 @@ extern crate noisy_float;
use crate::utils::stft;
use crate::utils::{hz_to_octs_inplace, Normalize};
use crate::BlissError;
use crate::{BlissError, BlissResult};
use ndarray::{arr1, arr2, concatenate, s, Array, Array1, Array2, Axis, Zip};
use ndarray_stats::interpolate::Midpoint;
use ndarray_stats::QuantileExt;
@ -51,7 +51,7 @@ impl ChromaDesc {
* Passing a full song here once instead of streaming smaller parts of the
* song will greatly improve accuracy.
*/
pub fn do_(&mut self, signal: &[f32]) -> Result<(), BlissError> {
pub fn do_(&mut self, signal: &[f32]) -> BlissResult<()> {
let mut stft = stft(signal, ChromaDesc::WINDOW_SIZE, 2205);
let tuning = estimate_tuning(
self.sample_rate as u32,
@ -155,7 +155,7 @@ fn chroma_filter(
n_fft: usize,
n_chroma: u32,
tuning: f64,
) -> Result<Array2<f64>, BlissError> {
) -> BlissResult<Array2<f64>> {
let ctroct = 5.0;
let octwidth = 2.;
let n_chroma_float = f64::from(n_chroma);
@ -226,7 +226,7 @@ fn pip_track(
sample_rate: u32,
spectrum: &Array2<f64>,
n_fft: usize,
) -> Result<(Vec<f64>, Vec<f64>), BlissError> {
) -> BlissResult<(Vec<f64>, Vec<f64>)> {
let sample_rate_float = f64::from(sample_rate);
let fmin = 150.0_f64;
let fmax = 4000.0_f64.min(sample_rate_float / 2.0);
@ -291,7 +291,7 @@ fn pitch_tuning(
frequencies: &mut Array1<f64>,
resolution: f64,
bins_per_octave: u32,
) -> Result<f64, BlissError> {
) -> BlissResult<f64> {
if frequencies.is_empty() {
return Ok(0.0);
}
@ -320,7 +320,7 @@ fn estimate_tuning(
n_fft: usize,
resolution: f64,
bins_per_octave: u32,
) -> Result<f64, BlissError> {
) -> BlissResult<f64> {
let (pitch, mag) = pip_track(sample_rate, &spectrum, n_fft)?;
let (filtered_pitch, filtered_mag): (Vec<N64>, Vec<N64>) = pitch
@ -372,6 +372,7 @@ mod test {
use ndarray::{arr1, arr2, Array2};
use ndarray_npy::ReadNpyExt;
use std::fs::File;
use std::path::Path;
#[test]
fn test_chroma_interval_features() {
@ -437,7 +438,7 @@ mod test {
#[test]
fn test_chroma_desc() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap();
let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
chroma_desc.do_(&song.sample_array).unwrap();
let expected_values = vec![
@ -459,7 +460,7 @@ mod test {
#[test]
fn test_chroma_stft_decode() {
let signal = Song::decode("data/s16_mono_22_5kHz.flac")
let signal = Song::decode(Path::new("data/s16_mono_22_5kHz.flac"))
.unwrap()
.sample_array;
let mut stft = stft(&signal, 8192, 2205);
@ -487,7 +488,7 @@ mod test {
#[test]
fn test_estimate_tuning_decode() {
let signal = Song::decode("data/s16_mono_22_5kHz.flac")
let signal = Song::decode(Path::new("data/s16_mono_22_5kHz.flac"))
.unwrap()
.sample_array;
let stft = stft(&signal, 8192, 2205);
@ -556,6 +557,7 @@ mod bench {
use ndarray_npy::ReadNpyExt;
use std::fs::File;
use test::Bencher;
use std::path::Path;
#[bench]
fn bench_estimate_tuning(b: &mut Bencher) {
@ -603,7 +605,7 @@ mod bench {
#[bench]
fn bench_chroma_desc(b: &mut Bencher) {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap();
let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
let signal = song.sample_array;
b.iter(|| {
@ -614,7 +616,7 @@ mod bench {
#[bench]
fn bench_chroma_stft(b: &mut Bencher) {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap();
let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
let signal = song.sample_array;
b.iter(|| {
@ -625,7 +627,7 @@ mod bench {
#[bench]
fn bench_chroma_stft_decode(b: &mut Bencher) {
let signal = Song::decode("data/s16_mono_22_5kHz.flac")
let signal = Song::decode(Path::new("data/s16_mono_22_5kHz.flac"))
.unwrap()
.sample_array;
let mut stft = stft(&signal, 8192, 2205);

View file

@ -24,9 +24,9 @@
//!
//! ## Analyze & compute the distance between two songs
//! ```no_run
//! use bliss_audio::{BlissError, Song};
//! use bliss_audio::{BlissResult, Song};
//!
//! fn main() -> Result<(), BlissError> {
//! fn main() -> BlissResult<()> {
//! let song1 = Song::new("/path/to/song1")?;
//! let song2 = Song::new("/path/to/song2")?;
//!
@ -37,15 +37,15 @@
//!
//! ### Make a playlist from a song
//! ```no_run
//! use bliss_audio::{BlissError, Song};
//! use bliss_audio::{BlissResult, Song};
//! use noisy_float::prelude::n32;
//!
//! fn main() -> Result<(), BlissError> {
//! fn main() -> BlissResult<()> {
//! let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"];
//! let mut songs: Vec<Song> = paths
//! .iter()
//! .map(|path| Song::new(path))
//! .collect::<Result<Vec<Song>, BlissError>>()?;
//! .collect::<BlissResult<Vec<Song>>>()?;
//!
//! // Assuming there is a first song
//! let first_song = songs.first().unwrap().to_owned();
@ -55,8 +55,8 @@
//! "Playlist is: {:?}",
//! songs
//! .iter()
//! .map(|song| &song.path)
//! .collect::<Vec<&String>>()
//! .map(|song| song.path.to_string_lossy().to_string())
//! .collect::<Vec<String>>()
//! );
//! Ok(())
//! }
@ -100,13 +100,16 @@ pub enum BlissError {
ProviderError(String),
}
/// bliss error type
pub type BlissResult<T> = Result<T, BlissError>;
/// Simple function to bulk analyze a set of songs represented by their
/// absolute paths.
///
/// When making an extension for an audio player, prefer
/// implementing the `Library` trait.
#[doc(hidden)]
pub fn bulk_analyse(paths: Vec<String>) -> Vec<Result<Song, BlissError>> {
pub fn bulk_analyse(paths: Vec<String>) -> Vec<BlissResult<Song>> {
let mut songs = Vec::with_capacity(paths.len());
let num_cpus = num_cpus::get();
@ -175,7 +178,7 @@ mod tests {
let mut analysed_songs: Vec<String> = results
.iter()
.filter_map(|x| x.as_ref().ok().map(|x| x.path.to_string()))
.filter_map(|x| x.as_ref().ok().map(|x| x.path.to_str().unwrap().to_string()))
.collect();
analysed_songs.sort_by(|a, b| a.cmp(b));

View file

@ -1,6 +1,6 @@
//! Module containing the Library trait, useful to get started to implement
//! a plug-in for an audio player.
use crate::{BlissError, Song};
use crate::{BlissError, BlissResult, Song};
use log::{debug, error, info};
use noisy_float::prelude::*;
use std::sync::mpsc;
@ -11,18 +11,18 @@ use std::thread;
pub trait Library {
/// Return the absolute path of all the songs in an
/// audio player's music library.
fn get_songs_paths(&self) -> Result<Vec<String>, BlissError>;
fn get_songs_paths(&self) -> BlissResult<Vec<String>>;
/// Store an analyzed Song object in some (cold) storage, e.g.
/// a database, a file...
fn store_song(&mut self, song: &Song) -> Result<(), BlissError>;
fn store_song(&mut self, song: &Song) -> BlissResult<()>;
/// Log and / or store that an error happened while trying to decode and
/// analyze a song.
fn store_error_song(&mut self, song_path: String, error: BlissError) -> Result<(), BlissError>;
fn store_error_song(&mut self, song_path: String, error: BlissError) -> BlissResult<()>;
/// Retrieve a list of all the stored Songs.
///
/// This should work only after having run `analyze_library` at least
/// once.
fn get_stored_songs(&self) -> Result<Vec<Song>, BlissError>;
fn get_stored_songs(&self) -> BlissResult<Vec<Song>>;
/// Return a list of songs that are similar to ``first_song``.
///
@ -41,7 +41,7 @@ pub trait Library {
&self,
first_song: Song,
playlist_length: usize,
) -> Result<Vec<Song>, BlissError> {
) -> BlissResult<Vec<Song>> {
let analysis_current_song = first_song.analysis;
let mut songs = self.get_stored_songs()?;
songs.sort_by_cached_key(|song| n32(analysis_current_song.distance(&song.analysis)));
@ -59,7 +59,7 @@ pub trait Library {
///
/// Note: this is mostly useful for updating a song library. For the first
/// run, you probably want to use `analyze_library`.
fn analyze_paths(&mut self, paths: Vec<String>) -> Result<(), BlissError> {
fn analyze_paths(&mut self, paths: Vec<String>) -> BlissResult<()> {
if paths.is_empty() {
return Ok(());
}
@ -67,8 +67,8 @@ pub trait Library {
#[allow(clippy::type_complexity)]
let (tx, rx): (
Sender<(String, Result<Song, BlissError>)>,
Receiver<(String, Result<Song, BlissError>)>,
Sender<(String, BlissResult<Song>)>,
Receiver<(String, BlissResult<Song>)>,
) = mpsc::channel();
let mut handles = Vec::new();
let mut chunk_length = paths.len() / num_cpus;
@ -96,8 +96,8 @@ pub trait Library {
match song {
Ok(song) => {
self.store_song(&song)
.unwrap_or_else(|_| error!("Error while storing song '{}'", song.path));
info!("Analyzed and stored song '{}' successfully.", song.path)
.unwrap_or_else(|_| error!("Error while storing song '{}'", song.path.display()));
info!("Analyzed and stored song '{}' successfully.", song.path.display())
}
Err(e) => {
self.store_error_song(path.to_string(), e.to_owned())
@ -122,7 +122,7 @@ pub trait Library {
/// Analyzes a song library, using `get_songs_paths`, `store_song` and
/// `store_error_song` implementations.
fn analyze_library(&mut self) -> Result<(), BlissError> {
fn analyze_library(&mut self) -> BlissResult<()> {
let paths = self
.get_songs_paths()
.map_err(|e| BlissError::ProviderError(e.to_string()))?;
@ -135,6 +135,7 @@ pub trait Library {
mod test {
use super::*;
use crate::song::Analysis;
use std::path::Path;
#[derive(Default)]
struct TestLibrary {
@ -143,7 +144,7 @@ mod test {
}
impl Library for TestLibrary {
fn get_songs_paths(&self) -> Result<Vec<String>, BlissError> {
fn get_songs_paths(&self) -> BlissResult<Vec<String>> {
Ok(vec![
String::from("./data/white_noise.flac"),
String::from("./data/s16_mono_22_5kHz.flac"),
@ -152,7 +153,7 @@ mod test {
])
}
fn store_song(&mut self, song: &Song) -> Result<(), BlissError> {
fn store_song(&mut self, song: &Song) -> BlissResult<()> {
self.internal_storage.push(song.to_owned());
Ok(())
}
@ -161,12 +162,12 @@ mod test {
&mut self,
song_path: String,
error: BlissError,
) -> Result<(), BlissError> {
) -> BlissResult<()> {
self.failed_files.push((song_path, error.to_string()));
Ok(())
}
fn get_stored_songs(&self) -> Result<Vec<Song>, BlissError> {
fn get_stored_songs(&self) -> BlissResult<Vec<Song>> {
Ok(self.internal_storage.to_owned())
}
}
@ -175,23 +176,23 @@ mod test {
struct FailingLibrary;
impl Library for FailingLibrary {
fn get_songs_paths(&self) -> Result<Vec<String>, BlissError> {
fn get_songs_paths(&self) -> BlissResult<Vec<String>> {
Err(BlissError::ProviderError(String::from(
"Could not get songs path",
)))
}
fn store_song(&mut self, _: &Song) -> Result<(), BlissError> {
fn store_song(&mut self, _: &Song) -> BlissResult<()> {
Ok(())
}
fn get_stored_songs(&self) -> Result<Vec<Song>, BlissError> {
fn get_stored_songs(&self) -> BlissResult<Vec<Song>> {
Err(BlissError::ProviderError(String::from(
"Could not get stored songs",
)))
}
fn store_error_song(&mut self, _: String, _: BlissError) -> Result<(), BlissError> {
fn store_error_song(&mut self, _: String, _: BlissError) -> BlissResult<()> {
Ok(())
}
}
@ -200,7 +201,7 @@ mod test {
struct FailingStorage;
impl Library for FailingStorage {
fn get_songs_paths(&self) -> Result<Vec<String>, BlissError> {
fn get_songs_paths(&self) -> BlissResult<Vec<String>> {
Ok(vec![
String::from("./data/white_noise.flac"),
String::from("./data/s16_mono_22_5kHz.flac"),
@ -209,14 +210,14 @@ mod test {
])
}
fn store_song(&mut self, song: &Song) -> Result<(), BlissError> {
fn store_song(&mut self, song: &Song) -> BlissResult<()> {
Err(BlissError::ProviderError(format!(
"Could not store song {}",
song.path
song.path.display()
)))
}
fn get_stored_songs(&self) -> Result<Vec<Song>, BlissError> {
fn get_stored_songs(&self) -> BlissResult<Vec<Song>> {
Ok(vec![])
}
@ -224,7 +225,7 @@ mod test {
&mut self,
song_path: String,
error: BlissError,
) -> Result<(), BlissError> {
) -> BlissResult<()> {
Err(BlissError::ProviderError(format!(
"Could not store errored song: {}, with error: {}",
song_path, error
@ -247,7 +248,7 @@ mod test {
fn test_playlist_from_song_fail() {
let test_library = FailingLibrary {};
let song = Song {
path: String::from("path-to-first"),
path: Path::new("path-to-first").to_path_buf(),
analysis: Analysis::new([0.; 20]),
..Default::default()
};
@ -294,7 +295,7 @@ mod test {
let mut songs = test_library
.internal_storage
.iter()
.map(|x| x.path.to_owned())
.map(|x| x.path.to_str().unwrap().to_string())
.collect::<Vec<String>>();
songs.sort();
@ -311,25 +312,25 @@ mod test {
fn test_playlist_from_song() {
let mut test_library = TestLibrary::default();
let first_song = Song {
path: String::from("path-to-first"),
path: Path::new("path-to-first").to_path_buf(),
analysis: Analysis::new([0.; 20]),
..Default::default()
};
let second_song = Song {
path: String::from("path-to-second"),
path: Path::new("path-to-second").to_path_buf(),
analysis: Analysis::new([0.1; 20]),
..Default::default()
};
let third_song = Song {
path: String::from("path-to-third"),
path: Path::new("path-to-third").to_path_buf(),
analysis: Analysis::new([10.; 20]),
..Default::default()
};
let fourth_song = Song {
path: String::from("path-to-fourth"),
path: Path::new("path-to-fourth").to_path_buf(),
analysis: Analysis::new([20.; 20]),
..Default::default()
};
@ -350,19 +351,19 @@ mod test {
fn test_playlist_from_song_too_little_songs() {
let mut test_library = TestLibrary::default();
let first_song = Song {
path: String::from("path-to-first"),
path: Path::new("path-to-first").to_path_buf(),
analysis: Analysis::new([0.; 20]),
..Default::default()
};
let second_song = Song {
path: String::from("path-to-second"),
path: Path::new("path-to-second").to_path_buf(),
analysis: Analysis::new([0.1; 20]),
..Default::default()
};
let third_song = Song {
path: String::from("path-to-third"),
path: Path::new("path-to-third").to_path_buf(),
analysis: Analysis::new([10.; 20]),
..Default::default()
};

View file

@ -64,10 +64,11 @@ impl Normalize for LoudnessDesc {
mod tests {
use super::*;
use crate::Song;
use std::path::Path;
#[test]
fn test_loudness() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap();
let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut loudness_desc = LoudnessDesc::default();
for chunk in song.sample_array.chunks_exact(LoudnessDesc::WINDOW_SIZE) {
loudness_desc.do_(&chunk);

View file

@ -17,7 +17,7 @@ use crate::chroma::ChromaDesc;
use crate::misc::LoudnessDesc;
use crate::temporal::BPMDesc;
use crate::timbral::{SpectralDesc, ZeroCrossingRateDesc};
use crate::{BlissError, SAMPLE_RATE};
use crate::{BlissError, BlissResult, SAMPLE_RATE};
use ::log::warn;
use core::ops::Index;
use crossbeam::thread;
@ -36,6 +36,8 @@ use std::convert::TryInto;
use std::fmt;
use std::sync::mpsc;
use std::sync::mpsc::Receiver;
use std::path::Path;
use std::path::PathBuf;
use std::thread as std_thread;
use strum::{EnumCount, IntoEnumIterator};
use strum_macros::{EnumCount, EnumIter};
@ -46,17 +48,17 @@ use strum_macros::{EnumCount, EnumIter};
/// other metadata (artist, genre...)
pub struct Song {
/// Song's provided file path
pub path: String,
/// Song's artist, read from the metadata (`""` if empty)
pub artist: String,
/// Song's title, read from the metadata (`""` if empty)
pub title: String,
/// Song's album name, read from the metadata (`""` if empty)
pub album: String,
/// Song's tracked number, read from the metadata (`""` if empty)
pub track_number: String,
pub path: PathBuf,
/// Song's artist, read from the metadata
pub artist: Option<String>,
/// Song's title, read from the metadata
pub title: Option<String>,
/// Song's album name, read from the metadata
pub album: Option<String>,
/// Song's tracked number, read from the metadata
pub track_number: Option<String>,
/// Song's genre, read from the metadata (`""` if empty)
pub genre: String,
pub genre: Option<String>,
/// bliss analysis results
pub analysis: Analysis,
}
@ -66,9 +68,9 @@ pub struct Song {
///
/// * Example:
/// ```no_run
/// use bliss_audio::{AnalysisIndex, BlissError, Song};
/// use bliss_audio::{AnalysisIndex, BlissResult, Song};
///
/// fn main() -> Result<(), BlissError> {
/// fn main() -> BlissResult<()> {
/// let song = Song::new("path/to/song")?;
/// println!("{}", song.analysis[AnalysisIndex::Tempo]);
/// Ok(())
@ -217,8 +219,8 @@ impl Song {
/// The error type returned should give a hint as to whether it was a
/// decoding ([DecodingError](BlissError::DecodingError)) or an analysis
/// ([AnalysisError](BlissError::AnalysisError)) error.
pub fn new(path: &str) -> Result<Self, BlissError> {
let raw_song = Song::decode(&path)?;
pub fn new<P: AsRef<Path>>(path: P) -> BlissResult<Self> {
let raw_song = Song::decode(path.as_ref())?;
Ok(Song {
path: raw_song.path,
@ -242,7 +244,7 @@ impl Song {
* Useful in the rare cases where the full song is not
* completely available.
**/
fn analyse(sample_array: Vec<f32>) -> Result<Analysis, BlissError> {
fn analyse(sample_array: Vec<f32>) -> BlissResult<Analysis> {
let largest_window = vec![
BPMDesc::WINDOW_SIZE,
ChromaDesc::WINDOW_SIZE,
@ -259,7 +261,7 @@ impl Song {
}
thread::scope(|s| {
let child_tempo: thread::ScopedJoinHandle<'_, Result<f32, BlissError>> =
let child_tempo: thread::ScopedJoinHandle<'_, BlissResult<f32>> =
s.spawn(|_| {
let mut tempo_desc = BPMDesc::new(SAMPLE_RATE)?;
let windows = sample_array
@ -272,7 +274,7 @@ impl Song {
Ok(tempo_desc.get_value())
});
let child_chroma: thread::ScopedJoinHandle<'_, Result<Vec<f32>, BlissError>> =
let child_chroma: thread::ScopedJoinHandle<'_, BlissResult<Vec<f32>>> =
s.spawn(|_| {
let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
chroma_desc.do_(&sample_array)?;
@ -282,7 +284,7 @@ impl Song {
#[allow(clippy::type_complexity)]
let child_timbral: thread::ScopedJoinHandle<
'_,
Result<(Vec<f32>, Vec<f32>, Vec<f32>), BlissError>,
BlissResult<(Vec<f32>, Vec<f32>, Vec<f32>)>,
> = s.spawn(|_| {
let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE)?;
let windows = sample_array
@ -297,13 +299,13 @@ impl Song {
Ok((centroid, rolloff, flatness))
});
let child_zcr: thread::ScopedJoinHandle<'_, Result<f32, BlissError>> = s.spawn(|_| {
let child_zcr: thread::ScopedJoinHandle<'_, BlissResult<f32>> = s.spawn(|_| {
let mut zcr_desc = ZeroCrossingRateDesc::default();
zcr_desc.do_(&sample_array);
Ok(zcr_desc.get_value())
});
let child_loudness: thread::ScopedJoinHandle<'_, Result<Vec<f32>, BlissError>> = s
let child_loudness: thread::ScopedJoinHandle<'_, BlissResult<Vec<f32>>> = s
.spawn(|_| {
let mut loudness_desc = LoudnessDesc::default();
let windows = sample_array.chunks(LoudnessDesc::WINDOW_SIZE);
@ -339,12 +341,12 @@ impl Song {
.unwrap()
}
pub(crate) fn decode(path: &str) -> Result<InternalSong, BlissError> {
pub(crate) fn decode(path: &Path) -> BlissResult<InternalSong> {
ffmpeg::init()
.map_err(|e| BlissError::DecodingError(format!("ffmpeg init error: {:?}.", e)))?;
log::set_level(Level::Quiet);
let mut song = InternalSong {
path: path.to_string(),
path: path.into(),
..Default::default()
};
let mut format = ffmpeg::format::input(&path)
@ -378,19 +380,35 @@ impl Song {
};
let sample_array: Vec<f32> = Vec::with_capacity(expected_sample_number as usize);
if let Some(title) = format.metadata().get("title") {
song.title = title.to_string();
song.title = match title {
"" => None,
t => Some(t.to_string()),
};
};
if let Some(artist) = format.metadata().get("artist") {
song.artist = artist.to_string();
song.artist = match artist {
"" => None,
a => Some(a.to_string()),
};
};
if let Some(album) = format.metadata().get("album") {
song.album = album.to_string();
song.album = match album {
"" => None,
a => Some(a.to_string()),
};
};
if let Some(genre) = format.metadata().get("genre") {
song.genre = genre.to_string();
song.genre = match genre {
"" => None,
g => Some(g.to_string()),
};
};
if let Some(track_number) = format.metadata().get("track") {
song.track_number = track_number.to_string();
song.track_number = match track_number {
"" => None,
t => Some(t.to_string()),
};
};
let in_channel_layout = {
if codec.channel_layout() == ChannelLayout::empty() {
@ -494,12 +512,12 @@ impl Song {
#[derive(Default, Debug)]
pub(crate) struct InternalSong {
pub path: String,
pub artist: String,
pub title: String,
pub album: String,
pub track_number: String,
pub genre: String,
pub path: PathBuf,
pub artist: Option<String>,
pub title: Option<String>,
pub album: Option<String>,
pub track_number: Option<String>,
pub genre: Option<String>,
pub sample_array: Vec<f32>,
}
@ -507,7 +525,7 @@ fn resample_frame(
rx: Receiver<Audio>,
mut resample_context: Context,
mut sample_array: Vec<f32>,
) -> Result<Vec<f32>, BlissError> {
) -> BlissResult<Vec<f32>> {
let mut resampled = ffmpeg::frame::Audio::empty();
for decoded in rx.iter() {
resampled = ffmpeg::frame::Audio::empty();
@ -564,6 +582,7 @@ fn push_to_sample_array(frame: &ffmpeg::frame::Audio, sample_array: &mut Vec<f32
mod tests {
use super::*;
use ripemd160::{Digest, Ripemd160};
use std::path::Path;
#[test]
fn test_analysis_too_small() {
@ -582,7 +601,7 @@ mod tests {
#[test]
fn test_analyse() {
let song = Song::new("data/s16_mono_22_5kHz.flac").unwrap();
let song = Song::new(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let expected_analysis = vec![
0.3846389,
-0.849141,
@ -610,7 +629,7 @@ mod tests {
}
}
fn _test_decode(path: &str, expected_hash: &[u8]) {
fn _test_decode(path: &Path, expected_hash: &[u8]) {
let song = Song::decode(path).unwrap();
let mut hasher = Ripemd160::new();
for sample in song.sample_array.iter() {
@ -622,17 +641,27 @@ mod tests {
#[test]
fn test_tags() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap();
assert_eq!(song.artist, "David TMX");
assert_eq!(song.title, "Renaissance");
assert_eq!(song.album, "Renaissance");
assert_eq!(song.track_number, "02");
assert_eq!(song.genre, "Pop");
let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
assert_eq!(song.artist, Some(String::from("David TMX")));
assert_eq!(song.title, Some(String::from("Renaissance")));
assert_eq!(song.album, Some(String::from("Renaissance")));
assert_eq!(song.track_number, Some(String::from("02")));
assert_eq!(song.genre, Some(String::from("Pop")));
}
#[test]
fn test_empty_tags() {
let song = Song::decode(Path::new("data/no_tags.flac")).unwrap();
assert_eq!(song.artist, None);
assert_eq!(song.title, None);
assert_eq!(song.album, None);
assert_eq!(song.track_number, None);
assert_eq!(song.genre, None);
}
#[test]
fn test_resample_multi() {
let path = String::from("data/s32_stereo_44_1_kHz.flac");
let path = Path::new("data/s32_stereo_44_1_kHz.flac");
let expected_hash = [
0xc5, 0xf8, 0x23, 0xce, 0x63, 0x2c, 0xf4, 0xa0, 0x72, 0x66, 0xbb, 0x49, 0xad, 0x84,
0xb6, 0xea, 0x48, 0x48, 0x9c, 0x50,
@ -642,7 +671,7 @@ mod tests {
#[test]
fn test_resample_stereo() {
let path = String::from("data/s16_stereo_22_5kHz.flac");
let path = Path::new("data/s16_stereo_22_5kHz.flac");
let expected_hash = [
0x24, 0xed, 0x45, 0x58, 0x06, 0xbf, 0xfb, 0x05, 0x57, 0x5f, 0xdc, 0x4d, 0xb4, 0x9b,
0xa5, 0x2b, 0x05, 0x56, 0x10, 0x4f,
@ -652,7 +681,7 @@ mod tests {
#[test]
fn test_decode_mono() {
let path = String::from("data/s16_mono_22_5kHz.flac");
let path = Path::new("data/s16_mono_22_5kHz.flac");
// Obtained through
// ffmpeg -i data/s16_mono_22_5kHz.flac -ar 22050 -ac 1 -c:a pcm_f32le
// -f hash -hash ripemd160 -
@ -665,7 +694,7 @@ mod tests {
#[test]
fn test_decode_mp3() {
let path = String::from("data/s32_stereo_44_1_kHz.mp3");
let path = Path::new("data/s32_stereo_44_1_kHz.mp3");
// Obtained through
// ffmpeg -i data/s16_mono_22_5kHz.mp3 -ar 22050 -ac 1 -c:a pcm_f32le
// -f hash -hash ripemd160 -
@ -678,13 +707,13 @@ mod tests {
#[test]
fn test_dont_panic_no_channel_layout() {
let path = String::from("data/no_channel.wav");
let path = Path::new("data/no_channel.wav");
Song::decode(&path).unwrap();
}
#[test]
fn test_decode_right_capacity_vec() {
let path = String::from("data/s16_mono_22_5kHz.flac");
let path = Path::new("data/s16_mono_22_5kHz.flac");
let song = Song::decode(&path).unwrap();
let sample_array = song.sample_array;
assert_eq!(
@ -692,7 +721,7 @@ mod tests {
sample_array.capacity()
);
let path = String::from("data/s32_stereo_44_1_kHz.flac");
let path = Path::new("data/s32_stereo_44_1_kHz.flac");
let song = Song::decode(&path).unwrap();
let sample_array = song.sample_array;
assert_eq!(
@ -700,7 +729,7 @@ mod tests {
sample_array.capacity()
);
let path = String::from("data/capacity_fix.ogg");
let path = Path::new("data/capacity_fix.ogg");
let song = Song::decode(&path).unwrap();
let sample_array = song.sample_array;
assert!(sample_array.len() as f32 / sample_array.capacity() as f32 > 0.90);
@ -738,13 +767,13 @@ mod tests {
#[test]
fn test_decode_errors() {
assert_eq!(
Song::decode("nonexistent").unwrap_err(),
Song::decode(Path::new("nonexistent")).unwrap_err(),
BlissError::DecodingError(String::from(
"while opening format: ffmpeg::Error(2: No such file or directory)."
)),
);
assert_eq!(
Song::decode("data/picture.png").unwrap_err(),
Song::decode(Path::new("data/picture.png")).unwrap_err(),
BlissError::DecodingError(String::from("No audio stream found.")),
);
}
@ -782,10 +811,11 @@ mod bench {
extern crate test;
use crate::Song;
use test::Bencher;
use std::path::Path;
#[bench]
fn bench_resample_multi(b: &mut Bencher) {
let path = String::from("./data/s32_stereo_44_1_kHz.flac");
let path = Path::new("./data/s32_stereo_44_1_kHz.flac");
b.iter(|| {
Song::decode(&path).unwrap();
});

View file

@ -4,7 +4,7 @@
//! of a given Song.
use crate::utils::Normalize;
use crate::BlissError;
use crate::{BlissError, BlissResult};
use bliss_audio_aubio_rs::{OnsetMode, Tempo};
use log::warn;
use ndarray::arr1;
@ -39,7 +39,7 @@ impl BPMDesc {
pub const WINDOW_SIZE: usize = 512;
pub const HOP_SIZE: usize = BPMDesc::WINDOW_SIZE / 2;
pub fn new(sample_rate: u32) -> Result<Self, BlissError> {
pub fn new(sample_rate: u32) -> BlissResult<Self> {
Ok(BPMDesc {
aubio_obj: Tempo::new(
OnsetMode::SpecFlux,
@ -57,7 +57,7 @@ impl BPMDesc {
})
}
pub fn do_(&mut self, chunk: &[f32]) -> Result<(), BlissError> {
pub fn do_(&mut self, chunk: &[f32]) -> BlissResult<()> {
let result = self.aubio_obj.do_result(chunk).map_err(|e| {
BlissError::AnalysisError(format!(
"aubio error while computing tempo {}",
@ -101,10 +101,11 @@ impl Normalize for BPMDesc {
mod tests {
use super::*;
use crate::{Song, SAMPLE_RATE};
use std::path::Path;
#[test]
fn test_tempo_real() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap();
let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut tempo_desc = BPMDesc::new(SAMPLE_RATE).unwrap();
for chunk in song.sample_array.chunks_exact(BPMDesc::HOP_SIZE) {
tempo_desc.do_(&chunk).unwrap();

View file

@ -9,7 +9,7 @@ use bliss_audio_aubio_rs::{bin_to_freq, PVoc, SpecDesc, SpecShape};
use ndarray::{arr1, Axis};
use super::utils::{geometric_mean, mean, number_crossings, Normalize};
use crate::{BlissError, SAMPLE_RATE};
use crate::{BlissError, BlissResult, SAMPLE_RATE};
/**
* General object holding all the spectral descriptor.
@ -120,7 +120,7 @@ impl SpectralDesc {
]
}
pub fn new(sample_rate: u32) -> Result<Self, BlissError> {
pub fn new(sample_rate: u32) -> BlissResult<Self> {
Ok(SpectralDesc {
centroid_aubio_desc: SpecDesc::new(SpecShape::Centroid, SpectralDesc::WINDOW_SIZE)
.map_err(|e| {
@ -158,7 +158,7 @@ impl SpectralDesc {
* `get_centroid`, `get_flatness` and `get_rolloff` to get the respective
* descriptors' values.
*/
pub fn do_(&mut self, chunk: &[f32]) -> Result<(), BlissError> {
pub fn do_(&mut self, chunk: &[f32]) -> BlissResult<()> {
let mut fftgrain: Vec<f32> = vec![0.0; SpectralDesc::WINDOW_SIZE];
self.phase_vocoder
.do_(chunk, fftgrain.as_mut_slice())
@ -266,6 +266,7 @@ impl Normalize for ZeroCrossingRateDesc {
mod tests {
use super::*;
use crate::Song;
use std::path::Path;
#[test]
fn test_zcr_boundaries() {
@ -287,7 +288,7 @@ mod tests {
#[test]
fn test_zcr() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap();
let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut zcr_desc = ZeroCrossingRateDesc::default();
for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) {
zcr_desc.do_(&chunk);
@ -309,7 +310,7 @@ mod tests {
assert!(0.0000001 > (expected - actual).abs());
}
let song = Song::decode("data/white_noise.flac").unwrap();
let song = Song::decode(Path::new("data/white_noise.flac")).unwrap();
let mut spectral_desc = SpectralDesc::new(22050).unwrap();
for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) {
spectral_desc.do_(&chunk).unwrap();
@ -326,7 +327,7 @@ mod tests {
#[test]
fn test_spectral_flatness() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap();
let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) {
spectral_desc.do_(&chunk).unwrap();
@ -356,7 +357,7 @@ mod tests {
assert!(0.0000001 > (expected - actual).abs());
}
let song = Song::decode("data/tone_11080Hz.flac").unwrap();
let song = Song::decode(Path::new("data/tone_11080Hz.flac")).unwrap();
let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) {
spectral_desc.do_(&chunk).unwrap();
@ -372,7 +373,7 @@ mod tests {
#[test]
fn test_spectral_roll_off() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap();
let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) {
spectral_desc.do_(&chunk).unwrap();
@ -390,7 +391,7 @@ mod tests {
#[test]
fn test_spectral_centroid() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap();
let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) {
spectral_desc.do_(&chunk).unwrap();
@ -419,7 +420,7 @@ mod tests {
{
assert!(0.0000001 > (expected - actual).abs());
}
let song = Song::decode("data/tone_11080Hz.flac").unwrap();
let song = Song::decode(Path::new("data/tone_11080Hz.flac")).unwrap();
let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
for chunk in song.sample_array.chunks_exact(SpectralDesc::HOP_SIZE) {
spectral_desc.do_(&chunk).unwrap();

View file

@ -172,6 +172,7 @@ mod tests {
use ndarray::{arr1, Array};
use ndarray_npy::ReadNpyExt;
use std::fs::File;
use std::path::Path;
#[test]
fn test_convolve() {
@ -497,7 +498,7 @@ mod tests {
let file = File::open("data/librosa-stft.npy").unwrap();
let expected_stft = Array2::<f32>::read_npy(file).unwrap().mapv(|x| x as f64);
let song = Song::decode("data/piano.flac").unwrap();
let song = Song::decode(Path::new("data/piano.flac")).unwrap();
let stft = stft(&song.sample_array, 2048, 512);
@ -525,6 +526,7 @@ mod bench {
use crate::Song;
use ndarray::Array;
use test::Bencher;
use std::path::Path;
#[bench]
fn bench_convolve(b: &mut Bencher) {
@ -538,7 +540,7 @@ mod bench {
#[bench]
fn bench_compute_stft(b: &mut Bencher) {
let signal = Song::decode("data/piano.flac").unwrap().sample_array;
let signal = Song::decode(Path::new("data/piano.flac")).unwrap().sample_array;
b.iter(|| {
stft(&signal, 2048, 512);