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 {}