1#![deny(clippy::missing_inline_in_public_items)]
11
12pub mod chroma;
13pub mod clustering;
14pub mod decoder;
15pub mod errors;
16pub mod misc;
17pub mod temporal;
18pub mod timbral;
19pub mod utils;
20
21use std::{ops::Index, path::PathBuf};
22
23use misc::LoudnessDesc;
24use serde::{Deserialize, Serialize};
25use strum::{EnumCount, EnumIter, IntoEnumIterator};
26
27use chroma::ChromaDesc;
28use errors::{AnalysisError, AnalysisResult};
29use temporal::BPMDesc;
30use timbral::{SpectralDesc, ZeroCrossingRateDesc};
31
32#[derive(Debug)]
36pub struct ResampledAudio {
37 pub path: PathBuf,
38 pub samples: Vec<f32>,
39}
40
41impl TryInto<Analysis> for ResampledAudio {
42 type Error = AnalysisError;
43
44 #[inline]
45 fn try_into(self) -> Result<Analysis, Self::Error> {
46 Analysis::from_samples(&self)
47 }
48}
49
50pub const SAMPLE_RATE: u32 = 22050;
52
53#[derive(Debug, EnumIter, EnumCount)]
54#[allow(missing_docs, clippy::module_name_repetitions)]
61pub enum AnalysisIndex {
62 Tempo,
63 Zcr,
64 MeanSpectralCentroid,
65 StdDeviationSpectralCentroid,
66 MeanSpectralRolloff,
67 StdDeviationSpectralRolloff,
68 MeanSpectralFlatness,
69 StdDeviationSpectralFlatness,
70 MeanLoudness,
71 StdDeviationLoudness,
72 Chroma1,
73 Chroma2,
74 Chroma3,
75 Chroma4,
76 Chroma5,
77 Chroma6,
78 Chroma7,
79 Chroma8,
80 Chroma9,
81 Chroma10,
82}
83
84pub type Feature = f64;
86pub const NUMBER_FEATURES: usize = AnalysisIndex::COUNT;
88
89#[derive(Default, PartialEq, Clone, Copy, Serialize, Deserialize)]
90pub struct Analysis {
105 pub(crate) internal_analysis: [Feature; NUMBER_FEATURES],
106}
107
108impl Index<AnalysisIndex> for Analysis {
109 type Output = Feature;
110
111 #[inline]
112 fn index(&self, index: AnalysisIndex) -> &Feature {
113 &self.internal_analysis[index as usize]
114 }
115}
116
117impl Index<usize> for Analysis {
118 type Output = Feature;
119
120 #[inline]
121 fn index(&self, index: usize) -> &Feature {
122 &self.internal_analysis[index]
123 }
124}
125
126impl std::fmt::Debug for Analysis {
127 #[inline]
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 let mut debug_struct = f.debug_struct("Analysis");
130 for feature in AnalysisIndex::iter() {
131 debug_struct.field(&format!("{feature:?}"), &self[feature]);
132 }
133 debug_struct.finish()?;
134 f.write_str(&format!(" /* {:?} */", &self.as_vec()))
135 }
136}
137
138impl Analysis {
139 #[must_use]
145 #[inline]
146 pub const fn new(analysis: [Feature; NUMBER_FEATURES]) -> Self {
147 Self {
148 internal_analysis: analysis,
149 }
150 }
151
152 #[inline]
160 pub fn from_vec(features: Vec<Feature>) -> Result<Self, AnalysisError> {
161 features
162 .try_into()
163 .map_err(|_| AnalysisError::InvalidFeaturesLen)
164 .map(Self::new)
165 }
166
167 #[must_use]
170 #[inline]
171 pub const fn inner(&self) -> &[Feature; NUMBER_FEATURES] {
172 &self.internal_analysis
173 }
174
175 #[must_use]
180 #[inline]
181 pub fn as_vec(&self) -> Vec<Feature> {
182 self.internal_analysis.to_vec()
183 }
184
185 #[allow(clippy::missing_inline_in_public_items)]
199 pub fn from_samples(audio: &ResampledAudio) -> AnalysisResult<Self> {
200 let largest_window = vec![
201 BPMDesc::WINDOW_SIZE,
202 ChromaDesc::WINDOW_SIZE,
203 SpectralDesc::WINDOW_SIZE,
204 LoudnessDesc::WINDOW_SIZE,
205 ]
206 .into_iter()
207 .max()
208 .unwrap();
209 if audio.samples.len() < largest_window {
210 return Err(AnalysisError::EmptySamples);
211 }
212
213 std::thread::scope(|s| -> AnalysisResult<Self> {
214 let child_chroma: std::thread::ScopedJoinHandle<AnalysisResult<Vec<Feature>>> = s
215 .spawn(|| {
216 let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
217 chroma_desc.do_(&audio.samples)?;
218 Ok(chroma_desc.get_value())
219 });
220
221 #[allow(clippy::type_complexity)]
222 let child_timbral: std::thread::ScopedJoinHandle<
223 AnalysisResult<(Vec<Feature>, Vec<Feature>, Vec<Feature>)>,
224 > = s.spawn(|| {
225 let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE)?;
226 let windows = audio
227 .samples
228 .windows(SpectralDesc::WINDOW_SIZE)
229 .step_by(SpectralDesc::HOP_SIZE);
230 for window in windows {
231 spectral_desc.do_(window)?;
232 }
233 let centroid = spectral_desc.get_centroid();
234 let rolloff = spectral_desc.get_rolloff();
235 let flatness = spectral_desc.get_flatness();
236 Ok((centroid, rolloff, flatness))
237 });
238
239 let child_temp_zcr_loudness: std::thread::ScopedJoinHandle<
241 AnalysisResult<(Feature, Feature, Vec<Feature>)>,
242 > = s.spawn(|| {
243 let mut tempo_desc = BPMDesc::new(SAMPLE_RATE)?;
245 let windows = audio
246 .samples
247 .windows(BPMDesc::WINDOW_SIZE)
248 .step_by(BPMDesc::HOP_SIZE);
249 for window in windows {
250 tempo_desc.do_(window)?;
251 }
252 let tempo = tempo_desc.get_value();
253
254 let mut zcr_desc = ZeroCrossingRateDesc::default();
256 zcr_desc.do_(&audio.samples);
257 let zcr = zcr_desc.get_value();
258
259 let mut loudness_desc = LoudnessDesc::default();
261 let windows = audio.samples.chunks(LoudnessDesc::WINDOW_SIZE);
262 for window in windows {
263 loudness_desc.do_(window);
264 }
265 let loudness = loudness_desc.get_value();
266
267 Ok((tempo, zcr, loudness))
268 });
269
270 let chroma = child_chroma.join().unwrap()?;
272 let (centroid, rolloff, flatness) = child_timbral.join().unwrap()?;
273 let (tempo, zcr, loudness) = child_temp_zcr_loudness.join().unwrap()?;
274
275 let mut result = vec![tempo, zcr];
276 result.extend_from_slice(¢roid);
277 result.extend_from_slice(&rolloff);
278 result.extend_from_slice(&flatness);
279 result.extend_from_slice(&loudness);
280 result.extend_from_slice(&chroma);
281 let array: [Feature; NUMBER_FEATURES] = result
282 .try_into()
283 .map_err(|_| AnalysisError::InvalidFeaturesLen)?;
284 Ok(Self::new(array))
285 })
286 }
287}