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::{self, SendError},
9    thread,
10};
11
12use log::info;
13use rayon::iter::{IntoParallelIterator, ParallelIterator};
14
15use crate::{Analysis, ResampledAudio, errors::AnalysisResult};
16
17mod mecomp;
18#[allow(clippy::module_name_repetitions)]
19pub use mecomp::{MecompDecoder, SymphoniaSource};
20
21/// Trait used to implement your own decoder.
22///
23/// The `decode` function should be implemented so that it
24/// decodes and resample a song to one channel with a sampling rate of 22050 Hz
25/// and a f32le layout.
26/// Once it is implemented, several functions
27/// to perform analysis from path(s) are available, such as
28/// [`song_from_path`](Decoder::song_from_path) and
29/// [`analyze_paths`](Decoder::analyze_paths).
30#[allow(clippy::module_name_repetitions)]
31pub trait Decoder {
32    /// A function that should decode and resample a song, optionally
33    /// extracting the song's metadata such as the artist, the album, etc.
34    ///
35    /// The output sample array should be resampled to f32le, one channel, with a sampling rate
36    /// of 22050 Hz. Anything other than that will yield wrong results.
37    ///
38    /// # Errors
39    ///
40    /// This function will return an error if the file path is invalid, if
41    /// the file path points to a file containing no or corrupted audio stream,
42    /// or if the analysis could not be conducted to the end for some reason.
43    ///
44    /// The error type returned should give a hint as to whether it was a
45    /// decoding or an analysis error.
46    fn decode(&self, path: &Path) -> AnalysisResult<ResampledAudio>;
47
48    /// Returns a decoded song's `Analysis` given a file path, or an error if the song
49    /// could not be analyzed for some reason.
50    ///
51    /// # Arguments
52    ///
53    /// * `path` - A [`Path`] holding a valid file path to a valid audio file.
54    ///
55    /// # Errors
56    ///
57    /// This function will return an error if the file path is invalid, if
58    /// the file path points to a file containing no or corrupted audio stream,
59    /// or if the analysis could not be conducted to the end for some reason.
60    ///
61    /// The error type returned should give a hint as to whether it was a
62    /// decoding or an analysis error.
63    #[inline]
64    fn analyze_path<P: AsRef<Path>>(&self, path: P) -> AnalysisResult<Analysis> {
65        self.decode(path.as_ref())?.try_into()
66    }
67
68    /// Analyze songs in `paths`, and return the `Analysis` objects through an
69    /// [`mpsc::IntoIter`].
70    ///
71    /// Returns an iterator, whose items are a tuple made of
72    /// the song path (to display to the user in case the analysis failed),
73    /// and a `Result<Analysis>`.
74    #[inline]
75    fn analyze_paths<P: Into<PathBuf>, F: IntoIterator<Item = P>>(
76        &self,
77        paths: F,
78    ) -> mpsc::IntoIter<(PathBuf, AnalysisResult<Analysis>)>
79    where
80        Self: Sync + Send,
81    {
82        let cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
83        self.analyze_paths_with_cores(paths, cores)
84    }
85
86    /// Analyze songs in `paths`, and return the `Analysis` objects through an
87    /// [`mpsc::IntoIter`]. `number_cores` sets the number of cores the analysis
88    /// will use, capped by your system's capacity. Most of the time, you want to
89    /// use the simpler `analyze_paths` functions, which autodetects the number
90    /// of cores in your system.
91    ///
92    /// Return an iterator, whose items are a tuple made of
93    /// the song path (to display to the user in case the analysis failed),
94    /// and a `Result<Analysis>`.
95    fn analyze_paths_with_cores<P: Into<PathBuf>, F: IntoIterator<Item = P>>(
96        &self,
97        paths: F,
98        number_cores: NonZeroUsize,
99    ) -> mpsc::IntoIter<(PathBuf, AnalysisResult<Analysis>)>
100    where
101        Self: Sync + Send,
102    {
103        let (tx, rx) = mpsc::channel::<(PathBuf, AnalysisResult<Analysis>)>();
104        self.analyze_paths_with_cores_with_callback(paths, number_cores, tx)
105            .unwrap();
106        rx.into_iter()
107    }
108
109    /// Returns a decoded song's `Analysis` given a file path, or an error if the song
110    /// could not be analyzed for some reason.
111    ///
112    /// # Arguments
113    ///
114    /// * `path` - A [`Path`] holding a valid file path to a valid audio file.
115    /// * `callback` - A function that will be called with the path and the result of the analysis.
116    ///
117    /// # Errors
118    ///
119    /// Errors if the `callback` channel is closed.
120    #[inline]
121    fn analyze_path_with_callback<P: AsRef<Path>>(
122        &self,
123        path: P,
124        callback: mpsc::Sender<(P, AnalysisResult<Analysis>)>,
125    ) -> Result<(), SendError<()>> {
126        let song = self.analyze_path(&path);
127        callback.send((path, song)).map_err(|_| SendError(()))
128
129        // We don't need to return the result of the send, as the receiver will
130    }
131
132    /// Analyze songs in `paths`, and return the `Analysis` objects through an
133    /// [`mpsc::IntoIter`].
134    ///
135    /// Returns an iterator, whose items are a tuple made of
136    /// the song path (to display to the user in case the analysis failed),
137    /// and a `Result<Analysis>`.
138    ///
139    /// You can cancel the job by dropping the `callback` channel.
140    ///
141    /// # Errors
142    ///
143    /// Errors if the `callback` channel is closed.
144    #[inline]
145    fn analyze_paths_with_callback<P: Into<PathBuf>, I: Send + IntoIterator<Item = P>>(
146        &self,
147        paths: I,
148        callback: mpsc::Sender<(PathBuf, AnalysisResult<Analysis>)>,
149    ) -> Result<(), SendError<()>>
150    where
151        Self: Sync + Send,
152    {
153        let cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
154        self.analyze_paths_with_cores_with_callback(paths, cores, callback)
155    }
156
157    /// Analyze songs in `paths`, and return the `Analysis` objects through an
158    /// [`mpsc::IntoIter`]. `number_cores` sets the number of cores the analysis
159    /// will use, capped by your system's capacity. Most of the time, you want to
160    /// use the simpler `analyze_paths_with_callback` functions, which autodetects the number
161    /// of cores in your system.
162    ///
163    /// Return an iterator, whose items are a tuple made of
164    /// the song path (to display to the user in case the analysis failed),
165    /// and a `Result<Analysis>`.
166    ///
167    /// You can cancel the job by dropping the `callback` channel.
168    ///
169    /// # Errors
170    ///
171    /// Errors if the `callback` channel is closed.
172    fn analyze_paths_with_cores_with_callback<P: Into<PathBuf>, I: IntoIterator<Item = P>>(
173        &self,
174        paths: I,
175        number_cores: NonZeroUsize,
176        callback: mpsc::Sender<(PathBuf, AnalysisResult<Analysis>)>,
177    ) -> Result<(), SendError<()>>
178    where
179        Self: Sync + Send,
180    {
181        let mut cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
182        if cores > number_cores {
183            cores = number_cores;
184        }
185        let paths: Vec<PathBuf> = paths.into_iter().map(Into::into).collect();
186
187        if paths.is_empty() {
188            return Ok(());
189        }
190
191        let pool = rayon::ThreadPoolBuilder::new()
192            .num_threads(cores.get())
193            .build()
194            .unwrap();
195
196        pool.install(|| {
197            paths.into_par_iter().try_for_each(|path| {
198                info!("Analyzing file '{}'", path.display());
199                self.analyze_path_with_callback(path, callback.clone())
200            })
201        })
202    }
203}