mecomp_analysis/decoder/mod.rs
1#![allow(clippy::missing_inline_in_public_items)]
2
3use std::{
4 clone::Clone,
5 marker::Send,
6 num::NonZeroUsize,
7 path::{Path, PathBuf},
8 sync::mpsc,
9 thread,
10};
11
12use log::info;
13
14use crate::{errors::AnalysisResult, Analysis, ResampledAudio};
15
16mod mecomp;
17#[allow(clippy::module_name_repetitions)]
18pub use mecomp::MecompDecoder;
19
20/// Trait used to implement your own decoder.
21///
22/// The `decode` function should be implemented so that it
23/// decodes and resample a song to one channel with a sampling rate of 22050 Hz
24/// and a f32le layout.
25/// Once it is implemented, several functions
26/// to perform analysis from path(s) are available, such as
27/// [`song_from_path`](Decoder::song_from_path) and
28/// [`analyze_paths`](Decoder::analyze_paths).
29#[allow(clippy::module_name_repetitions)]
30pub trait Decoder {
31 /// A function that should decode and resample a song, optionally
32 /// extracting the song's metadata such as the artist, the album, etc.
33 ///
34 /// The output sample array should be resampled to f32le, one channel, with a sampling rate
35 /// of 22050 Hz. Anything other than that will yield wrong results.
36 ///
37 /// # Errors
38 ///
39 /// This function will return an error if the file path is invalid, if
40 /// the file path points to a file containing no or corrupted audio stream,
41 /// or if the analysis could not be conducted to the end for some reason.
42 ///
43 /// The error type returned should give a hint as to whether it was a
44 /// decoding or an analysis error.
45 fn decode(path: &Path) -> AnalysisResult<ResampledAudio>;
46
47 /// Returns a decoded song's `Analysis` given a file path, or an error if the song
48 /// could not be analyzed for some reason.
49 ///
50 /// # Arguments
51 ///
52 /// * `path` - A [`Path`] holding a valid file path to a valid audio file.
53 ///
54 /// # Errors
55 ///
56 /// This function will return an error if the file path is invalid, if
57 /// the file path points to a file containing no or corrupted audio stream,
58 /// or if the analysis could not be conducted to the end for some reason.
59 ///
60 /// The error type returned should give a hint as to whether it was a
61 /// decoding or an analysis error.
62 #[inline]
63 fn analyze_path<P: AsRef<Path>>(path: P) -> AnalysisResult<Analysis> {
64 Self::decode(path.as_ref())?.try_into()
65 }
66
67 /// Analyze songs in `paths`, and return the `Analysis` objects through an
68 /// [`mpsc::IntoIter`].
69 ///
70 /// Returns an iterator, whose items are a tuple made of
71 /// the song path (to display to the user in case the analysis failed),
72 /// and a `Result<Analysis>`.
73 #[inline]
74 fn analyze_paths<P: Into<PathBuf>, F: IntoIterator<Item = P>>(
75 paths: F,
76 ) -> mpsc::IntoIter<(PathBuf, AnalysisResult<Analysis>)> {
77 let cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
78 Self::analyze_paths_with_cores(paths, cores)
79 }
80
81 /// Analyze songs in `paths`, and return the `Analysis` objects through an
82 /// [`mpsc::IntoIter`]. `number_cores` sets the number of cores the analysis
83 /// will use, capped by your system's capacity. Most of the time, you want to
84 /// use the simpler `analyze_paths` functions, which autodetects the number
85 /// of cores in your system.
86 ///
87 /// Return an iterator, whose items are a tuple made of
88 /// the song path (to display to the user in case the analysis failed),
89 /// and a `Result<Analysis>`.
90 fn analyze_paths_with_cores<P: Into<PathBuf>, F: IntoIterator<Item = P>>(
91 paths: F,
92 number_cores: NonZeroUsize,
93 ) -> mpsc::IntoIter<(PathBuf, AnalysisResult<Analysis>)> {
94 let mut cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
95 if cores > number_cores {
96 cores = number_cores;
97 }
98 let paths: Vec<PathBuf> = paths.into_iter().map(Into::into).collect();
99 let (tx, rx) = mpsc::channel::<(PathBuf, AnalysisResult<Analysis>)>();
100 if paths.is_empty() {
101 return rx.into_iter();
102 }
103 let mut handles = Vec::new();
104 let mut chunk_length = paths.len() / cores;
105 if chunk_length == 0 {
106 chunk_length = paths.len();
107 }
108 for chunk in paths.chunks(chunk_length) {
109 let tx_thread = tx.clone();
110 let owned_chunk = chunk.to_owned();
111 let child = thread::spawn(move || {
112 for path in owned_chunk {
113 info!("Analyzing file '{:?}'", path);
114 let song = Self::analyze_path(&path);
115 tx_thread.send((path.clone(), song)).unwrap();
116 }
117 });
118 handles.push(child);
119 }
120
121 for handle in handles {
122 handle.join().unwrap();
123 }
124
125 rx.into_iter()
126 }
127}
128
129/// This trait implements functions in the [`Decoder`] trait that take a callback to run on the results.
130///
131/// It should not be implemented directly, it will be automatically implemented for any type that implements
132/// the [`Decoder`] trait.
133///
134/// Instead of sending an iterator of results, this trait sends each result over the provided channel as soon as it's ready
135#[allow(clippy::module_name_repetitions)]
136pub trait DecoderWithCallback: Decoder {
137 /// Returns a decoded song's `Analysis` given a file path, or an error if the song
138 /// could not be analyzed for some reason.
139 ///
140 /// # Arguments
141 ///
142 /// * `path` - A [`Path`] holding a valid file path to a valid audio file.
143 /// * `callback` - A function that will be called with the path and the result of the analysis.
144 ///
145 /// # Errors
146 ///
147 /// This function will return an error if the file path is invalid, if
148 /// the file path points to a file containing no or corrupted audio stream,
149 /// or if the analysis could not be conducted to the end for some reason.
150 ///
151 /// The error type returned should give a hint as to whether it was a
152 /// decoding or an analysis error.
153 #[inline]
154 fn analyze_path_with_callback<P: AsRef<Path>, CallbackState>(
155 path: P,
156 callback: mpsc::Sender<(P, AnalysisResult<Analysis>)>,
157 ) {
158 let song = Self::analyze_path(&path);
159 callback.send((path, song)).unwrap();
160
161 // We don't need to return the result of the send, as the receiver will
162 }
163
164 /// Analyze songs in `paths`, and return the `Analysis` objects through an
165 /// [`mpsc::IntoIter`].
166 ///
167 /// Returns an iterator, whose items are a tuple made of
168 /// the song path (to display to the user in case the analysis failed),
169 /// and a `Result<Analysis>`.
170 #[inline]
171 fn analyze_paths_with_callback<P: Into<PathBuf>, I: Send + IntoIterator<Item = P>>(
172 paths: I,
173 callback: mpsc::Sender<(PathBuf, AnalysisResult<Analysis>)>,
174 ) {
175 let cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
176 Self::analyze_paths_with_cores_with_callback(paths, cores, callback);
177 }
178
179 /// Analyze songs in `paths`, and return the `Analysis` objects through an
180 /// [`mpsc::IntoIter`]. `number_cores` sets the number of cores the analysis
181 /// will use, capped by your system's capacity. Most of the time, you want to
182 /// use the simpler `analyze_paths_with_callback` functions, which autodetects the number
183 /// of cores in your system.
184 ///
185 /// Return an iterator, whose items are a tuple made of
186 /// the song path (to display to the user in case the analysis failed),
187 /// and a `Result<Analysis>`.
188 fn analyze_paths_with_cores_with_callback<P: Into<PathBuf>, I: IntoIterator<Item = P>>(
189 paths: I,
190 number_cores: NonZeroUsize,
191 callback: mpsc::Sender<(PathBuf, AnalysisResult<Analysis>)>,
192 ) {
193 let mut cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
194 if cores > number_cores {
195 cores = number_cores;
196 }
197 let paths: Vec<PathBuf> = paths.into_iter().map(Into::into).collect();
198 let mut chunk_length = paths.len() / cores;
199 if chunk_length == 0 {
200 chunk_length = paths.len();
201 }
202
203 if paths.is_empty() {
204 return;
205 }
206
207 thread::scope(move |scope| {
208 let mut handles = Vec::new();
209 for chunk in paths.chunks(chunk_length) {
210 let owned_chunk = chunk.to_owned();
211
212 let tx_thread: mpsc::Sender<_> = callback.clone();
213
214 let child = scope.spawn(move || {
215 for path in owned_chunk {
216 info!("Analyzing file '{:?}'", path);
217
218 let song = Self::analyze_path(&path);
219
220 tx_thread.send((path, song)).unwrap();
221 }
222 });
223 handles.push(child);
224 }
225
226 for handle in handles {
227 handle.join().unwrap();
228 }
229 });
230 }
231}
232
233impl<T: Decoder> DecoderWithCallback for T {}