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_tempo: std::thread::ScopedJoinHandle<AnalysisResult<Feature>> =
215 s.spawn(|| {
216 let mut tempo_desc = BPMDesc::new(SAMPLE_RATE)?;
217 let windows = audio
218 .samples
219 .windows(BPMDesc::WINDOW_SIZE)
220 .step_by(BPMDesc::HOP_SIZE);
221
222 for window in windows {
223 tempo_desc.do_(window)?;
224 }
225 Ok(tempo_desc.get_value())
226 });
227
228 let child_chroma: std::thread::ScopedJoinHandle<AnalysisResult<Vec<Feature>>> = s
229 .spawn(|| {
230 let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
231 chroma_desc.do_(&audio.samples)?;
232 Ok(chroma_desc.get_value())
233 });
234
235 #[allow(clippy::type_complexity)]
236 let child_timbral: std::thread::ScopedJoinHandle<
237 AnalysisResult<(Vec<Feature>, Vec<Feature>, Vec<Feature>)>,
238 > = s.spawn(|| {
239 let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE)?;
240 let windows = audio
241 .samples
242 .windows(SpectralDesc::WINDOW_SIZE)
243 .step_by(SpectralDesc::HOP_SIZE);
244 for window in windows {
245 spectral_desc.do_(window)?;
246 }
247 let centroid = spectral_desc.get_centroid();
248 let rolloff = spectral_desc.get_rolloff();
249 let flatness = spectral_desc.get_flatness();
250 Ok((centroid, rolloff, flatness))
251 });
252
253 let child_zcr: std::thread::ScopedJoinHandle<AnalysisResult<Feature>> = s.spawn(|| {
254 let mut zcr_desc = ZeroCrossingRateDesc::default();
255 zcr_desc.do_(&audio.samples);
256 Ok(zcr_desc.get_value())
257 });
258
259 let child_loudness: std::thread::ScopedJoinHandle<AnalysisResult<Vec<Feature>>> = s
260 .spawn(|| {
261 let mut loudness_desc = LoudnessDesc::default();
262 let windows = audio.samples.chunks(LoudnessDesc::WINDOW_SIZE);
263
264 for window in windows {
265 loudness_desc.do_(window);
266 }
267 Ok(loudness_desc.get_value())
268 });
269
270 let tempo = child_tempo.join().unwrap()?;
272 let chroma = child_chroma.join().unwrap()?;
273 let (centroid, rolloff, flatness) = child_timbral.join().unwrap()?;
274 let loudness = child_loudness.join().unwrap()?;
275 let zcr = child_zcr.join().unwrap()?;
276
277 let mut result = vec![tempo, zcr];
278 result.extend_from_slice(¢roid);
279 result.extend_from_slice(&rolloff);
280 result.extend_from_slice(&flatness);
281 result.extend_from_slice(&loudness);
282 result.extend_from_slice(&chroma);
283 let array: [Feature; NUMBER_FEATURES] = result
284 .try_into()
285 .map_err(|_| AnalysisError::InvalidFeaturesLen)?;
286 Ok(Self::new(array))
287 })
288 }
289}