#![deny(clippy::missing_inline_in_public_items)]
pub mod chroma;
pub mod clustering;
pub mod decoder;
pub mod errors;
pub mod misc;
pub mod temporal;
pub mod timbral;
pub mod utils;
use std::{ops::Index, path::PathBuf};
use misc::LoudnessDesc;
use serde::{Deserialize, Serialize};
use strum::{EnumCount, EnumIter, IntoEnumIterator};
use chroma::ChromaDesc;
use errors::{AnalysisError, AnalysisResult};
use temporal::BPMDesc;
use timbral::{SpectralDesc, ZeroCrossingRateDesc};
#[derive(Debug)]
pub struct ResampledAudio {
pub path: PathBuf,
pub samples: Vec<f32>,
}
impl TryInto<Analysis> for ResampledAudio {
type Error = AnalysisError;
#[inline]
fn try_into(self) -> Result<Analysis, Self::Error> {
Analysis::from_samples(&self)
}
}
pub const SAMPLE_RATE: u32 = 22050;
#[derive(Debug, EnumIter, EnumCount)]
#[allow(missing_docs, clippy::module_name_repetitions)]
pub enum AnalysisIndex {
Tempo,
Zcr,
MeanSpectralCentroid,
StdDeviationSpectralCentroid,
MeanSpectralRolloff,
StdDeviationSpectralRolloff,
MeanSpectralFlatness,
StdDeviationSpectralFlatness,
MeanLoudness,
StdDeviationLoudness,
Chroma1,
Chroma2,
Chroma3,
Chroma4,
Chroma5,
Chroma6,
Chroma7,
Chroma8,
Chroma9,
Chroma10,
}
pub type Feature = f64;
pub const NUMBER_FEATURES: usize = AnalysisIndex::COUNT;
#[derive(Default, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub struct Analysis {
pub(crate) internal_analysis: [Feature; NUMBER_FEATURES],
}
impl Index<AnalysisIndex> for Analysis {
type Output = Feature;
#[inline]
fn index(&self, index: AnalysisIndex) -> &Feature {
&self.internal_analysis[index as usize]
}
}
impl Index<usize> for Analysis {
type Output = Feature;
#[inline]
fn index(&self, index: usize) -> &Feature {
&self.internal_analysis[index]
}
}
impl std::fmt::Debug for Analysis {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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 {
#[must_use]
#[inline]
pub const fn new(analysis: [Feature; NUMBER_FEATURES]) -> Self {
Self {
internal_analysis: analysis,
}
}
#[inline]
pub fn from_vec(features: Vec<Feature>) -> Result<Self, AnalysisError> {
features
.try_into()
.map_err(|_| AnalysisError::InvalidFeaturesLen)
.map(Self::new)
}
#[must_use]
#[inline]
pub const fn inner(&self) -> &[Feature; NUMBER_FEATURES] {
&self.internal_analysis
}
#[must_use]
#[inline]
pub fn as_vec(&self) -> Vec<Feature> {
self.internal_analysis.to_vec()
}
#[allow(clippy::missing_inline_in_public_items)]
pub fn from_samples(audio: &ResampledAudio) -> AnalysisResult<Self> {
let largest_window = vec![
BPMDesc::WINDOW_SIZE,
ChromaDesc::WINDOW_SIZE,
SpectralDesc::WINDOW_SIZE,
LoudnessDesc::WINDOW_SIZE,
]
.into_iter()
.max()
.unwrap();
if audio.samples.len() < largest_window {
return Err(AnalysisError::EmptySamples);
}
std::thread::scope(|s| -> AnalysisResult<Self> {
let child_chroma: std::thread::ScopedJoinHandle<AnalysisResult<Vec<Feature>>> = s
.spawn(|| {
let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
chroma_desc.do_(&audio.samples)?;
Ok(chroma_desc.get_value())
});
#[allow(clippy::type_complexity)]
let child_timbral: std::thread::ScopedJoinHandle<
AnalysisResult<(Vec<Feature>, Vec<Feature>, Vec<Feature>)>,
> = s.spawn(|| {
let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE)?;
let windows = audio
.samples
.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_temp_zcr_loudness: std::thread::ScopedJoinHandle<
AnalysisResult<(Feature, Feature, Vec<Feature>)>,
> = s.spawn(|| {
let mut tempo_desc = BPMDesc::new(SAMPLE_RATE)?;
let windows = audio
.samples
.windows(BPMDesc::WINDOW_SIZE)
.step_by(BPMDesc::HOP_SIZE);
for window in windows {
tempo_desc.do_(window)?;
}
let tempo = tempo_desc.get_value();
let mut zcr_desc = ZeroCrossingRateDesc::default();
zcr_desc.do_(&audio.samples);
let zcr = zcr_desc.get_value();
let mut loudness_desc = LoudnessDesc::default();
let windows = audio.samples.chunks(LoudnessDesc::WINDOW_SIZE);
for window in windows {
loudness_desc.do_(window);
}
let loudness = loudness_desc.get_value();
Ok((tempo, zcr, loudness))
});
let chroma = child_chroma.join().unwrap()?;
let (centroid, rolloff, flatness) = child_timbral.join().unwrap()?;
let (tempo, zcr, loudness) = child_temp_zcr_loudness.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: [Feature; NUMBER_FEATURES] = result
.try_into()
.map_err(|_| AnalysisError::InvalidFeaturesLen)?;
Ok(Self::new(array))
})
}
}