Merge pull request #13 from Polochon-street/review-comments

Some review comments
This commit is contained in:
Polochon-street 2021-06-15 23:41:25 +02:00
commit 138ff39dd1
14 changed files with 181 additions and 131 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"

View file

@ -8,7 +8,8 @@ library used to make playlists by analyzing songs, and computing distance betwee
Like bliss, it eases the creation of « intelligent » playlists and/or continuous
play, à la Spotify/Grooveshark Radio, as well as easing creating plug-ins for
existing audio players.
existing audio players. For instance, you can use it to make calm playlists
to help you sleeping, fast playlists to get you started during the day, etc.
For now (and if you're looking for an easy-to use smooth play experience),
[blissify](https://crates.io/crates/blissify) implements bliss for

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);