884 lines
31 KiB
Rust
884 lines
31 KiB
Rust
//! Song decoding / analysis module.
|
|
//!
|
|
//! Use decoding, and features-extraction functions from other modules
|
|
//! e.g. tempo features, spectral features, etc to build a Song and its
|
|
//! corresponding Analysis.
|
|
//!
|
|
//! For implementation of plug-ins for already existing audio players,
|
|
//! a look at Library is instead recommended.
|
|
|
|
extern crate crossbeam;
|
|
extern crate ffmpeg_next as ffmpeg;
|
|
extern crate ndarray;
|
|
extern crate ndarray_npy;
|
|
|
|
use crate::chroma::ChromaDesc;
|
|
use crate::distance::{euclidean_distance, DistanceMetric};
|
|
use crate::misc::LoudnessDesc;
|
|
use crate::temporal::BPMDesc;
|
|
use crate::timbral::{SpectralDesc, ZeroCrossingRateDesc};
|
|
use crate::{BlissError, BlissResult, SAMPLE_RATE};
|
|
use crate::{CHANNELS, FEATURES_VERSION};
|
|
use ::log::warn;
|
|
use core::ops::Index;
|
|
use crossbeam::thread;
|
|
use ffmpeg_next::codec::threading::{Config, Type as ThreadingType};
|
|
use ffmpeg_next::util;
|
|
use ffmpeg_next::util::channel_layout::ChannelLayout;
|
|
use ffmpeg_next::util::error::Error;
|
|
use ffmpeg_next::util::error::EINVAL;
|
|
use ffmpeg_next::util::format::sample::{Sample, Type};
|
|
use ffmpeg_next::util::frame::audio::Audio;
|
|
use ffmpeg_next::util::log;
|
|
use ffmpeg_next::util::log::level::Level;
|
|
use ndarray::{arr1, Array1};
|
|
use std::convert::TryInto;
|
|
use std::fmt;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::sync::mpsc;
|
|
use std::sync::mpsc::Receiver;
|
|
use std::thread as std_thread;
|
|
use strum::{EnumCount, IntoEnumIterator};
|
|
use strum_macros::{EnumCount, EnumIter};
|
|
|
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
#[derive(Default, Debug, PartialEq, Clone)]
|
|
/// Simple object used to represent a Song, with its path, analysis, and
|
|
/// other metadata (artist, genre...)
|
|
pub struct Song {
|
|
/// Song's provided file path
|
|
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: Option<String>,
|
|
/// bliss analysis results
|
|
pub analysis: Analysis,
|
|
/// Version of the features the song was analyzed with.
|
|
/// A simple integer that is bumped every time a breaking change
|
|
/// is introduced in the features.
|
|
pub features_version: u16,
|
|
}
|
|
|
|
#[derive(Debug, EnumIter, EnumCount)]
|
|
/// Indexes different fields of an [Analysis](Song::analysis).
|
|
///
|
|
/// * Example:
|
|
/// ```no_run
|
|
/// use bliss_audio::{AnalysisIndex, BlissResult, Song};
|
|
///
|
|
/// fn main() -> BlissResult<()> {
|
|
/// let song = Song::from_path("path/to/song")?;
|
|
/// println!("{}", song.analysis[AnalysisIndex::Tempo]);
|
|
/// Ok(())
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Prints the tempo value of an analysis.
|
|
///
|
|
/// Note that this should mostly be used for debugging / distance metric
|
|
/// customization purposes.
|
|
#[allow(missing_docs)]
|
|
pub enum AnalysisIndex {
|
|
Tempo,
|
|
Zcr,
|
|
MeanSpectralCentroid,
|
|
StdDeviationSpectralCentroid,
|
|
MeanSpectralRolloff,
|
|
StdDeviationSpectralRolloff,
|
|
MeanSpectralFlatness,
|
|
StdDeviationSpectralFlatness,
|
|
MeanLoudness,
|
|
StdDeviationLoudness,
|
|
Chroma1,
|
|
Chroma2,
|
|
Chroma3,
|
|
Chroma4,
|
|
Chroma5,
|
|
Chroma6,
|
|
Chroma7,
|
|
Chroma8,
|
|
Chroma9,
|
|
Chroma10,
|
|
}
|
|
/// The number of features used in `Analysis`
|
|
pub const NUMBER_FEATURES: usize = AnalysisIndex::COUNT;
|
|
|
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
#[derive(Default, PartialEq, Clone, Copy)]
|
|
/// Object holding the results of the song's analysis.
|
|
///
|
|
/// 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.
|
|
///
|
|
/// Under the hood, it is just an array of f32 holding different numeric
|
|
/// features.
|
|
///
|
|
/// For more info on the different features, build the
|
|
/// documentation with private items included using
|
|
/// `cargo doc --document-private-items`, and / or read up
|
|
/// [this document](https://lelele.io/thesis.pdf), that contains a description
|
|
/// on most of the features, except the chroma ones, which are documented
|
|
/// directly in this code.
|
|
pub struct Analysis {
|
|
internal_analysis: [f32; NUMBER_FEATURES],
|
|
}
|
|
|
|
impl Index<AnalysisIndex> for Analysis {
|
|
type Output = f32;
|
|
|
|
fn index(&self, index: AnalysisIndex) -> &f32 {
|
|
&self.internal_analysis[index as usize]
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Analysis {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let mut debug_struct = f.debug_struct("Analysis");
|
|
for feature in AnalysisIndex::iter() {
|
|
debug_struct.field(&format!("{:?}", feature), &self[feature]);
|
|
}
|
|
debug_struct.finish()?;
|
|
f.write_str(&format!(" /* {:?} */", &self.as_vec()))
|
|
}
|
|
}
|
|
|
|
impl Analysis {
|
|
/// Create a new Analysis object.
|
|
///
|
|
/// Usually not needed, unless you have already computed and stored
|
|
/// features somewhere, and need to recreate a Song with an already
|
|
/// existing Analysis yourself.
|
|
pub fn new(analysis: [f32; NUMBER_FEATURES]) -> Analysis {
|
|
Analysis {
|
|
internal_analysis: analysis,
|
|
}
|
|
}
|
|
|
|
/// Return an ndarray `Array1` representing the analysis' features.
|
|
///
|
|
/// Particularly useful if you want to make a custom distance metric.
|
|
pub fn as_arr1(&self) -> Array1<f32> {
|
|
arr1(&self.internal_analysis)
|
|
}
|
|
|
|
/// Return a Vec<f32> representing the analysis' features.
|
|
///
|
|
/// Particularly useful if you want iterate through the values to store
|
|
/// them somewhere.
|
|
pub fn as_vec(&self) -> Vec<f32> {
|
|
self.internal_analysis.to_vec()
|
|
}
|
|
|
|
/// Compute distance between two analysis using a user-provided distance
|
|
/// metric. You most likely want to use `song.custom_distance` directly
|
|
/// rather than this function.
|
|
///
|
|
/// For this function to be integrated properly with the rest
|
|
/// of bliss' parts, it should be a valid distance metric, i.e.:
|
|
/// 1. For X, Y real vectors, d(X, Y) = 0 ⇔ X = Y
|
|
/// 2. For X, Y real vectors, d(X, Y) >= 0
|
|
/// 3. For X, Y real vectors, d(X, Y) = d(Y, X)
|
|
/// 4. For X, Y, Z real vectors d(X, Y) ≤ d(X + Z) + d(Z, Y)
|
|
///
|
|
/// Note that almost all distance metrics you will find obey these
|
|
/// properties, so don't sweat it too much.
|
|
pub fn custom_distance(&self, other: &Self, distance: impl DistanceMetric) -> f32 {
|
|
distance(&self.as_arr1(), &other.as_arr1())
|
|
}
|
|
}
|
|
|
|
impl Song {
|
|
#[allow(dead_code)]
|
|
/// Compute the distance between the current song and any given
|
|
/// Song.
|
|
///
|
|
/// The smaller the number, the closer the songs; usually more useful
|
|
/// if compared between several songs
|
|
/// (e.g. if song1.distance(song2) < song1.distance(song3), then song1 is
|
|
/// closer to song2 than it is to song3.
|
|
///
|
|
/// Currently uses the euclidean distance, but this can change in an
|
|
/// upcoming release if another metric performs better.
|
|
pub fn distance(&self, other: &Self) -> f32 {
|
|
self.analysis
|
|
.custom_distance(&other.analysis, euclidean_distance)
|
|
}
|
|
|
|
/// Compute distance between two songs using a user-provided distance
|
|
/// metric.
|
|
///
|
|
/// For this function to be integrated properly with the rest
|
|
/// of bliss' parts, it should be a valid distance metric, i.e.:
|
|
/// 1. For X, Y real vectors, d(X, Y) = 0 ⇔ X = Y
|
|
/// 2. For X, Y real vectors, d(X, Y) >= 0
|
|
/// 3. For X, Y real vectors, d(X, Y) = d(Y, X)
|
|
/// 4. For X, Y, Z real vectors d(X, Y) ≤ d(X + Z) + d(Z, Y)
|
|
///
|
|
/// Note that almost all distance metrics you will find obey these
|
|
/// properties, so don't sweat it too much.
|
|
pub fn custom_distance(&self, other: &Self, distance: impl DistanceMetric) -> f32 {
|
|
self.analysis.custom_distance(&other.analysis, distance)
|
|
}
|
|
|
|
/// Returns a decoded [Song] given a file path, or an error if the song
|
|
/// could not be analyzed for some reason.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `path` - A [Path] holding a valid file path to a valid audio file.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// This function will return an error if the file path is invalid, if
|
|
/// the file path points to a file containing no or corrupted audio stream,
|
|
/// or if the analysis could not be conducted to the end for some reason.
|
|
///
|
|
/// 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 from_path<P: AsRef<Path>>(path: P) -> BlissResult<Self> {
|
|
let raw_song = Song::decode(path.as_ref())?;
|
|
|
|
Ok(Song {
|
|
path: raw_song.path,
|
|
artist: raw_song.artist,
|
|
title: raw_song.title,
|
|
album: raw_song.album,
|
|
track_number: raw_song.track_number,
|
|
genre: raw_song.genre,
|
|
analysis: Song::analyze(raw_song.sample_array)?,
|
|
features_version: FEATURES_VERSION,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Analyze a song decoded in `sample_array`, with one channel @ 22050 Hz.
|
|
*
|
|
* The current implementation doesn't make use of it,
|
|
* but the song can also be streamed wrt.
|
|
* each descriptor (with the exception of the chroma descriptor which
|
|
* yields worse results when streamed).
|
|
*
|
|
* Useful in the rare cases where the full song is not
|
|
* completely available.
|
|
**/
|
|
fn analyze(sample_array: Vec<f32>) -> BlissResult<Analysis> {
|
|
let largest_window = vec![
|
|
BPMDesc::WINDOW_SIZE,
|
|
ChromaDesc::WINDOW_SIZE,
|
|
SpectralDesc::WINDOW_SIZE,
|
|
LoudnessDesc::WINDOW_SIZE,
|
|
]
|
|
.into_iter()
|
|
.max()
|
|
.unwrap();
|
|
if sample_array.len() < largest_window {
|
|
return Err(BlissError::AnalysisError(String::from(
|
|
"empty or too short song.",
|
|
)));
|
|
}
|
|
|
|
thread::scope(|s| {
|
|
let child_tempo: thread::ScopedJoinHandle<'_, BlissResult<f32>> = s.spawn(|_| {
|
|
let mut tempo_desc = BPMDesc::new(SAMPLE_RATE)?;
|
|
let windows = sample_array
|
|
.windows(BPMDesc::WINDOW_SIZE)
|
|
.step_by(BPMDesc::HOP_SIZE);
|
|
|
|
for window in windows {
|
|
tempo_desc.do_(window)?;
|
|
}
|
|
Ok(tempo_desc.get_value())
|
|
});
|
|
|
|
let child_chroma: thread::ScopedJoinHandle<'_, BlissResult<Vec<f32>>> = s.spawn(|_| {
|
|
let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
|
|
chroma_desc.do_(&sample_array)?;
|
|
Ok(chroma_desc.get_values())
|
|
});
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
let child_timbral: thread::ScopedJoinHandle<
|
|
'_,
|
|
BlissResult<(Vec<f32>, Vec<f32>, Vec<f32>)>,
|
|
> = s.spawn(|_| {
|
|
let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE)?;
|
|
let windows = sample_array
|
|
.windows(SpectralDesc::WINDOW_SIZE)
|
|
.step_by(SpectralDesc::HOP_SIZE);
|
|
for window in windows {
|
|
spectral_desc.do_(window)?;
|
|
}
|
|
let centroid = spectral_desc.get_centroid();
|
|
let rolloff = spectral_desc.get_rolloff();
|
|
let flatness = spectral_desc.get_flatness();
|
|
Ok((centroid, rolloff, flatness))
|
|
});
|
|
|
|
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<'_, BlissResult<Vec<f32>>> =
|
|
s.spawn(|_| {
|
|
let mut loudness_desc = LoudnessDesc::default();
|
|
let windows = sample_array.chunks(LoudnessDesc::WINDOW_SIZE);
|
|
|
|
for window in windows {
|
|
loudness_desc.do_(window);
|
|
}
|
|
Ok(loudness_desc.get_value())
|
|
});
|
|
|
|
// Non-streaming approach for that one
|
|
let tempo = child_tempo.join().unwrap()?;
|
|
let chroma = child_chroma.join().unwrap()?;
|
|
let (centroid, rolloff, flatness) = child_timbral.join().unwrap()?;
|
|
let loudness = child_loudness.join().unwrap()?;
|
|
let zcr = child_zcr.join().unwrap()?;
|
|
|
|
let mut result = vec![tempo, zcr];
|
|
result.extend_from_slice(¢roid);
|
|
result.extend_from_slice(&rolloff);
|
|
result.extend_from_slice(&flatness);
|
|
result.extend_from_slice(&loudness);
|
|
result.extend_from_slice(&chroma);
|
|
let array: [f32; NUMBER_FEATURES] = result.try_into().map_err(|_| {
|
|
BlissError::AnalysisError(
|
|
"Too many or too little features were provided at the end of
|
|
the analysis."
|
|
.to_string(),
|
|
)
|
|
})?;
|
|
Ok(Analysis::new(array))
|
|
})
|
|
.unwrap()
|
|
}
|
|
|
|
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.into(),
|
|
..Default::default()
|
|
};
|
|
let mut format = ffmpeg::format::input(&path)
|
|
.map_err(|e| BlissError::DecodingError(format!("while opening format: {:?}.", e)))?;
|
|
let (mut codec, stream, expected_sample_number) = {
|
|
let stream = format
|
|
.streams()
|
|
.find(|s| s.parameters().medium() == ffmpeg::media::Type::Audio)
|
|
.ok_or_else(|| BlissError::DecodingError(String::from("No audio stream found.")))?;
|
|
let mut context = ffmpeg::codec::context::Context::from_parameters(stream.parameters())
|
|
.map_err(|e| {
|
|
BlissError::DecodingError(format!("Could not load the codec context: {:?}", e))
|
|
})?;
|
|
context.set_threading(Config {
|
|
kind: ThreadingType::Frame,
|
|
count: 0,
|
|
safe: true,
|
|
});
|
|
let codec = context
|
|
.decoder()
|
|
.audio()
|
|
.map_err(|e| BlissError::DecodingError(format!("when finding codec: {:?}.", e)))?;
|
|
// Add SAMPLE_RATE to have one second margin to avoid reallocating if
|
|
// the duration is slightly more than estimated
|
|
// TODO>1.0 another way to get the exact number of samples is to decode
|
|
// everything once, compute the real number of samples from that,
|
|
// allocate the array with that number, and decode again. Check
|
|
// what's faster between reallocating, and just have one second
|
|
// leeway.
|
|
let expected_sample_number = (SAMPLE_RATE as f32 * stream.duration() as f32
|
|
/ stream.time_base().denominator() as f32)
|
|
.ceil()
|
|
+ SAMPLE_RATE as f32;
|
|
(codec, stream.index(), expected_sample_number)
|
|
};
|
|
let sample_array: Vec<f32> = Vec::with_capacity(expected_sample_number as usize);
|
|
if let Some(title) = format.metadata().get("title") {
|
|
song.title = match title {
|
|
"" => None,
|
|
t => Some(t.to_string()),
|
|
};
|
|
};
|
|
if let Some(artist) = format.metadata().get("artist") {
|
|
song.artist = match artist {
|
|
"" => None,
|
|
a => Some(a.to_string()),
|
|
};
|
|
};
|
|
if let Some(album) = format.metadata().get("album") {
|
|
song.album = match album {
|
|
"" => None,
|
|
a => Some(a.to_string()),
|
|
};
|
|
};
|
|
if let Some(genre) = format.metadata().get("genre") {
|
|
song.genre = match genre {
|
|
"" => None,
|
|
g => Some(g.to_string()),
|
|
};
|
|
};
|
|
if let Some(track_number) = format.metadata().get("track") {
|
|
song.track_number = match track_number {
|
|
"" => None,
|
|
t => Some(t.to_string()),
|
|
};
|
|
};
|
|
let in_channel_layout = {
|
|
if codec.channel_layout() == ChannelLayout::empty() {
|
|
ChannelLayout::default(codec.channels().into())
|
|
} else {
|
|
codec.channel_layout()
|
|
}
|
|
};
|
|
codec.set_channel_layout(in_channel_layout);
|
|
|
|
let (tx, rx) = mpsc::channel();
|
|
let in_codec_format = codec.format();
|
|
let in_codec_rate = codec.rate();
|
|
let child = std_thread::spawn(move || {
|
|
resample_frame(
|
|
rx,
|
|
in_codec_format,
|
|
in_channel_layout,
|
|
in_codec_rate,
|
|
sample_array,
|
|
)
|
|
});
|
|
for (s, packet) in format.packets() {
|
|
if s.index() != stream {
|
|
continue;
|
|
}
|
|
match codec.send_packet(&packet) {
|
|
Ok(_) => (),
|
|
Err(Error::Other { errno: EINVAL }) => {
|
|
return Err(BlissError::DecodingError(String::from(
|
|
"wrong codec opened.",
|
|
)))
|
|
}
|
|
Err(Error::Eof) => {
|
|
warn!("Premature EOF reached while decoding.");
|
|
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 codec.receive_frame(&mut decoded) {
|
|
Ok(_) => {
|
|
tx.send(decoded).map_err(|e| {
|
|
BlissError::DecodingError(format!(
|
|
"while sending decoded frame to the resampling thread: {:?}",
|
|
e
|
|
))
|
|
})?;
|
|
}
|
|
Err(_) => break,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Flush the stream
|
|
let packet = ffmpeg::codec::packet::Packet::empty();
|
|
match codec.send_packet(&packet) {
|
|
Ok(_) => (),
|
|
Err(Error::Other { errno: EINVAL }) => {
|
|
return Err(BlissError::DecodingError(String::from(
|
|
"wrong codec opened.",
|
|
)))
|
|
}
|
|
Err(Error::Eof) => {
|
|
warn!("Premature EOF reached while decoding.");
|
|
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 codec.receive_frame(&mut decoded) {
|
|
Ok(_) => {
|
|
tx.send(decoded).map_err(|e| {
|
|
BlissError::DecodingError(format!(
|
|
"while sending decoded frame to the resampling thread: {:?}",
|
|
e
|
|
))
|
|
})?;
|
|
}
|
|
Err(_) => break,
|
|
}
|
|
}
|
|
|
|
drop(tx);
|
|
song.sample_array = child.join().unwrap()?;
|
|
Ok(song)
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Debug)]
|
|
pub(crate) struct InternalSong {
|
|
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>,
|
|
}
|
|
|
|
fn resample_frame(
|
|
rx: Receiver<Audio>,
|
|
in_codec_format: Sample,
|
|
in_channel_layout: ChannelLayout,
|
|
in_rate: u32,
|
|
mut sample_array: Vec<f32>,
|
|
) -> BlissResult<Vec<f32>> {
|
|
let mut resample_context = ffmpeg::software::resampling::context::Context::get(
|
|
in_codec_format,
|
|
in_channel_layout,
|
|
in_rate,
|
|
Sample::F32(Type::Packed),
|
|
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();
|
|
for decoded in rx.iter() {
|
|
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);
|
|
}
|
|
// 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>) {
|
|
if frame.samples() == 0 {
|
|
return;
|
|
}
|
|
// Account for the padding
|
|
let actual_size = util::format::sample::Buffer::size(
|
|
Sample::F32(Type::Packed),
|
|
CHANNELS,
|
|
frame.samples(),
|
|
false,
|
|
);
|
|
let f32_frame: Vec<f32> = frame.data(0)[..actual_size]
|
|
.chunks_exact(4)
|
|
.map(|x| {
|
|
let mut a: [u8; 4] = [0; 4];
|
|
a.copy_from_slice(x);
|
|
f32::from_le_bytes(a)
|
|
})
|
|
.collect();
|
|
sample_array.extend_from_slice(&f32_frame);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use ripemd160::{Digest, Ripemd160};
|
|
use std::path::Path;
|
|
|
|
#[test]
|
|
fn test_analysis_too_small() {
|
|
let error = Song::analyze(vec![0.]).unwrap_err();
|
|
assert_eq!(
|
|
error,
|
|
BlissError::AnalysisError(String::from("empty or too short song."))
|
|
);
|
|
|
|
let error = Song::analyze(vec![]).unwrap_err();
|
|
assert_eq!(
|
|
error,
|
|
BlissError::AnalysisError(String::from("empty or too short song."))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_analyze() {
|
|
let song = Song::from_path(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
|
|
let expected_analysis = vec![
|
|
0.3846389,
|
|
-0.849141,
|
|
-0.75481045,
|
|
-0.8790748,
|
|
-0.63258266,
|
|
-0.7258959,
|
|
-0.775738,
|
|
-0.8146726,
|
|
0.2716726,
|
|
0.25779057,
|
|
-0.35661936,
|
|
-0.63578653,
|
|
-0.29593682,
|
|
0.06421304,
|
|
0.21852458,
|
|
-0.581239,
|
|
-0.9466835,
|
|
-0.9481153,
|
|
-0.9820945,
|
|
-0.95968974,
|
|
];
|
|
for (x, y) in song.analysis.as_vec().iter().zip(expected_analysis) {
|
|
assert!(0.01 > (x - y).abs());
|
|
}
|
|
assert_eq!(FEATURES_VERSION, song.features_version);
|
|
}
|
|
|
|
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() {
|
|
hasher.update(sample.to_le_bytes().to_vec());
|
|
}
|
|
|
|
assert_eq!(expected_hash, hasher.finalize().as_slice());
|
|
}
|
|
|
|
#[test]
|
|
fn test_tags() {
|
|
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 = 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,
|
|
];
|
|
_test_decode(&path, &expected_hash);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resample_stereo() {
|
|
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,
|
|
];
|
|
_test_decode(&path, &expected_hash);
|
|
}
|
|
|
|
#[test]
|
|
fn test_decode_mono() {
|
|
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 -
|
|
let expected_hash = [
|
|
0x9d, 0x95, 0xa5, 0xf2, 0xd2, 0x9c, 0x68, 0xe8, 0x8a, 0x70, 0xcd, 0xf3, 0x54, 0x2c,
|
|
0x5b, 0x45, 0x98, 0xb4, 0xf3, 0xb4,
|
|
];
|
|
_test_decode(&path, &expected_hash);
|
|
}
|
|
|
|
#[test]
|
|
fn test_decode_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 -
|
|
let expected_hash = [
|
|
0x28, 0x25, 0x6b, 0x7b, 0x6e, 0x37, 0x1c, 0xcf, 0xc7, 0x06, 0xdf, 0x62, 0x8c, 0x0e,
|
|
0x91, 0xf7, 0xd6, 0x1f, 0xac, 0x5b,
|
|
];
|
|
_test_decode(&path, &expected_hash);
|
|
}
|
|
|
|
#[test]
|
|
fn test_dont_panic_no_channel_layout() {
|
|
let path = Path::new("data/no_channel.wav");
|
|
Song::decode(&path).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_decode_right_capacity_vec() {
|
|
let path = Path::new("data/s16_mono_22_5kHz.flac");
|
|
let song = Song::decode(&path).unwrap();
|
|
let sample_array = song.sample_array;
|
|
assert_eq!(
|
|
sample_array.len() + SAMPLE_RATE as usize,
|
|
sample_array.capacity()
|
|
);
|
|
|
|
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!(
|
|
sample_array.len() + SAMPLE_RATE as usize,
|
|
sample_array.capacity()
|
|
);
|
|
|
|
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);
|
|
assert!(sample_array.len() as f32 / (sample_array.capacity() as f32) < 1.);
|
|
}
|
|
|
|
#[test]
|
|
fn test_analysis_distance() {
|
|
let mut a = Song::default();
|
|
a.analysis = Analysis::new([
|
|
0.16391512, 0.11326739, 0.96868552, 0.8353934, 0.49867523, 0.76532606, 0.63448005,
|
|
0.82506196, 0.71457147, 0.62395476, 0.69680329, 0.9855766, 0.41369333, 0.13900452,
|
|
0.68001012, 0.11029723, 0.97192943, 0.57727861, 0.07994821, 0.88993185,
|
|
]);
|
|
|
|
let mut b = Song::default();
|
|
b.analysis = Analysis::new([
|
|
0.5075758, 0.36440256, 0.28888011, 0.43032829, 0.62387977, 0.61894916, 0.99676086,
|
|
0.11913155, 0.00640396, 0.15943407, 0.33829514, 0.34947174, 0.82927523, 0.18987604,
|
|
0.54437275, 0.22076826, 0.91232151, 0.29233168, 0.32846024, 0.04522147,
|
|
]);
|
|
assert_eq!(a.distance(&b), 1.9469079)
|
|
}
|
|
|
|
#[test]
|
|
fn test_analysis_distance_indiscernible() {
|
|
let mut a = Song::default();
|
|
a.analysis = Analysis::new([
|
|
1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15., 16., 17., 18., 19.,
|
|
20.,
|
|
]);
|
|
assert_eq!(a.distance(&a), 0.)
|
|
}
|
|
|
|
#[test]
|
|
fn test_decode_errors() {
|
|
assert_eq!(
|
|
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(Path::new("data/picture.png")).unwrap_err(),
|
|
BlissError::DecodingError(String::from("No audio stream found.")),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_index_analysis() {
|
|
let song = Song::from_path("data/s16_mono_22_5kHz.flac").unwrap();
|
|
assert_eq!(song.analysis[AnalysisIndex::Tempo], 0.3846389);
|
|
assert_eq!(song.analysis[AnalysisIndex::Chroma10], -0.95968974);
|
|
}
|
|
|
|
#[test]
|
|
fn test_debug_analysis() {
|
|
let song = Song::from_path("data/s16_mono_22_5kHz.flac").unwrap();
|
|
assert_eq!(
|
|
"Analysis { Tempo: 0.3846389, Zcr: -0.849141, MeanSpectralCentroid: \
|
|
-0.75481045, StdDeviationSpectralCentroid: -0.8790748, MeanSpectralR\
|
|
olloff: -0.63258266, StdDeviationSpectralRolloff: -0.7258959, MeanSp\
|
|
ectralFlatness: -0.7757379, StdDeviationSpectralFlatness: -0.8146726\
|
|
, MeanLoudness: 0.2716726, StdDeviationLoudness: 0.25779057, Chroma1\
|
|
: -0.35661936, Chroma2: -0.63578653, Chroma3: -0.29593682, Chroma4: \
|
|
0.06421304, Chroma5: 0.21852458, Chroma6: -0.581239, Chroma7: -0.946\
|
|
6835, Chroma8: -0.9481153, Chroma9: -0.9820945, Chroma10: -0.95968974 } \
|
|
/* [0.3846389, -0.849141, -0.75481045, -0.8790748, -0.63258266, -0.\
|
|
7258959, -0.7757379, -0.8146726, 0.2716726, 0.25779057, -0.35661936, \
|
|
-0.63578653, -0.29593682, 0.06421304, 0.21852458, -0.581239, -0.946\
|
|
6835, -0.9481153, -0.9820945, -0.95968974] */",
|
|
format!("{:?}", song.analysis),
|
|
);
|
|
}
|
|
|
|
fn dummy_distance(_: &Array1<f32>, _: &Array1<f32>) -> f32 {
|
|
0.
|
|
}
|
|
#[test]
|
|
fn test_custom_distance() {
|
|
let mut a = Song::default();
|
|
a.analysis = Analysis::new([
|
|
0.16391512, 0.11326739, 0.96868552, 0.8353934, 0.49867523, 0.76532606, 0.63448005,
|
|
0.82506196, 0.71457147, 0.62395476, 0.69680329, 0.9855766, 0.41369333, 0.13900452,
|
|
0.68001012, 0.11029723, 0.97192943, 0.57727861, 0.07994821, 0.88993185,
|
|
]);
|
|
|
|
let mut b = Song::default();
|
|
b.analysis = Analysis::new([
|
|
0.5075758, 0.36440256, 0.28888011, 0.43032829, 0.62387977, 0.61894916, 0.99676086,
|
|
0.11913155, 0.00640396, 0.15943407, 0.33829514, 0.34947174, 0.82927523, 0.18987604,
|
|
0.54437275, 0.22076826, 0.91232151, 0.29233168, 0.32846024, 0.04522147,
|
|
]);
|
|
assert_eq!(a.custom_distance(&b, dummy_distance), 0.);
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "bench", test))]
|
|
mod bench {
|
|
extern crate test;
|
|
use crate::Song;
|
|
use std::path::Path;
|
|
use test::Bencher;
|
|
|
|
#[bench]
|
|
fn bench_resample_multi(b: &mut Bencher) {
|
|
let path = Path::new("./data/s32_stereo_44_1_kHz.flac");
|
|
b.iter(|| {
|
|
Song::decode(&path).unwrap();
|
|
});
|
|
}
|
|
}
|