Skip to main content

songrec/fingerprinting/
algorithm.rs

1use chfft::RFft1D;
2use std::error::Error;
3use std::io::BufReader;
4use std::collections::HashMap;
5
6use crate::fingerprinting::hanning::HANNING_WINDOW_2048_MULTIPLIERS;
7use crate::fingerprinting::signature_format::{DecodedSignature, FrequencyBand, FrequencyPeak};
8
9
10pub struct SignatureGenerator {
11
12    // Used when processing input:
13
14    ring_buffer_of_samples: Vec<i16>,
15    /// Ring buffer.
16    ring_buffer_of_samples_index: usize,
17
18    reordered_ring_buffer_of_samples: Vec<f32>,
19    /// Reordered, temporary version of the ring buffer above, with floats for precision because we applied Hanning window.
20
21    fft_outputs: Vec<Vec<f32>>,
22    /// Ring buffer. Lists of 1025 floats, premultiplied with a Hanning function before being passed through FFT, computed from the ring buffer every new 128 samples
23    fft_outputs_index: usize,
24
25    fft_object: RFft1D<f32>,
26
27    spread_fft_outputs: Vec<Vec<f32>>,
28    /// Ring buffer.
29    spread_fft_outputs_index: usize,
30
31    num_spread_ffts_done: u32,
32
33    signature: DecodedSignature,
34}
35
36impl SignatureGenerator {
37    pub fn make_signature_from_file(file_path: &str) -> Result<DecodedSignature, Box<dyn Error>> {
38        // Check if file exists
39        if !std::path::Path::new(file_path).exists() {
40            return Err(format!("File not found: {}", file_path).into());
41        }
42
43        // Decode the .WAV, .MP3, .OGG or .FLAC file
44        let file = std::fs::File::open(file_path)
45            .map_err(|e| format!("Failed to open file '{}': {}", file_path, e))?;
46        
47        let decoder = rodio::Decoder::new(BufReader::new(file))
48            .map_err(|e| format!("Failed to decode audio file '{}': {}. Note: M4A/AAC format may not be fully supported on all platforms.", file_path, e))?;
49        
50        // Downsample the raw PCM samples to 16 KHz, and skip to the middle of the file
51        // in order to increase recognition odds. Take 12 seconds of sample.
52
53        use std::num::{NonZeroU16, NonZeroU32};
54        let converted_file = rodio::source::UniformSourceIterator::new(
55            decoder,
56            NonZeroU16::new(1).unwrap(),
57            NonZeroU32::new(16000).unwrap(),
58        );
59
60        let raw_pcm_samples: Vec<i16> = converted_file
61            .map(|s| (s.clamp(-1.0, 1.0) * 32767.0) as i16)
62            .collect();
63        
64        // Check if we got any samples
65        if raw_pcm_samples.is_empty() {
66            return Err(format!("No audio samples could be extracted from file '{}'. The file may be corrupted or in an unsupported format.", file_path).into());
67        }
68
69        let mut raw_pcm_samples_slice: &[i16] = &raw_pcm_samples;
70
71        let slice_len = raw_pcm_samples_slice.len().min(12 * 16000);
72        
73        // Check if we have enough samples for fingerprinting (at least 3 seconds)
74        if slice_len < 3 * 16000 {
75            return Err(format!("Audio file '{}' is too short for fingerprinting. Need at least 3 seconds of audio, but only got {:.2} seconds.", 
76                file_path, slice_len as f32 / 16000.0).into());
77        }
78
79        if raw_pcm_samples_slice.len() > 12 * 16000 {
80            let middle = raw_pcm_samples.len() / 2;
81
82            raw_pcm_samples_slice = &raw_pcm_samples_slice[middle - (6 * 16000)..middle + (6 * 16000)];
83        }
84
85        Ok(SignatureGenerator::make_signature_from_buffer(&raw_pcm_samples_slice[..slice_len]))
86    }
87
88    pub fn make_signature_from_buffer(s16_mono_16khz_buffer: &[i16]) -> DecodedSignature {
89        let mut this = SignatureGenerator {
90            ring_buffer_of_samples: vec![0i16; 2048],
91            ring_buffer_of_samples_index: 0,
92
93            reordered_ring_buffer_of_samples: vec![0.0f32; 2048],
94
95            fft_outputs: vec![vec![0.0f32; 1025]; 256],
96            fft_outputs_index: 0,
97
98            fft_object: RFft1D::<f32>::new(2048),
99
100            spread_fft_outputs: vec![vec![0.0f32; 1025]; 256],
101            spread_fft_outputs_index: 0,
102
103            num_spread_ffts_done: 0,
104
105            signature: DecodedSignature {
106                sample_rate_hz: 16000,
107                number_samples: s16_mono_16khz_buffer.len() as u32,
108                frequency_band_to_sound_peaks: HashMap::new(),
109            },
110        };        for chunk in s16_mono_16khz_buffer.chunks_exact(128) {
111            this.do_fft_internal(chunk);
112
113            this.do_peak_spreading();
114
115            this.num_spread_ffts_done += 1;
116
117            if this.num_spread_ffts_done >= 46 {
118                this.do_peak_recognition();
119            }
120        }
121
122        this.signature
123    }
124
125    /// Create a new SignatureGenerator instance for streaming recognition
126    pub fn new() -> Self {
127        Self {
128            ring_buffer_of_samples: vec![0i16; 2048],
129            ring_buffer_of_samples_index: 0,
130            reordered_ring_buffer_of_samples: vec![0.0f32; 2048],
131            fft_outputs: vec![vec![0.0f32; 1025]; 256],
132            fft_outputs_index: 0,
133            fft_object: RFft1D::<f32>::new(2048),
134            spread_fft_outputs: vec![vec![0.0f32; 1025]; 256],
135            spread_fft_outputs_index: 0,
136            num_spread_ffts_done: 0,
137            signature: DecodedSignature {
138                sample_rate_hz: 16000,
139                number_samples: 0,
140                frequency_band_to_sound_peaks: HashMap::new(),
141            },
142        }
143    }
144
145    /// Process audio samples and update the signature
146    /// This is a public version of do_fft that also updates sample count
147    pub fn do_fft(&mut self, s16_mono_16khz_buffer: &[i16], sample_rate: u32) {
148        // Update sample count
149        self.signature.number_samples += s16_mono_16khz_buffer.len() as u32;
150        self.signature.sample_rate_hz = sample_rate;
151
152        // Call the internal FFT processing
153        self.do_fft_internal(s16_mono_16khz_buffer);
154        
155        self.do_peak_spreading();
156        self.num_spread_ffts_done += 1;
157
158        if self.num_spread_ffts_done >= 46 {
159            self.do_peak_recognition();
160        }
161    }
162
163    /// Get the current signature
164    pub fn get_signature(&self) -> DecodedSignature {
165        self.signature.clone()
166    }
167
168    fn do_fft_internal(&mut self, s16_mono_16khz_buffer: &[i16]) {
169
170        // Copy the 128 input s16le samples to the local ring buffer
171
172        self.ring_buffer_of_samples[self.ring_buffer_of_samples_index..self.ring_buffer_of_samples_index + 128].copy_from_slice(s16_mono_16khz_buffer);
173
174        self.ring_buffer_of_samples_index += 128;
175        self.ring_buffer_of_samples_index &= 2047;
176
177        // Reorder the items (put the latest data at end) and apply Hanning window
178
179        for (index, multiplier) in HANNING_WINDOW_2048_MULTIPLIERS.iter().enumerate() {
180            self.reordered_ring_buffer_of_samples[index] =
181                self.ring_buffer_of_samples[(index + self.ring_buffer_of_samples_index) & 2047] as f32 *
182                    multiplier;
183        }
184
185        // Perform Fast Fourier transform
186
187        let complex_fft_results = self.fft_object.forward(&self.reordered_ring_buffer_of_samples);
188
189        assert_eq!(complex_fft_results.len(), 1025);
190
191        // Turn complex into reals, and put the results into a local array
192
193        let real_fft_results = &mut self.fft_outputs[self.fft_outputs_index];
194
195        for index in 0..=1024 {
196            real_fft_results[index] = (
197                (
198                    complex_fft_results[index].re.powi(2) +
199                        complex_fft_results[index].im.powi(2)
200                ) / ((1 << 17) as f32)
201            ).max(0.0000000001);
202        }
203
204        self.fft_outputs_index += 1;
205        self.fft_outputs_index &= 255;
206    }
207
208    fn do_peak_spreading(&mut self) {
209        let real_fft_results = &self.fft_outputs[((self.fft_outputs_index as i32 - 1) & 255) as usize];
210
211        let spread_fft_results = &mut self.spread_fft_outputs[self.spread_fft_outputs_index];
212
213        // Perform frequency-domain spreading of peak values
214
215        spread_fft_results.copy_from_slice(real_fft_results);
216
217        for position in 0..=1022 {
218            spread_fft_results[position] = spread_fft_results[position]
219                .max(spread_fft_results[position + 1])
220                .max(spread_fft_results[position + 2]);
221        }
222
223        // Perform time-domain spreading of peak values
224
225        let spread_fft_results_copy = spread_fft_results.clone(); // Avoid mutable+mutable borrow of self.spread_fft_outputs
226
227        for position in 0..=1024 {
228            for former_fft_number in &[1, 3, 6] {
229                let former_fft_output = &mut self.spread_fft_outputs[((self.spread_fft_outputs_index as i32 - *former_fft_number) & 255) as usize];
230
231                former_fft_output[position] = former_fft_output[position]
232                    .max(spread_fft_results_copy[position]);
233            }
234        }
235
236        self.spread_fft_outputs_index += 1;
237        self.spread_fft_outputs_index &= 255;
238    }
239
240    fn do_peak_recognition(&mut self) {
241
242        // Note: when substracting an array index, casting to signed is needed
243        // to avoid underflow panics at runtime.
244
245        let fft_minus_46 = &self.fft_outputs[((self.fft_outputs_index as i32 - 46) & 255) as usize];
246        let fft_minus_49 = &self.spread_fft_outputs[((self.spread_fft_outputs_index as i32 - 49) & 255) as usize];
247
248        for bin_position in 10..=1014 {
249
250            // Ensure that the bin is large enough to be a peak
251
252            if fft_minus_46[bin_position] >= 1.0 / 64.0 &&
253                fft_minus_46[bin_position] >= fft_minus_49[bin_position - 1] {
254
255                // Ensure that it is frequency-domain local minimum
256
257                let mut max_neighbor_in_fft_minus_49: f32 = 0.0;
258
259                for neighbor_offset in &[-10, -7, -4, -3, 1, 2, 5, 8] {
260                    max_neighbor_in_fft_minus_49 = max_neighbor_in_fft_minus_49
261                        .max(fft_minus_49[(bin_position as i32 + *neighbor_offset) as usize]);
262                }
263
264                if fft_minus_46[bin_position] > max_neighbor_in_fft_minus_49 {
265
266                    // Ensure that it is a time-domain local minimum
267
268                    let mut max_neighbor_in_other_adjacent_ffts = max_neighbor_in_fft_minus_49;
269
270                    for other_offset in &[-53, -45,
271                        165, 172, 179, 186, 193, 200,
272                        214, 221, 228, 235, 242, 249] {
273                        let other_fft = &self.spread_fft_outputs[((self.spread_fft_outputs_index as i32 + other_offset) & 255) as usize];
274
275                        max_neighbor_in_other_adjacent_ffts = max_neighbor_in_other_adjacent_ffts
276                            .max(other_fft[bin_position - 1]);
277                    }
278
279                    if fft_minus_46[bin_position] > max_neighbor_in_other_adjacent_ffts {
280
281                        // This is a peak, store the peak
282
283                        let fft_pass_number = self.num_spread_ffts_done - 46;
284
285                        let peak_magnitude: f32 = fft_minus_46[bin_position].ln().max(1.0 / 64.0) * 1477.3 + 6144.0;
286                        let peak_magnitude_before: f32 = fft_minus_46[bin_position - 1].ln().max(1.0 / 64.0) * 1477.3 + 6144.0;
287                        let peak_magnitude_after: f32 = fft_minus_46[bin_position + 1].ln().max(1.0 / 64.0) * 1477.3 + 6144.0;
288
289                        let peak_variation_1: f32 = peak_magnitude * 2.0 - peak_magnitude_before - peak_magnitude_after;
290                        let peak_variation_2: f32 = (peak_magnitude_after - peak_magnitude_before) * 32.0 / peak_variation_1;
291
292                        let corrected_peak_frequency_bin: u16 = ((bin_position as i32 * 64) + (peak_variation_2 as i32)) as u16;
293
294                        assert!(peak_variation_1 >= 0.0);
295
296                        // Convert back a FFT bin to a frequency, given a 16 KHz sample
297                        // rate, 1024 useful bins and the multiplication by 64 made before
298                        // storing the information
299
300                        let frequency_hz: f32 = corrected_peak_frequency_bin as f32 * (16000.0 / 2.0 / 1024.0 / 64.0);
301
302                        // Ignore peaks outside the 250 Hz-5.5 KHz range, store them into
303                        // a lookup table that will be used to generate the binary fingerprint
304                        // otherwise
305
306                        let frequency_band = match frequency_hz as i32 {
307                            250..=519 => FrequencyBand::_250_520,
308                            520..=1449 => FrequencyBand::_520_1450,
309                            1450..=3499 => FrequencyBand::_1450_3500,
310                            3500..=5500 => FrequencyBand::_3500_5500,
311                            _ => { continue; }
312                        };
313
314                        // In Rust, the entry method returns an Entry object,
315                        // which represents a cell in a HashMap that is either occupied or vacant.
316                        // You can use or_default to insert a value if the key is missing,
317                        // which avoids a double search of the key in the hash map.
318                        self.signature.frequency_band_to_sound_peaks
319                            .entry(frequency_band)
320                            .or_default();
321
322                        self.signature.frequency_band_to_sound_peaks.get_mut(&frequency_band).unwrap().push(
323                            FrequencyPeak {
324                                fft_pass_number,
325                                peak_magnitude: peak_magnitude as u16,
326                                corrected_peak_frequency_bin
327                            }
328                        );
329                    }
330                }
331            }
332        }
333    }
334}