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}