rusty-chromaprint-0.3.0/.cargo_vcs_info.json0000644000000001510000000000100145430ustar { "git": { "sha1": "97d67dfed02854ac5d5eaa9ac965b9abe3dff690" }, "path_in_vcs": "chromaprint" }rusty-chromaprint-0.3.0/Cargo.lock0000644000000053370000000000100125310ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "num-complex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" dependencies = [ "num-traits", ] [[package]] name = "num-integer" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", ] [[package]] name = "num-traits" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] [[package]] name = "primal-check" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9df7f93fd637f083201473dab4fee2db4c429d32e55e3299980ab3957ab916a0" dependencies = [ "num-integer", ] [[package]] name = "realfft" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953d9f7e5cdd80963547b456251296efc2626ed4e3cbf36c869d9564e0220571" dependencies = [ "rustfft", ] [[package]] name = "rubato" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0fe3acbd4cc7c6d726def76dcfd77164c35a65e034256de2741db8ead9a4ae5" dependencies = [ "num-complex", "num-integer", "num-traits", "realfft", ] [[package]] name = "rustfft" version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86" dependencies = [ "num-complex", "num-integer", "num-traits", "primal-check", "strength_reduce", "transpose", "version_check", ] [[package]] name = "rusty-chromaprint" version = "0.3.0" dependencies = [ "rubato", "rustfft", ] [[package]] name = "strength_reduce" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" [[package]] name = "transpose" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6522d49d03727ffb138ae4cbc1283d3774f0d10aa7f9bf52e6784c45daf9b23" dependencies = [ "num-integer", "strength_reduce", ] [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" rusty-chromaprint-0.3.0/Cargo.toml0000644000000020670000000000100125510ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.63" name = "rusty-chromaprint" version = "0.3.0" build = false exclude = ["data/**"] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Pure Rust port of Chromaprint" homepage = "https://github.com/darksv/rusty-chromaprint" readme = "README.md" license = "MIT" repository = "https://github.com/darksv/rusty-chromaprint" [lib] name = "rusty_chromaprint" path = "src/lib.rs" [[example]] name = "test" path = "examples/test.rs" [dependencies.rubato] version = "0.16.0" [dependencies.rustfft] version = "6.2.0" rusty-chromaprint-0.3.0/Cargo.toml.orig000064400000000000000000000006011046102023000162220ustar 00000000000000[package] name = "rusty-chromaprint" version = "0.3.0" edition = "2021" description = "Pure Rust port of Chromaprint" license = "MIT" homepage = "https://github.com/darksv/rusty-chromaprint" repository = "https://github.com/darksv/rusty-chromaprint" readme = "../README.md" rust-version = "1.63" exclude = ["data/**"] [dependencies] rustfft = "6.2.0" rubato = "0.16.0" rusty-chromaprint-0.3.0/README.md000064400000000000000000000047051046102023000146230ustar 00000000000000# rusty-chromaprint This is an in-progress port of [chromaprint](https://github.com/acoustid/chromaprint): > Chromaprint is an audio fingerprint library developed for the [AcoustID](https://acoustid.org/) project. It's designed to identify near-identical audio and the fingerprints it generates are as compact as possible to achieve that. It's not a general purpose audio fingerprinting solution. It trades precision and robustness for search performance. The target use cases are full audio file identification, duplicate audio file detection and long audio stream monitoring. ## Usage To calculate a fingerprint for an audio stream simply create a new `Fingerprinter` and give it all the audio samples: ```rust use rusty_chromaprint::{Configuration, Fingerprinter}; fn main() { // Use a preset configuration. This must be always the same for the audio fingerprints // that are going to be compared against each other. let mut printer = Fingerprinter::new(&Configuration::preset_test2()); // Sampling rate is set to 44100 and stream has 2 audio channels. It is expected that samples // are interleaved: in this case left channel samples are placed at even indices // and right channel - at odd ones. printer.start(44100, 2).unwrap(); // Process a few samples... printer.consume(&[-100, -100, -50, -50, 1000, 1000]); // ... and add some more... printer.consume(&more_samples); // Make sure that all the sample are processed. printer.finish(); // Get the fingerprint. let fingerprint = printer.fingerprint(); println!("fingerprint = {:08x?}", &fingerprint); } ``` For a complete example check out [`compare`](https://github.com/darksv/rusty-chromaprint/blob/main/compare/src/main.rs) from this repository which is using [Symphonia](https://github.com/pdeljanov/Symphonia) to decode various audio formats. It compares two files and prints out their common segments ``` cargo run --release --bin compare -- audio1.mp3 audio2.wav ``` ``` # | File 1 | File 2 | Duration | Score -----+--------------------------+--------------------------+------------+--------- 1 | 0:00:04.83 -- 0:00:19.44 | 0:00:00.00 -- 0:00:14.61 | 0:00:14.61 | 0.69 ``` For more details on comparing audio fingerprints reach out to the [documentation](https://docs.rs/rusty-chromaprint/latest/rusty_chromaprint/fn.match_fingerprints.html). rusty-chromaprint-0.3.0/examples/test.rs000064400000000000000000000022661046102023000165070ustar 00000000000000use std::path::Path; use rusty_chromaprint::{Configuration, Fingerprinter}; fn read_s16le(path: impl AsRef) -> Vec { std::fs::read(path) .unwrap() .chunks_exact(2) .map(|chunk| i16::from_le_bytes([chunk[0], chunk[1]])) .collect::>() } fn main() { let mut printer = Fingerprinter::new(&Configuration::preset_test1()); printer.start(11025, 2).unwrap(); printer.consume(&read_s16le("data/test_stereo_44100.raw")); printer.finish(); assert_eq!( printer.fingerprint(), &[ 3086176501, 3077772469, 3077638581, 3052408789, 3048228821, 3046201301, 3042148311, 3037102035, 2969993073, 3041294129, 3045483313, 3046514967, 3050712326, 3040164098, 3040163847, 3073719559, 3073733965, 3212169693, 3212169693, 3220542455, 3220542399, 3212152503, 3077933717, 3086327509, 3080034295, 4120237047, 4119197543, 4119295527, 4123424293, 1975934501, 2110152245, 2111233559, 2144501255, 1005778439, 1001636359, 1005683463, 1005682948, 1005686104, 991003132, 991031785, 995223531, 995190635, 1003562858 ] ); } rusty-chromaprint-0.3.0/src/audio_processor.rs000064400000000000000000000232271046102023000177010ustar 00000000000000use std::fmt::{Display, Formatter}; use rubato::Resampler; use crate::stages::{AudioConsumer, Stage}; const MIN_SAMPLE_RATE: u32 = 1000; const MAX_BUFFER_SIZE: usize = 1024 * 32; pub struct AudioProcessor> { buffer: Box<[i16]>, buffer_offset: usize, output_buffer: Vec, input: Vec, channels: u32, consumer: C, target_sample_rate: u32, resampler: Option>, } impl> AudioProcessor { pub(crate) fn new(target_sample_rate: u32, consumer: C) -> Self { Self { buffer: vec![0; MAX_BUFFER_SIZE].into_boxed_slice(), buffer_offset: 0, output_buffer: Vec::new(), input: Vec::new(), channels: 0, consumer, target_sample_rate, resampler: None, } } fn load(&mut self, input: &[i16], channels: usize) -> usize { assert!(self.buffer_offset <= self.buffer.len()); assert_eq!(input.len() % channels, 0); let available_samples = input.len() / channels; let consumed = available_samples.min(self.available_space()); let input = &input[..consumed * channels]; match channels { 1 => { for sample in input.iter().copied() { self.push_sample(sample); } } 2 => { for sample in input.chunks_exact(2) { self.push_sample(((i32::from(sample[0]) + i32::from(sample[1])) / 2) as i16); } } _ => { for sample in input.chunks_exact(channels) { let sum: i32 = sample.iter().copied().map(i32::from).sum(); let samples: i32 = sample.len().try_into().unwrap(); let average: i32 = sum / samples; self.push_sample(average.try_into().unwrap()); } } } consumed * channels } fn resample(&mut self, is_end: bool) { for &sample in &self.buffer[..self.buffer_offset] { self.input.push(f64::from(sample) / f64::from(i16::MAX)); } self.buffer_offset = 0; if let Some(resampler) = self.resampler.as_mut() { let default_input_frames = resampler.input_frames_next(); while !self.input.is_empty() { if self.input.len() < resampler.input_frames_next() { if is_end { // Update chunk size to accept the remaining samples resampler .set_chunk_size(self.input.len()) .expect("cannot update chunk size for the resampler"); } else { break; } } let required_input = resampler.input_frames_next(); self.output_buffer .resize(resampler.output_frames_next(), 0.0); let (read_samples, written_samples) = resampler .process_into_buffer( &[&self.input[..required_input]], std::slice::from_mut(&mut self.output_buffer), None, ) .expect("invalid parameters for resampler"); self.input.drain(..read_samples); self.consumer .consume(&self.output_buffer[..written_samples]); if is_end { resampler .set_chunk_size(default_input_frames) .expect("cannot restore chunk size for the resampler"); } } } else { self.consumer.consume(&self.input); self.input.clear(); } } fn available_space(&self) -> usize { self.buffer.len() - self.buffer_offset } #[inline] fn push_sample(&mut self, value: i16) { self.buffer[self.buffer_offset] = value; self.buffer_offset += 1; } pub(crate) fn reset(&mut self, sample_rate: u32, channels: u32) -> Result<(), ResetError> { if channels == 0 { return Err(ResetError::NoChannels); } if sample_rate <= MIN_SAMPLE_RATE { return Err(ResetError::SampleRateTooLow); } self.channels = channels; self.buffer_offset = 0; self.consumer.reset(); if self.target_sample_rate != sample_rate { let resampler = rubato::SincFixedIn::new( self.target_sample_rate as f64 / sample_rate as f64, 1.0, rubato::SincInterpolationParameters { sinc_len: 16, f_cutoff: 0.8, oversampling_factor: 128, interpolation: rubato::SincInterpolationType::Nearest, window: rubato::WindowFunction::Blackman, }, MAX_BUFFER_SIZE, 1, )?; self.output_buffer .resize(resampler.output_frames_max(), 0.0); self.resampler = Some(resampler); } Ok(()) } pub(crate) fn flush(&mut self) { if self.buffer_offset > 0 { self.resample(true); } self.consumer.flush(); } } impl> Stage for AudioProcessor { type Output = C::Output; fn output(&self) -> &Self::Output { self.consumer.output() } } impl> AudioConsumer for AudioProcessor { fn reset(&mut self) { todo!(); } fn consume(&mut self, data: &[i16]) { assert_eq!(data.len() % self.channels as usize, 0); let mut index = 0; while index < data.len() { index += self.load(&data[index..], self.channels as usize); if self.buffer.len() == self.buffer_offset { // Full buffer self.resample(false); } } } fn flush(&mut self) {} } #[derive(Debug)] pub enum ResetError { SampleRateTooLow, NoChannels, CannotResample(rubato::ResamplerConstructionError), } impl From for ResetError { fn from(e: rubato::ResamplerConstructionError) -> Self { ResetError::CannotResample(e) } } impl Display for ResetError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { ResetError::SampleRateTooLow => writeln!( f, "Sample rate is too low. Required min. {}", MIN_SAMPLE_RATE ), ResetError::NoChannels => writeln!(f, "At least one channel is required"), ResetError::CannotResample(e) => writeln!(f, "Cannot resample: {}", e), } } } impl std::error::Error for ResetError {} #[cfg(test)] mod tests { use crate::assert_eq_float_slice; use crate::audio_processor::{AudioConsumer, AudioProcessor, Stage}; use crate::utils::read_s16le; fn i16_to_f64(s: &[i16]) -> Vec { s.iter() .copied() .map(|x| (x as f64) / (i16::MAX as f64)) .collect::>() } #[test] fn pass_through() { let data = read_s16le("data/test_mono_44100.raw"); let mut processor = AudioProcessor::new(44100, AudioBuffer::new()); processor.reset(44100, 1).unwrap(); processor.consume(&data); processor.flush(); assert_eq_float_slice!(processor.output(), i16_to_f64(&data)); } #[test] #[ignore] fn mono() { let data1 = read_s16le("data/test_mono_44100.raw"); let data2 = read_s16le("data/test_mono_11025.raw"); let mut processor = AudioProcessor::new(11025, AudioBuffer::new()); processor.reset(44100, 1).unwrap(); processor.consume(&data1); processor.flush(); assert_eq_float_slice!(processor.output(), i16_to_f64(&data2)); } #[test] #[ignore] fn mono_non_integer() { let data1 = read_s16le("data/test_mono_44100.raw"); let data2 = read_s16le("data/test_mono_8000.raw"); let mut processor = AudioProcessor::new(8000, AudioBuffer::new()); processor.reset(44100, 1).unwrap(); processor.consume(&data1); processor.flush(); assert_eq_float_slice!(processor.output(), i16_to_f64(&data2)); } #[test] fn stereo_to_mono() { let data1 = read_s16le("data/test_mono_44100.raw"); let data2 = read_s16le("data/test_stereo_44100.raw"); let mut processor = AudioProcessor::new(44100, AudioBuffer::new()); processor.reset(44100, 2).unwrap(); processor.consume(&data2); processor.flush(); assert_eq_float_slice!(processor.output(), i16_to_f64(&data1)); } struct AudioBuffer { data: Vec, } impl AudioBuffer { fn new() -> Self { Self { data: Vec::new() } } } impl Stage for AudioBuffer { type Output = [T]; fn output(&self) -> &Self::Output { self.data.as_slice() } } impl AudioConsumer for AudioBuffer { fn reset(&mut self) { self.data.clear(); } fn consume(&mut self, data: &[T]) { self.data.extend_from_slice(data); } fn flush(&mut self) {} } } rusty-chromaprint-0.3.0/src/chroma.rs000064400000000000000000000165401046102023000157520ustar 00000000000000use crate::stages::{FeatureVectorConsumer, Stage}; pub(crate) struct Chroma { interpolate: bool, notes: Box<[u8]>, notes_frac: Box<[f64]>, min_index: usize, max_index: usize, features: [f64; NUM_BANDS], consumer: C, } const NUM_BANDS: usize = 12; impl Chroma { pub(crate) fn new( min_freq: u32, max_freq: u32, frame_size: usize, sample_rate: u32, consumer: C, ) -> Self { let mut chroma = Self { interpolate: false, notes: vec![0; frame_size].into_boxed_slice(), notes_frac: vec![0.0; frame_size].into_boxed_slice(), min_index: 0, max_index: 0, features: [0.0; NUM_BANDS], consumer, }; chroma.prepare_notes(min_freq, max_freq, frame_size, sample_rate); chroma } fn prepare_notes(&mut self, min_freq: u32, max_freq: u32, frame_size: usize, sample_rate: u32) { self.min_index = freq_to_index(min_freq, frame_size, sample_rate).max(1); self.max_index = freq_to_index(max_freq, frame_size, sample_rate).min(frame_size / 2); for i in self.min_index..self.max_index { let freq = index_to_freq(i, frame_size, sample_rate); let octave = freq_to_octave(freq); let note = NUM_BANDS as f64 * (octave - octave.floor()); self.notes[i] = note.floor() as u8; self.notes_frac[i] = note - note.floor(); } } } impl Stage for Chroma { type Output = C::Output; fn output(&self) -> &Self::Output { self.consumer.output() } } impl FeatureVectorConsumer for Chroma { fn consume(&mut self, frame: &[f64]) { self.features.fill(0.0); for (i, energy) in frame .iter() .enumerate() .take(self.max_index) .skip(self.min_index) { let note = self.notes[i] as usize; if self.interpolate { let mut note2 = note; let mut a = 1.0; if self.notes_frac[i] < 0.5 { note2 = (note + NUM_BANDS - 1) % NUM_BANDS; a = 0.5 + self.notes_frac[i]; } if self.notes_frac[i] > 0.5 { note2 = (note + 1) % NUM_BANDS; a = 1.5 - self.notes_frac[i]; } self.features[note] += energy * a; self.features[note2] += energy * (1.0 - a); } else { self.features[note] += energy; } } self.consumer.consume(&self.features); } fn reset(&mut self) { self.consumer.reset(); } } fn freq_to_index(freq: u32, frame_size: usize, sample_rate: u32) -> usize { (frame_size as f64 * freq as f64 / sample_rate as f64).round() as usize } fn index_to_freq(i: usize, frame_size: usize, sample_rate: u32) -> f64 { (i as f64) * sample_rate as f64 / frame_size as f64 } fn freq_to_octave(freq: f64) -> f64 { let base = 440.0 / 16.0; f64::log2(freq / base) } #[cfg(test)] mod tests { use crate::assert_eq_float; use crate::chroma::{Chroma, FeatureVectorConsumer}; use crate::stages::Stage; #[test] fn normal_a() { let mut chroma = Chroma::new(10, 510, 256, 1000, FeatureVectorBuffer::new()); let mut frame = vec![0.0; 128]; frame[113] = 1.0; chroma.consume(&frame); let features = chroma.output(); assert_eq!(12, features.len()); let expected_features = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; for i in 0..12 { assert_eq_float!(expected_features[i], features[i], 0.0001); } } #[test] fn normal_gsharp() { let mut chroma = Chroma::new(10, 510, 256, 1000, FeatureVectorBuffer::new()); let mut frame = vec![0.0; 128]; frame[112] = 1.0; chroma.consume(&frame); let features = chroma.output(); assert_eq!(12, features.len()); let expected_features = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]; for i in 0..12 { assert_eq_float!(expected_features[i], features[i], 0.0001); } } #[test] fn normal_b() { let mut chroma = Chroma::new(10, 510, 256, 1000, FeatureVectorBuffer::new()); let mut frame = vec![0.0; 128]; frame[64] = 1.0; chroma.consume(&frame); let features = chroma.output(); assert_eq!(12, features.len()); let expected_features = [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; for i in 0..12 { assert_eq_float!(expected_features[i], features[i], 0.0001); } } #[test] fn interpolated_a() { let mut frame = vec![0.0; 128]; frame[113] = 1.0; let mut chroma = Chroma::new(10, 510, 256, 1000, FeatureVectorBuffer::new()); chroma.interpolate = true; chroma.consume(&frame); let features = chroma.output(); assert_eq!(12, features.len()); let expected_features = [ 0.555242, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.444758, ]; for i in 0..12 { assert_eq_float!(expected_features[i], features[i], 0.0001); } } #[test] fn interpolated_gsharp() { let mut frame = vec![0.0; 128]; frame[112] = 1.0; let mut chroma = Chroma::new(10, 510, 256, 1000, FeatureVectorBuffer::new()); chroma.interpolate = true; chroma.consume(&frame); let features = chroma.output(); assert_eq!(12, features.len()); let expected_features = [ 0.401354, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.598646, ]; for i in 0..12 { assert_eq_float!(expected_features[i], features[i], 0.0001); } } #[test] fn interpolated_b() { let mut frame = vec![0.0; 128]; frame[64] = 1.0; let mut chroma = Chroma::new(10, 510, 256, 1000, FeatureVectorBuffer::new()); chroma.interpolate = true; chroma.consume(&frame); let features = chroma.output(); assert_eq!(12, features.len()); let expected_features = [ 0.0, 0.286905, 0.713095, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ]; for i in 0..12 { assert_eq_float!(expected_features[i], features[i], 0.0001); } } struct FeatureVectorBuffer { features: Vec, } impl FeatureVectorBuffer { fn new() -> Self { Self { features: vec![] } } } impl Stage for FeatureVectorBuffer { type Output = [f64]; fn output(&self) -> &Self::Output { self.features.as_slice() } } impl FeatureVectorConsumer for FeatureVectorBuffer { fn consume(&mut self, features: &[f64]) { self.features.clear(); self.features.extend_from_slice(features); } fn reset(&mut self) { self.features.clear(); } } } rusty-chromaprint-0.3.0/src/chroma_filter.rs000064400000000000000000000121521046102023000173120ustar 00000000000000use crate::stages::{FeatureVectorConsumer, Stage}; pub struct ChromaFilter { coefficients: Box<[f64]>, consumer: C, buffer: [[f64; 12]; 8], result: [f64; 12], buffer_offset: usize, buffer_size: usize, } impl ChromaFilter { pub(crate) fn new(coefficients: Box<[f64]>, consumer: C) -> Self { Self { coefficients, consumer, buffer: std::array::from_fn(|_| [0.0; 12]), result: [0.0; 12], buffer_offset: 0, buffer_size: 1, } } } impl Stage for ChromaFilter { type Output = C::Output; fn output(&self) -> &Self::Output { self.consumer.output() } } impl FeatureVectorConsumer for ChromaFilter { fn consume(&mut self, features: &[f64]) { self.buffer[self.buffer_offset].copy_from_slice(features); self.buffer_offset = (self.buffer_offset + 1) % self.buffer.len(); if self.buffer_size >= self.coefficients.len() { let offset = (self.buffer_offset + self.buffer.len() - self.coefficients.len()) % self.buffer.len(); self.result.fill(0.0); for i in 0..self.result.len() { for j in 0..self.coefficients.len() { self.result[i] += self.buffer[(offset + j) % self.buffer.len()][i] * self.coefficients[j]; } } self.consumer.consume(&self.result); } else { self.buffer_size += 1; } } fn reset(&mut self) { self.buffer_size = 1; self.buffer_offset = 0; } } #[cfg(test)] mod tests { use crate::assert_eq_float; use crate::chroma_filter::ChromaFilter; use crate::stages::{FeatureVectorConsumer, Stage}; #[test] fn blur2() { let coefficients = [0.5, 0.5]; let mut image = Image::new(12); let mut filter = ChromaFilter::new(coefficients.into(), &mut image); let d1 = [0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; let d2 = [1.0, 6.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; let d3 = [2.0, 7.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; filter.consume(&d1); filter.consume(&d2); filter.consume(&d3); assert_eq!(2, image.rows()); assert_eq!(0.5, image.get(0, 0)); assert_eq!(1.5, image.get(1, 0)); assert_eq!(5.5, image.get(0, 1)); assert_eq!(6.5, image.get(1, 1)); } #[test] fn blur3() { let coefficients = [0.5, 0.7, 0.5]; let mut image = Image::new(12); let mut filter = ChromaFilter::new(coefficients.into(), &mut image); let d1 = [0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; let d2 = [1.0, 6.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; let d3 = [2.0, 7.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; let d4 = [3.0, 8.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; filter.consume(&d1); filter.consume(&d2); filter.consume(&d3); filter.consume(&d4); assert_eq!(2, image.rows()); assert_eq_float!(1.7, image.get(0, 0)); assert_eq_float!(3.399999999999999, image.get(1, 0)); assert_eq_float!(10.199999999999999, image.get(0, 1)); assert_eq_float!(11.899999999999999, image.get(1, 1)); } #[test] fn diff() { let coefficients = [1.0, -1.0]; let mut image = Image::new(12); let mut filter = ChromaFilter::new(coefficients.into(), &mut image); let d1 = [0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; let d2 = [1.0, 6.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; let d3 = [2.0, 7.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; filter.consume(&d1); filter.consume(&d2); filter.consume(&d3); assert_eq!(2, image.rows()); assert_eq!(-1.0, image.get(0, 0)); assert_eq!(-1.0, image.get(1, 0)); assert_eq!(-1.0, image.get(0, 1)); assert_eq!(-1.0, image.get(1, 1)); } struct Image { columns: usize, data: Vec, } impl Image { fn new(columns: usize) -> Self { Self { columns, data: vec![], } } fn rows(&self) -> usize { self.data.len() / self.columns } fn get(&self, row: usize, col: usize) -> f64 { self.data[row * self.columns + col] } } impl Stage for Image { type Output = [f64]; fn output(&self) -> &Self::Output { self.data.as_slice() } } impl FeatureVectorConsumer for Image { fn consume(&mut self, features: &[f64]) { self.data.extend_from_slice(features); } fn reset(&mut self) { self.data.clear(); } } } rusty-chromaprint-0.3.0/src/chroma_normalizer.rs000064400000000000000000000040701046102023000202070ustar 00000000000000use crate::stages::{FeatureVectorConsumer, Stage}; pub struct ChromaNormalizer { consumer: C, } impl ChromaNormalizer { pub(crate) fn new(consumer: C) -> Self { Self { consumer } } } impl Stage for ChromaNormalizer { type Output = C::Output; fn output(&self) -> &Self::Output { self.consumer.output() } } impl FeatureVectorConsumer for ChromaNormalizer { fn consume(&mut self, features: &[f64]) { let mut features = features.to_vec(); normalize(&mut features, 0.01); self.consumer.consume(&features); } fn reset(&mut self) { self.consumer.reset(); } } fn normalize(values: &mut [f64], eps: f64) { let norm = values.iter().fold(0.0, |acc, &x| acc + x.powi(2)).sqrt(); if norm < eps { values.fill(0.0); } else { for x in values { *x /= norm; } } } #[cfg(test)] mod tests { use crate::assert_eq_float; use crate::chroma_normalizer::normalize; #[test] fn normalize_vector() { let data = [0.1, 0.2, 0.4, 1.0]; let normalized = [0.090909, 0.181818, 0.363636, 0.909091]; let mut normalized_data = data; normalize(&mut normalized_data, 0.01); for i in 0..4 { assert_eq_float!(normalized_data[i], normalized[i], 1e-5); } } #[test] fn normalize_vector_near_zero() { let data = [0.0, 0.001, 0.002, 0.003]; let mut normalized_data = data; normalize(&mut normalized_data, 0.01); for i in 0..4 { assert_eq_float!(normalized_data[i], 0.0, 1e-5); } } #[test] fn normalize_vector_zero() { let data = [0.0, 0.0, 0.0, 0.0]; let mut normalized_data = data; normalize(&mut normalized_data, 0.01); for i in 0..4 { assert_eq_float!(normalized_data[i], 0.0, 1e-5); } } } rusty-chromaprint-0.3.0/src/classifier.rs000064400000000000000000000011001046102023000166070ustar 00000000000000use crate::filter::{Filter, Image}; use crate::quantize::Quantizer; #[derive(Debug, Clone, Copy)] pub struct Classifier { filter: Filter, quantizer: Quantizer, } impl Classifier { pub const fn new(filter: Filter, quantizer: Quantizer) -> Self { Self { filter, quantizer } } pub(crate) fn classify(&self, image: &impl Image, offset: usize) -> u32 { let value = self.filter.apply(image, offset); self.quantizer.quantize(value) } pub(crate) fn filter(&self) -> &Filter { &self.filter } } rusty-chromaprint-0.3.0/src/compression.rs000064400000000000000000000245451046102023000170460ustar 00000000000000use crate::Configuration; /// Number of "normal" bits. const NORMAL_BITS: u8 = 3; /// Maximum "normal" value above which a value becomes "exceptional". const MAX_NORMAL_VALUE: u8 = (1 << NORMAL_BITS) - 1; /// Turns an object (e.g. an `u32`) over an iterator of bits. trait IntoBitIterator { /// Converts the item into an an iterator over its bits. fn into_bit_iter(self) -> impl Iterator; } impl IntoBitIterator for u32 { fn into_bit_iter(self) -> impl Iterator { (0..Self::BITS).map(move |index| ((self >> index) & 1) == 1) } } pub struct FingerprintCompressor<'a>(&'a Configuration); impl<'a> FingerprintCompressor<'a> { /// Compress a sub-fingerprint. fn compress_subfingerprint(subfingerprint: u32) -> impl Iterator)> { subfingerprint .into_bit_iter() .enumerate() .filter_map(|(bit_index, is_bit_set)| { is_bit_set.then_some(u8::try_from(bit_index + 1).unwrap()) }) .scan(0, |last_bit_index, bit_index| { let value = bit_index - *last_bit_index; let result = if value >= MAX_NORMAL_VALUE { (MAX_NORMAL_VALUE, Some(value - MAX_NORMAL_VALUE)) } else { (value, None) }; *last_bit_index = bit_index; Some(result) }) .chain(std::iter::once((0, None))) } /// Compress the fingerprint. pub fn compress(&self, fingerprint: &[u32]) -> Vec { let size = fingerprint.len(); let (normal_bits, exceptional_bits) = fingerprint .iter() .scan(0, |last_subfp, current_subfp| { let value = current_subfp ^ *last_subfp; *last_subfp = *current_subfp; Some(value) }) .flat_map(Self::compress_subfingerprint) .fold( ( Vec::::with_capacity(size), Vec::::with_capacity(size), ), |(mut normal_bits, mut exceptional_bits), (normal_value, exceptional_value)| { normal_bits.push(normal_value); if let Some(exceptional_value) = exceptional_value { exceptional_bits.push(exceptional_value); } (normal_bits, exceptional_bits) }, ); let header_size = 4; let normal_size = packed_intn_array_len(normal_bits.len(), 3); let exceptional_size = packed_intn_array_len(exceptional_bits.len(), 5); let expected_size = header_size + normal_size + exceptional_size; #[allow(clippy::cast_possible_truncation)] let output = [ self.0.id(), ((size >> 16) & 0xFF) as u8, ((size >> 8) & 0xFF) as u8, (size & 0xFF) as u8, ]; let output = output .into_iter() .chain(iter_packed_intn_array::<3>(&normal_bits)) .chain(iter_packed_intn_array::<5>(&exceptional_bits)) .collect::>(); debug_assert_eq!(output.len(), expected_size); output } } impl<'a> From<&'a Configuration> for FingerprintCompressor<'a> { fn from(value: &'a Configuration) -> Self { Self(value) } } /// Calculate the size of a packed Int array. const fn packed_intn_array_len(array_len: usize, n: usize) -> usize { (array_len * n + 7) / 8 } /// Iterate bytes as packed Int array. fn iter_packed_intn_array(array: &[u8]) -> impl Iterator + '_ { let mask = (0xFF << (8 - N)) >> (8 - N); array.chunks(8).flat_map(move |slice| { let (size, result) = slice.iter().map(|s| s & mask).enumerate().fold( (0, [0u8; N]), |(_, mut result), (i, bits)| { let rightmost_bit_index = i * N; let leftmost_bit_index = rightmost_bit_index + N - 1; let right_byte = rightmost_bit_index / 8; let left_byte = leftmost_bit_index / 8; result[right_byte] |= bits << (rightmost_bit_index % 8); if left_byte != right_byte { result[left_byte] |= bits >> ((8 - (rightmost_bit_index % 8)) % 8); } (left_byte + 1, result) }, ); result.into_iter().take(size) }) } #[cfg(test)] mod tests { use super::*; const ONE_BYTE: [u8; 1] = [0b1011_1010]; const NINE_BYTES: [u8; 9] = [ 0b1010_1010, 0b0011_0011, 0b1100_1100, 0b1100_0111, 0b0101_0101, 0b1100_1100, 0b1010_1010, 0b0000_0000, 0b1111_1111, ]; const SIXTYFOUR_BYTES: [u8; 64] = [ 0xA2, 0x87, 0xE3, 0xED, 0xAA, 0xD7, 0xE8, 0x94, 0x53, 0x4E, 0x9B, 0xD5, 0x83, 0x12, 0x05, 0x43, 0x67, 0x7E, 0x0A, 0xAF, 0x2D, 0x85, 0xB4, 0x03, 0xEB, 0x13, 0x8E, 0x47, 0x07, 0xA6, 0x76, 0x5D, 0x43, 0x67, 0x8D, 0x9F, 0xEA, 0xAD, 0x3F, 0x34, 0x86, 0xF4, 0x25, 0xC8, 0xA2, 0xBF, 0xF1, 0x22, 0xB5, 0xA6, 0xB8, 0x4A, 0xED, 0xA2, 0xF5, 0x25, 0xDB, 0x62, 0x70, 0xC2, 0xB7, 0x9C, 0xB1, 0x3C, ]; #[test] fn test_iter_packed_int3_array_single_byte() { const N: usize = 3; let packed = iter_packed_intn_array::(&ONE_BYTE).collect::>(); assert_eq!(packed.len(), packed_intn_array_len(ONE_BYTE.len(), N)); assert_eq!(&packed, &[0b0000_0010]); } #[test] fn test_iter_packed_int3_array_some_bytes() { const N: usize = 3; let packed = iter_packed_intn_array::(&NINE_BYTES).collect::>(); assert_eq!(packed.len(), packed_intn_array_len(NINE_BYTES.len(), N)); assert_eq!( &packed, &[0b0001_1010, 0b0101_1111, 0b0000_1010, 0b0000_0111] ); } #[test] fn test_iter_packed_int3_array_many_bytes() { const N: usize = 3; let packed = iter_packed_intn_array::(&SIXTYFOUR_BYTES).collect::>(); assert_eq!( packed.len(), packed_intn_array_len(SIXTYFOUR_BYTES.len(), N) ); assert_eq!( &packed, &[ 0xFA, 0xAA, 0x83, 0xF3, 0x3A, 0x75, 0xB7, 0xDE, 0x72, 0x9B, 0x7F, 0xBB, 0x7B, 0xAF, 0x9E, 0x66, 0xA1, 0x47, 0x35, 0x54, 0xB5, 0x13, 0x74, 0x86 ], ); } #[test] fn test_iter_packed_int5_array_many_bytes() { const N: usize = 5; let packed = iter_packed_intn_array::(&SIXTYFOUR_BYTES).collect::>(); assert_eq!( packed.len(), packed_intn_array_len(SIXTYFOUR_BYTES.len(), N) ); assert_eq!( &packed, &[ 0xE2, 0x8C, 0xA6, 0x2E, 0xA2, 0xD3, 0xED, 0x3A, 0x64, 0x19, 0xC7, 0xAB, 0xD7, 0x0A, 0x1D, 0x6B, 0xBA, 0x73, 0x8C, 0xED, 0xE3, 0xB4, 0xAF, 0xDA, 0xA7, 0x86, 0x16, 0x24, 0x7E, 0x14, 0xD5, 0x60, 0xD5, 0x44, 0x2D, 0x5B, 0x40, 0x71, 0x79, 0xE4, ], ); } #[test] fn test_iter_packed_int5_array_single_byte() { const N: usize = 5; let packed = iter_packed_intn_array::(&ONE_BYTE).collect::>(); assert_eq!(packed.len(), packed_intn_array_len(ONE_BYTE.len(), N)); assert_eq!(&packed, &[0b0001_1010]); } #[test] fn test_iter_packed_int5_array_some_bytes() { const N: usize = 5; let packed = iter_packed_intn_array::(&NINE_BYTES).collect::>(); assert_eq!(packed.len(), packed_intn_array_len(NINE_BYTES.len(), N)); assert_eq!( &packed, &[ 0b0110_1010, 0b1011_0010, 0b0101_0011, 0b1001_1001, 0b0000_0010, 0b0001_1111 ] ); } #[test] fn test_compression() { const INPUT: [u32; 32] = [ 0x0FCAF446, 0xE3519E89, 0xD3494DD6, 0x8F219806, 0x9200D530, 0x06B1D52F, 0xB48CC681, 0x428991C3, 0x59AFBD6B, 0x6ECFB2E5, 0xE8EB7BC3, 0x99A44270, 0x31FFEC13, 0x4A4D81DA, 0x53887C82, 0x2BB7BEC2, 0xAB895A65, 0x9D7C0AE4, 0xDA356857, 0xE030F7D8, 0x4D428EEE, 0x0558E019, 0xC3278998, 0xA1D035E4, 0x582E98E5, 0x44C8B708, 0x2E8BA9E2, 0xCB13BC48, 0xB169A3D8, 0x861274AF, 0x1213EF1C, 0x1F9F06B8, ]; const OUTPUT: [u8; 220] = [ 0x01, 0x00, 0x00, 0x20, 0x0A, 0xA9, 0x24, 0xD2, 0x92, 0x24, 0x48, 0x92, 0x45, 0x52, 0x14, 0x65, 0x8B, 0x12, 0x24, 0x49, 0xA4, 0x4C, 0x61, 0x1E, 0x54, 0x89, 0xA4, 0x50, 0x61, 0x22, 0x28, 0xCA, 0x94, 0xA9, 0x53, 0x82, 0x24, 0xC9, 0x19, 0x4D, 0x83, 0x12, 0x29, 0x19, 0x95, 0x84, 0x8B, 0xA0, 0x2A, 0x91, 0xA4, 0x47, 0x49, 0x40, 0x69, 0x11, 0xB3, 0x45, 0x81, 0x12, 0x26, 0xC9, 0xA3, 0x44, 0x81, 0xB2, 0x6D, 0xD9, 0x98, 0x22, 0x59, 0x94, 0x25, 0x4B, 0x32, 0x31, 0x41, 0xC2, 0x2C, 0x91, 0x12, 0x45, 0x95, 0x90, 0x2D, 0x51, 0x94, 0x2D, 0x4A, 0x94, 0x04, 0x8C, 0xA4, 0x24, 0x49, 0xC4, 0x64, 0xC1, 0xD7, 0x24, 0x49, 0xE2, 0x24, 0x48, 0x32, 0x6D, 0x89, 0x92, 0xE4, 0xC8, 0x2B, 0x49, 0x49, 0x14, 0x05, 0xC9, 0x22, 0x31, 0xDA, 0x94, 0x10, 0x49, 0xC2, 0x24, 0xC9, 0xA2, 0x2B, 0x81, 0xA2, 0x6C, 0x49, 0xB6, 0x44, 0x8A, 0x84, 0x24, 0x4A, 0xA2, 0x44, 0x99, 0xF2, 0x21, 0xCF, 0x14, 0x25, 0x49, 0xB2, 0x30, 0x58, 0x92, 0x30, 0x89, 0x92, 0x28, 0x89, 0x18, 0xE4, 0x8A, 0xA4, 0x24, 0x49, 0xB2, 0x24, 0x41, 0x14, 0x25, 0x49, 0x22, 0x66, 0xC9, 0x12, 0x48, 0x4A, 0x94, 0x84, 0xE9, 0xA4, 0x40, 0x92, 0x22, 0x3D, 0x8B, 0x96, 0xA0, 0x4B, 0x92, 0x54, 0x49, 0xA6, 0x24, 0x48, 0xA2, 0x44, 0x89, 0x94, 0x44, 0x49, 0x94, 0x28, 0x48, 0x16, 0x25, 0xCA, 0x72, 0x0D, 0x9B, 0x32, 0x25, 0x0B, 0xA3, 0x00, 0xA1, 0x80, 0x01, 0x06, 0x00, 0x00, 0x04, 0x30, 0x00, ]; let config = Configuration::default(); let compressor = FingerprintCompressor::from(&config); let output = compressor.compress(&INPUT); assert_eq!(output, OUTPUT); } } rusty-chromaprint-0.3.0/src/fft.rs000064400000000000000000000163521046102023000152610ustar 00000000000000use std::collections::VecDeque; use std::sync::Arc; use rustfft::num_complex::{Complex, Complex64}; use rustfft::num_traits::Zero; use crate::stages::{AudioConsumer, FeatureVectorConsumer, Stage}; pub struct Fft { consumer: C, frame_size: usize, frame_overlap: usize, fft_plan: Arc>, fft_buffer_complex: Box<[Complex64]>, fft_frame: Box<[f64]>, fft_scratch: Box<[Complex64]>, window: Box<[f64]>, ring_buf: VecDeque, } impl Fft { pub(crate) fn new(frame_size: usize, frame_overlap: usize, consumer: C) -> Self { let fft_plan = rustfft::FftPlanner::new().plan_fft_forward(frame_size); Self { consumer, frame_size, frame_overlap, fft_buffer_complex: vec![Complex64::zero(); frame_size].into_boxed_slice(), fft_scratch: vec![Complex::zero(); fft_plan.get_inplace_scratch_len()] .into_boxed_slice(), fft_frame: vec![0.0; 1 + frame_size / 2].into_boxed_slice(), fft_plan, window: make_hamming_window(frame_size, 1.0), ring_buf: VecDeque::new(), } } } impl Stage for Fft { type Output = C::Output; fn output(&self) -> &Self::Output { self.consumer.output() } } impl AudioConsumer for Fft { fn reset(&mut self) { self.consumer.reset(); } fn consume(&mut self, data: &[f64]) { self.ring_buf.extend(data.iter().copied()); while self.ring_buf.len() >= self.frame_size { let window = self.ring_buf.iter().copied().take(self.frame_size); assert_eq!(self.fft_buffer_complex.len(), self.frame_size); assert_eq!(self.window.len(), self.frame_size); for (i, (output, input)) in self.fft_buffer_complex.iter_mut().zip(window).enumerate() { output.re = input * self.window[i]; output.im = 0.0; } self.fft_plan .process_with_scratch(&mut self.fft_buffer_complex, &mut self.fft_scratch); for i in 0..self.frame_size / 2 { self.fft_frame[i] = self.fft_buffer_complex[i].norm_sqr(); } self.consumer.consume(&self.fft_frame); self.ring_buf.drain(..self.frame_size - self.frame_overlap); } } fn flush(&mut self) { // It makes sense to pad the remaining samples with zeros and process the last frame, // but the reference implementation doesn't do it. // if !self.ring_buf.is_empty() && self.ring_buf.len() < self.frame_size { // self.ring_buf.resize(self.frame_size, 0.0); // self.consume(&[]); // } } } fn make_hamming_window(size: usize, scale: f64) -> Box<[f64]> { let mut window = Vec::with_capacity(size); for i in 0..size { window.push( scale * (0.54 - 0.46 * f64::cos(2.0 * std::f64::consts::PI * (i as f64) / (size as f64 - 1.0))), ); } window.into_boxed_slice() } #[cfg(test)] mod tests { use crate::fft::Fft; use crate::stages::{AudioConsumer, FeatureVectorConsumer, Stage}; struct Collector { frames: Vec>, } impl Collector { fn new() -> Self { Self { frames: vec![] } } } impl Stage for Collector { type Output = [Vec]; fn output(&self) -> &Self::Output { &self.frames } } impl FeatureVectorConsumer for Collector { fn consume(&mut self, features: &[f64]) { self.frames.push(features.to_vec()); } fn reset(&mut self) { self.frames.clear(); } } #[test] fn sine() { let nframes = 3; let frame_size = 32; let overlap = 8; let sample_rate = 1000; let freq = 7 * (sample_rate / 2) / (frame_size / 2); let mut input = vec![0.0; frame_size + (nframes - 1) * (frame_size - overlap)]; for i in 0..input.len() { input[i] = f64::sin(i as f64 * freq as f64 * 2.0 * std::f64::consts::PI / sample_rate as f64); } let collector = Collector::new(); let mut fft = Fft::new(frame_size, overlap, collector); assert_eq!(frame_size, fft.frame_size); assert_eq!(overlap, fft.frame_overlap); let chunk_size = 100; for chunk in input.chunks(chunk_size) { fft.consume(chunk); } assert_eq!(nframes, fft.output().len()); let expected_spectrum = [ 2.87005e-05, 0.00011901, 0.00029869, 0.000667172, 0.00166813, 0.00605612, 0.228737, 0.494486, 0.210444, 0.00385322, 0.00194379, 0.00124616, 0.000903851, 0.000715237, 0.000605707, 0.000551375, 0.000534304, ]; for (frame_idx, frame) in fft.output().iter().enumerate() { for i in 0..frame.len() { let magnitude = f64::sqrt(frame[i]) / frame.len() as f64; let expected_mag = expected_spectrum[i]; if (expected_mag - magnitude).abs() > 0.001 { panic!("different magnitude for frame {frame_idx} at offset {i}: s[{i}]={magnitude} (!= {expected_mag})"); } } } } #[test] fn dc() { let nframes = 3; let frame_size = 32; let overlap = 8; let input = vec![0.5; frame_size + (nframes - 1) * (frame_size - overlap)]; let collector = Collector::new(); let mut fft = Fft::new(frame_size, overlap, collector); assert_eq!(frame_size, fft.frame_size); assert_eq!(overlap, fft.frame_overlap); let chunk_size = 100; for chunk in input.chunks(chunk_size) { fft.consume(chunk); } assert_eq!(nframes, fft.output().len()); let expected_spectrum = [ 0.494691, 0.219547, 0.00488079, 0.00178991, 0.000939219, 0.000576082, 0.000385808, 0.000272904, 0.000199905, 0.000149572, 0.000112947, 8.5041e-05, 6.28312e-05, 4.4391e-05, 2.83757e-05, 1.38507e-05, 0.0, ]; for (frame_idx, frame) in fft.output().iter().enumerate() { for i in 0..frame.len() { let magnitude = f64::sqrt(frame[i]) / frame.len() as f64; let expected_mag = expected_spectrum[i]; if (expected_mag - magnitude).abs() > 0.001 { panic!("different magnitude for frame {frame_idx} at offset {i}: s[{i}]={magnitude} (!= {expected_mag})"); } } } } } rusty-chromaprint-0.3.0/src/filter.rs000064400000000000000000000176361046102023000157750ustar 00000000000000#[derive(Debug, Clone, Copy)] pub struct Filter { kind: FilterKind, y: usize, height: usize, width: usize, } #[derive(Debug, Clone, Copy)] pub enum FilterKind { Filter0, Filter1, Filter2, Filter3, Filter4, Filter5, } impl Filter { pub(crate) const fn new(kind: FilterKind, y: usize, height: usize, width: usize) -> Self { Self { kind, y, height, width, } } pub(crate) fn apply(&self, image: &impl Image, x: usize) -> f64 { let filter = match self.kind { FilterKind::Filter0 => filter0, FilterKind::Filter1 => filter1, FilterKind::Filter2 => filter2, FilterKind::Filter3 => filter3, FilterKind::Filter4 => filter4, FilterKind::Filter5 => filter5, }; filter(image, x, self.y, self.width, self.height, subtract_log) } pub(crate) fn width(&self) -> usize { self.width } } fn subtract_log(a: f64, b: f64) -> f64 { let r = f64::ln((1.0 + a) / (1.0 + b)); assert!(!r.is_nan()); r } pub trait Image { fn area(&self, x: usize, y: usize, w: usize, h: usize) -> f64; } type Comparator = fn(f64, f64) -> f64; // oooooooooooooooo // oooooooooooooooo // oooooooooooooooo // oooooooooooooooo fn filter0(image: &impl Image, x: usize, y: usize, w: usize, h: usize, cmp: Comparator) -> f64 { assert!(w >= 1); assert!(h >= 1); let a = image.area(x, y, x + w, y + h); let b = 0.0; cmp(a, b) } // ................ // ................ // oooooooooooooooo // oooooooooooooooo fn filter1(image: &impl Image, x: usize, y: usize, w: usize, h: usize, cmp: Comparator) -> f64 { assert!(w >= 1); assert!(h >= 1); let h_2 = h / 2; let a = image.area(x, y + h_2, x + w, y + h); let b = image.area(x, y, x + w, y + h_2); cmp(a, b) } // .......ooooooooo // .......ooooooooo // .......ooooooooo // .......ooooooooo fn filter2(image: &impl Image, x: usize, y: usize, w: usize, h: usize, cmp: Comparator) -> f64 { assert!(w >= 1); assert!(h >= 1); let w_2 = w / 2; let a = image.area(x + w_2, y, x + w, y + h); let b = image.area(x, y, x + w_2, y + h); cmp(a, b) } // .......ooooooooo // .......ooooooooo // ooooooo......... // ooooooo......... fn filter3(image: &impl Image, x: usize, y: usize, w: usize, h: usize, cmp: Comparator) -> f64 { assert!(w >= 1); assert!(h >= 1); let w_2 = w / 2; let h_2 = h / 2; let a = image.area(x, y + h_2, x + w_2, y + h) + image.area(x + w_2, y, x + w, y + h_2); let b = image.area(x, y, x + w_2, y + h_2) + image.area(x + w_2, y + h_2, x + w, y + h); cmp(a, b) } // ................ // oooooooooooooooo // ................ fn filter4(image: &impl Image, x: usize, y: usize, w: usize, h: usize, cmp: Comparator) -> f64 { assert!(w >= 1); assert!(h >= 1); let h_3 = h / 3; let a = image.area(x, y + h_3, x + w, y + 2 * h_3); let b = image.area(x, y, x + w, y + h_3) + image.area(x, y + 2 * h_3, x + w, y + h); cmp(a, b) } // .....oooooo..... // .....oooooo..... // .....oooooo..... // .....oooooo..... fn filter5(image: &impl Image, x: usize, y: usize, w: usize, h: usize, cmp: Comparator) -> f64 { assert!(w >= 1); assert!(h >= 1); let w_3 = w / 3; let a = image.area(x + w_3, y, x + 2 * w_3, y + h); let b = image.area(x, y, x + w_3, y + h) + image.area(x + 2 * w_3, y, x + w, y + h); cmp(a, b) } #[cfg(test)] mod tests { use crate::assert_eq_float; use crate::filter::{ filter0, filter1, filter2, filter3, filter4, filter5, subtract_log, Filter, FilterKind, }; use crate::rolling_image::RollingIntegralImage; #[test] fn test_compare_subtract() { let res = subtract(2.0, 1.0); assert_eq_float!(1.0, res); } #[test] fn test_compare_subtract_log() { let res = subtract_log(2.0, 1.0); assert_eq_float!(0.4054651, res); } #[test] fn test_filter_with_filter0() { let data = [0.0, 1.0, 2.0, 3.0]; let mut integral_image = RollingIntegralImage::from_data(2, &data); let flt1 = Filter::new(FilterKind::Filter0, 0, 1, 1); assert_eq_float!(0.0, flt1.apply(&mut integral_image, 0)); assert_eq_float!(1.0986123, flt1.apply(&mut integral_image, 1)); } #[test] fn test_filter0() { let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]; let integral_image = RollingIntegralImage::from_data(3, &data); let res = filter0(&integral_image, 0, 0, 1, 1, subtract); assert_eq_float!(1.0, res); let res = filter0(&integral_image, 0, 0, 2, 2, subtract); assert_eq_float!(12.0, res); let res = filter0(&integral_image, 0, 0, 3, 3, subtract); assert_eq_float!(45.0, res); let res = filter0(&integral_image, 1, 1, 2, 2, subtract); assert_eq_float!(28.0, res); let res = filter0(&integral_image, 2, 2, 1, 1, subtract); assert_eq_float!(9.0, res); let res = filter0(&integral_image, 0, 0, 3, 1, subtract); assert_eq_float!(12.0, res); let res = filter0(&integral_image, 0, 0, 1, 3, subtract); assert_eq_float!(6.0, res); } #[test] fn test_filter1() { let data = [1.0, 2.1, 3.4, 3.1, 4.1, 5.1, 6.0, 7.1, 8.0]; let integral_image = RollingIntegralImage::from_data(3, &data); let res = filter1(&integral_image, 0, 0, 1, 1, subtract); assert_eq_float!(1.0 - 0.0, res); let res = filter1(&integral_image, 1, 1, 1, 1, subtract); assert_eq_float!(4.1 - 0.0, res); let res = filter1(&integral_image, 0, 0, 1, 2, subtract); assert_eq_float!(2.1 - 1.0, res); let res = filter1(&integral_image, 0, 0, 2, 2, subtract); assert_eq_float!((2.1 + 4.1) - (1.0 + 3.1), res); let res = filter1(&integral_image, 0, 0, 3, 2, subtract); assert_eq_float!((2.1 + 4.1 + 7.1) - (1.0 + 3.1 + 6.0), res); } #[test] fn test_filter2() { let data = [1.0, 2.0, 3.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; let integral_image = RollingIntegralImage::from_data(3, &data); let res = filter2(&integral_image, 0, 0, 2, 1, subtract); assert_eq_float!(2.0, res); // 3 - 1 let res = filter2(&integral_image, 0, 0, 2, 2, subtract); assert_eq_float!(4.0, res); // 3+4 - 1+2 let res = filter2(&integral_image, 0, 0, 2, 3, subtract); assert_eq_float!(6.0, res); // 3+4+5 - 1+2+3 } #[test] fn test_filter3() { let data = [1.0, 2.1, 3.4, 3.1, 4.1, 5.1, 6.0, 7.1, 8.0]; let integral_image = RollingIntegralImage::from_data(3, &data); let res = filter3(&integral_image, 0, 0, 2, 2, subtract); assert_eq_float!(0.1, res); // 2.1+3.1 - 1+4.1 let res = filter3(&integral_image, 1, 1, 2, 2, subtract); assert_eq_float!(0.1, res); // 4+8 - 5+7 let res = filter3(&integral_image, 0, 1, 2, 2, subtract); assert_eq_float!(0.3, res); // 2.1+5.1 - 3.4+4.1 } #[test] fn test_filter4() { let data = [1.0, 2.0, 3.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; let integral_image = RollingIntegralImage::from_data(3, &data); let res = filter4(&integral_image, 0, 0, 3, 3, subtract); assert_eq_float!(-13.0, res); // 2+4+7 - (1+3+6) - (3+5+8) } #[test] fn test_filter5() { let data = [1.0, 2.0, 3.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]; let integral_image = RollingIntegralImage::from_data(3, &data); let res = filter5(&integral_image, 0, 0, 3, 3, subtract); assert_eq_float!(-15.0, res); // 3+4+5 - (1+2+3) - (6+7+8) } fn subtract(a: f64, b: f64) -> f64 { a - b } } rusty-chromaprint-0.3.0/src/fingerprint_calculator.rs000064400000000000000000000033251046102023000212360ustar 00000000000000use crate::classifier::Classifier; use crate::rolling_image::RollingIntegralImage; use crate::stages::{FeatureVectorConsumer, Stage}; pub(crate) struct FingerprintCalculator { classifiers: Vec, max_filter_width: usize, image: RollingIntegralImage, fingerprint: Vec, } impl FingerprintCalculator { pub(crate) fn new(classifiers: Vec) -> Self { let max_width = classifiers .iter() .map(|c| c.filter().width()) .max() .unwrap(); assert!(max_width > 0); assert!(max_width <= 256); Self { max_filter_width: max_width, classifiers, image: RollingIntegralImage::new(255), fingerprint: vec![], } } fn calculate_subfingerprint(&self, offset: usize) -> u32 { let mut bits = 0u32; for classifier in &self.classifiers { bits = (bits << 2) | gray_code(classifier.classify(&self.image, offset)); } bits } } impl Stage for FingerprintCalculator { type Output = [u32]; fn output(&self) -> &Self::Output { self.fingerprint.as_slice() } } impl FeatureVectorConsumer for FingerprintCalculator { fn consume(&mut self, features: &[f64]) { self.image.add_row(features); if self.image.rows() >= self.max_filter_width { self.fingerprint .push(self.calculate_subfingerprint(self.image.rows() - self.max_filter_width)); } } fn reset(&mut self) { self.image.reset(); self.fingerprint.clear(); } } fn gray_code(i: u32) -> u32 { [0, 1, 3, 2][i as usize] } rusty-chromaprint-0.3.0/src/fingerprint_matcher.rs000064400000000000000000000500541046102023000205310ustar 00000000000000use std::cmp::Reverse; use std::fmt::{Display, Formatter}; use crate::fingerprinter::Configuration; use crate::gaussian::gaussian_filter; use crate::gradient::gradient; #[derive(Debug)] pub enum MatchError { FingerprintTooLong { index: u8 }, } impl Display for MatchError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { MatchError::FingerprintTooLong { index } => { write!(f, "Fingerprint #{index} is too long") } } } } impl std::error::Error for MatchError {} const ALIGN_BITS: u32 = 12; const HASH_SHIFT: u32 = 32 - ALIGN_BITS; const HASH_MASK: u32 = ((1 << ALIGN_BITS) - 1) << HASH_SHIFT; const OFFSET_MASK: u32 = (1 << (32 - ALIGN_BITS - 1)) - 1; const SOURCE_MASK: u32 = 1 << (32 - ALIGN_BITS - 1); fn align_strip(x: u32) -> u32 { x >> (32 - ALIGN_BITS) } /// Returns similar segments of two audio streams using their fingerprints. pub fn match_fingerprints( fp1: &[u32], fp2: &[u32], _config: &Configuration, ) -> Result, MatchError> { if fp1.len() + 1 >= OFFSET_MASK as usize { return Err(MatchError::FingerprintTooLong { index: 0 }); } if fp2.len() + 1 >= OFFSET_MASK as usize { return Err(MatchError::FingerprintTooLong { index: 1 }); } let mut offsets = Vec::with_capacity(fp1.len() + fp2.len()); for (i, &segment) in fp1.iter().enumerate() { offsets.push((align_strip(segment) << HASH_SHIFT) | (i as u32)); } for (i, &segment) in fp2.iter().enumerate() { offsets.push((align_strip(segment) << HASH_SHIFT) | (i as u32) | SOURCE_MASK); } offsets.sort_unstable(); let mut histogram = vec![0u32; fp1.len() + fp2.len()]; for (offset_idx, item1) in offsets.iter().enumerate() { let hash1 = item1 & HASH_MASK; let offset1 = item1 & OFFSET_MASK; let source1 = item1 & SOURCE_MASK; if source1 != 0 { // if we got hash from fp2, it means there is no hash from fp1, // because if there was, it would be first continue; } for item2 in offsets.iter().skip(offset_idx + 1) { let hash2 = item2 & HASH_MASK; if hash1 != hash2 { break; } let offset2 = item2 & OFFSET_MASK; let source2 = item2 & SOURCE_MASK; if source2 != 0 { let offset_diff = offset1 as usize + fp2.len() - offset2 as usize; histogram[offset_diff] += 1; } } } let mut best_alignments = Vec::new(); let histogram_size = histogram.len(); for i in 0..histogram_size { let count = histogram[i]; if histogram[i] > 1 { let is_peak_left = if i > 0 { histogram[i - 1] <= count } else { true }; let is_peak_right = if i < histogram_size - 1 { histogram[i + 1] <= count } else { true }; if is_peak_left && is_peak_right { best_alignments.push((count, i)); } } } best_alignments.sort_unstable_by_key(|it| Reverse(*it)); let mut segments: Vec = Vec::new(); if let Some((_count, offset)) = best_alignments.into_iter().next() { let offset_diff = offset as isize - fp2.len() as isize; let offset1 = if offset_diff > 0 { offset_diff as usize } else { 0 }; let offset2 = if offset_diff < 0 { -offset_diff as usize } else { 0 }; let size = usize::min(fp1.len() - offset1, fp2.len() - offset2); let mut bit_counts = Vec::new(); for i in 0..size { bit_counts.push((fp1[offset1 + i] ^ fp2[offset2 + i]).count_ones() as f64); } let orig_bit_counts = bit_counts.clone(); let mut smoothed_bit_counts = vec![0.0; size]; gaussian_filter(&mut bit_counts, &mut smoothed_bit_counts, 8.0, 3); let mut grad = Vec::with_capacity(size); gradient(smoothed_bit_counts.iter().copied(), &mut grad); for item in grad.iter_mut().take(size) { *item = item.abs(); } let mut gradient_peaks = Vec::new(); for i in 0..size { let gi = grad[i]; if i > 0 && i < size - 1 && gi > 0.15 && gi >= grad[i - 1] && gi >= grad[i + 1] && (gradient_peaks.is_empty() || gradient_peaks.last().unwrap() + 1 < i) { gradient_peaks.push(i); } } gradient_peaks.push(size); let match_threshold = 10.0; let max_score_difference = 0.7; let mut begin = 0; for end in gradient_peaks { let duration = end - begin; let score: f64 = orig_bit_counts[begin..end].iter().sum::() / (duration as f64); if score < match_threshold { let new_segment = Segment { offset1: offset1 + begin, offset2: offset2 + begin, items_count: duration, score, }; let mut added = false; if let Some(s1) = segments.last_mut() { if (s1.score - score).abs() < max_score_difference { if let Some(merged) = s1.try_merge(&new_segment) { *s1 = merged; added = true; } } } if !added { segments.push(new_segment); } } begin = end; } } Ok(segments) } /// Segment of an audio that is similar between two fingerprints. #[derive(Debug)] pub struct Segment { /// Index of the item in the first fingerprint. pub offset1: usize, /// Index of an item in the second fingerprint. pub offset2: usize, /// Number of items from the fingerprint corresponding to this segment. pub items_count: usize, /// Score that corresponds to similarity of this segment. /// The smaller this value is, the stronger similarity. /// /// This value can be be 0 up to 32. pub score: f64, } impl Segment { /// A timestamp representing the start of the segment in the first fingerprint. pub fn start1(&self, config: &Configuration) -> f32 { config.item_duration_in_seconds() * self.offset1 as f32 } /// A timestamp representing the end of the segment in the first fingerprint. pub fn end1(&self, config: &Configuration) -> f32 { self.start1(config) + self.duration(config) } /// A timestamp representing the start of the segment in the second fingerprint. pub fn start2(&self, config: &Configuration) -> f32 { config.item_duration_in_seconds() * self.offset2 as f32 } /// A timestamp representing the end of the segment in the second fingerprint. pub fn end2(&self, config: &Configuration) -> f32 { self.start2(config) + self.duration(config) } /// Duration of the segment (in seconds). pub fn duration(&self, config: &Configuration) -> f32 { config.item_duration_in_seconds() * self.items_count as f32 } } impl Segment { /// Try to merge two consecutive segments into one. fn try_merge(&self, other: &Self) -> Option { // Check if segments are consecutive if self.offset1 + self.items_count != other.offset1 { return None; } if self.offset2 + self.items_count != other.offset2 { return None; } let new_duration = self.items_count + other.items_count; let new_score = (self.score * self.items_count as f64 + other.score * other.items_count as f64) / new_duration as f64; Some(Segment { offset1: self.offset1, offset2: self.offset2, items_count: new_duration, score: new_score, }) } } #[cfg(test)] mod tests { use crate::assert_eq_float; use crate::fingerprint_matcher::match_fingerprints; use crate::fingerprinter::Configuration; #[test] fn simple() { let fp1_data: [i32; 221] = [ 1889975932, -257508804, -240734660, -236548548, -1275161041, -1283549650, -1288796626, -1288845782, -231944646, -227770598, -227596006, -428984294, 1717901370, 1847860266, 1848063034, 1849045002, 978722826, 994451466, 1002856472, 969236792, 969204600, 2026173048, 2026215160, 2026348248, 2022681304, -393368872, -393430280, -393429272, -1462980951, -1471637847, -1475836247, -1475752279, -1408508261, -1404080485, -1404098886, -1395845206, -1260649558, -185335378, -194965314, -262004554, -262000394, -261962634, -253506554, -235820026, -235689979, -210588412, -212693436, -216889788, -216889804, -1288588764, -1288375764, -1285242323, -1309883603, -1309878993, -1578117826, -1276185314, -1284510370, -1284669458, -1284683793, -1284683779, -1284548771, -1285074676, -1284157156, -1217105619, -1242275539, -1246273217, -1246338786, -1112055010, -1118472386, -1117960918, -1088601046, -1084668806, -1101269797, -19311413, -19377013, -27759477, -15127142, -48616006, -65392726, -597817414, -589954150, -657063522, -661311010, -661643170, -644866018, -879632354, -610950114, -610948034, -606815169, -1688355556, -1184907508, -1176519156, -1109817764, -1386707332, -1390831876, -1491496216, -1491500312, -1506163992, -1557658904, -1590950929, -1607728786, -1603534722, -521333698, -525386690, -256956306, -257017634, -257017650, 1370385550, 1403976846, 1391445390, 1458405790, 1449972222, 1584197934, 1585410350, 1581195839, 1602167353, 1568612921, 1551839800, 1558123048, 1490825577, 1486627321, 1485595851, 1485616331, 416066651, 416132123, 416197643, 467577871, 442403855, 440304655, 507478045, 507150399, 507166847, 505077871, 505110766, 1046175982, 1046110463, 1062863055, 1014628813, 1014694876, 477704956, 349775612, 886646376, 886650488, 349707865, 278404699, 282599130, 299636714, 301741482, 284964010, 276582554, 276578442, 276611210, 352309434, 352236794, 352236766, 1442751822, 1476306255, 1191163167, 1191031101, 1459467068, 1585427260, 1581228348, 1579147564, 1579017580, 1579017452, 1582298364, 1607389405, 1574617551, 1557840350, 1560003070, 1551618558, 1551463550, 1547527286, 1547527414, 1547003350, 1549231830, 1557394166, -594291978, -578776362, -578767242, -679434394, -1748979865, -1211650084, -1244225828, -1319663940, -1319270740, -1287814484, -1287798084, -1287793988, -1287851028, -1287784979, -1284680593, -1586531201, -1578114017, -1594890977, -1578186993, -1578187249, -1586510305, -1553074377, -1553144522, -1553407978, -1557344201, -1590738889, -1523711658, -444714402, -436329922, -436395474, -402902226, -402910946, -419680246, -402820069, -402795479, -436208615, -453055479, -461374199, 1685898841, 1681703659, 1681654523, 1681855067, -193064325, -184667797, ]; let fp2_data: [i32; 221] = [ -1288792466, -1288780246, -1288845782, -231977158, -226662118, -496031718, 1718564890, 1713642554, 1847931962, 1847996442, 1047928842, 978722826, 1002840075, 969367608, 969204024, 2026173048, 2026149496, 2026348280, 2022686424, -124802344, -393368872, -393429256, -1467171160, -1471365463, -1475836247, -1475836247, -1475752263, -1404080485, -1404080485, -1395841350, -1529080918, -1261174358, -193723970, -194826082, -262004490, -261992330, -253574058, -252457978, -235821049, -235755515, -210583804, -212693436, -216889788, -216820172, -1288658396, -1284181460, -1310407891, -1309884113, -1309813458, -1309682402, -1284506338, -1284510338, -1284677649, -1284683793, -1284552739, -1284549811, -1284026100, -1284222659, -1242271443, -1246404289, -1246273250, -1246338786, -1107860706, -1117956290, -1117960918, -1084668822, -1101409030, -19303205, -19377013, -27757429, -27775861, -14996070, -48615494, -60932182, -597817414, -657063010, -661188130, -661573154, -644865954, -644784098, -611196898, -610950122, -606755777, -606815171, -614615780, -1184909812, -1109420532, -1126592932, -1403478276, -1390823684, -1491495252, -1506032984, -1506065748, -1574436177, -1591213074, -1607729106, -529792978, -521391058, -257087442, -256956290, 1890465998, 1940813966, 1387191438, 1391363470, 1382908302, 1449955518, 1450045678, 1580068910, 1580146991, 1596858925, 1567564345, 1551839801, 1560220201, 1560089131, 1486627627, 1486628186, 1485628490, 1485614154, 411872346, 412003406, 432978958, 467573775, 442401805, 440370205, 507478068, 507166772, 440057957, 437936357, 442196199, 442196199, 450506951, 536477893, 477823445, 477696756, 486089340, 483993196, 483993196, 479805048, 345521737, 345517771, 362299386, 299373994, 299636138, 274464954, 8126602, 8142986, 16568474, 16707770, 12498170, 79602718, 1157539086, 1174381855, 1207805245, 1191031068, 1191153948, 1182699804, 1444852028, 1579066732, 1579148780, 1579017452, 1583342780, 1600050589, 534430095, 534496159, 1568391646, 1568232830, 1568502830, 1547527214, 1547002918, 1551327574, 1551262294, 1553453654, -577387946, -578776490, -545216905, -1752912011, -1748652076, -1782071596, -1780511052, -1327659348, -1319271764, -1317240148, -1287863636, -1287851347, -1288834067, -1288834577, -1322396185, -1603406729, -1594889129, -1594950377, -1594962681, -1594962169, -1586505929, -1553078985, -1574313705, -1574309881, -1574223817, -1557462681, -1486220425, -436329866, -436395410, -440651217, -436456674, -402902770, -402836469, -402820037, -402817991, -436208631, -436225015, -167993525, 1971094107, 1966916347, 1966953050, 1967002202, -176287382, -184668054, -188926869, -188992263, -188777271, -188818231, -33628727, ]; let fp1 = fp1_data .iter() .copied() .map(|c| c as u32) .collect::>(); let fp2 = fp2_data .iter() .copied() .map(|c| c as u32) .collect::>(); let conf = Configuration::preset_test2(); let segments = match_fingerprints(&fp1, &fp2, &conf).unwrap(); assert_eq!(segments.len(), 1); assert_eq!(segments[0].offset1, 5); assert_eq!(segments[0].offset2, 0); assert_eq!(segments[0].items_count, 216); assert_eq_float!(segments[0].score, 3.17183, 0.001); } } rusty-chromaprint-0.3.0/src/fingerprinter.rs000064400000000000000000000333401046102023000173540ustar 00000000000000use crate::audio_processor::{AudioProcessor, ResetError}; use crate::chroma::Chroma; use crate::chroma_filter::ChromaFilter; use crate::chroma_normalizer::ChromaNormalizer; use crate::classifier::Classifier; use crate::fft::Fft; use crate::filter::{Filter, FilterKind}; use crate::fingerprint_calculator::FingerprintCalculator; use crate::quantize::Quantizer; use crate::stages::{AudioConsumer, Stage}; /// Structure containing configuration for a [Fingerprinter]. #[derive(Debug, Clone)] pub struct Configuration { id: u8, classifiers: Vec, remove_silence: bool, silence_threshold: u32, frame_size: usize, frame_overlap: usize, filter_coefficients: Vec, max_filter_width: usize, interpolate: bool, } impl Configuration { /// Creates a new default configuration. fn new() -> Self { Self { id: 0xFF, classifiers: Vec::new(), remove_silence: false, silence_threshold: 0, frame_size: 0, frame_overlap: 0, filter_coefficients: Vec::new(), max_filter_width: 0, interpolate: false, } } /// Adds an ID to the configuration. /// /// This ID is used for fingerprint compression. pub fn with_id(mut self, id: u8) -> Self { self.id = id; self } /// Adds classifiers to the configuration. pub fn with_classifiers(mut self, classifiers: Vec) -> Self { self.max_filter_width = classifiers .iter() .map(|c| c.filter().width()) .max() .unwrap_or(0); self.classifiers = classifiers; self } /// Updates coefficients for internal chroma filter. pub fn with_coefficients(mut self, coefficients: Vec) -> Self { self.filter_coefficients = coefficients; self } /// Enables or disables interpolation. pub fn with_interpolation(mut self, interpolate: bool) -> Self { self.interpolate = interpolate; self } /// Sets number of samples in a single frame for FFT. pub fn with_frame_size(mut self, frame_size: usize) -> Self { self.frame_size = frame_size; self } /// Sets number of samples overlapping between two consecutive frames for FFT. pub fn with_frame_overlap(mut self, frame_overlap: usize) -> Self { self.frame_overlap = frame_overlap; self } /// Enables removal of silence with a specified threshold. pub fn with_removed_silence(mut self, silence_threshold: u32) -> Self { self.remove_silence = true; self.silence_threshold = silence_threshold; self } /// Target sample rate for fingerprint calculation. pub fn sample_rate(&self) -> u32 { DEFAULT_SAMPLE_RATE } pub fn preset_test1() -> Self { Self::new() .with_id(0) .with_classifiers(CLASSIFIER_TEST1.into()) .with_coefficients(CHROMA_FILTER_COEFFICIENTS.into()) .with_interpolation(false) .with_frame_size(DEFAULT_FRAME_SIZE) .with_frame_overlap(DEFAULT_FRAME_OVERLAP) } pub fn preset_test2() -> Self { Self::new() .with_id(1) .with_classifiers(CLASSIFIER_TEST2.into()) .with_coefficients(CHROMA_FILTER_COEFFICIENTS.into()) .with_interpolation(false) .with_frame_size(DEFAULT_FRAME_SIZE) .with_frame_overlap(DEFAULT_FRAME_OVERLAP) } pub fn preset_test3() -> Self { Self::new() .with_id(2) .with_classifiers(CLASSIFIER_TEST3.into()) .with_coefficients(CHROMA_FILTER_COEFFICIENTS.into()) .with_interpolation(true) .with_frame_size(DEFAULT_FRAME_SIZE) .with_frame_overlap(DEFAULT_FRAME_OVERLAP) } pub fn preset_test4() -> Self { Self::new().with_id(3).with_removed_silence(50) } pub fn preset_test5() -> Self { Self::new() .with_id(4) .with_frame_size(DEFAULT_FRAME_SIZE / 2) .with_frame_overlap(DEFAULT_FRAME_SIZE / 2 - DEFAULT_FRAME_SIZE / 4) } fn samples_in_item(&self) -> usize { self.frame_size - self.frame_overlap } /// The algorithm ID of this configuration (only used for fingerprint compression). pub fn id(&self) -> u8 { self.id } /// A duration of a single item from the fingerprint. pub fn item_duration_in_seconds(&self) -> f32 { self.samples_in_item() as f32 / self.sample_rate() as f32 } /// Get the delay. pub fn delay(&self) -> usize { ((self.filter_coefficients.len() - 1) + (self.max_filter_width - 1)) * self.samples_in_item() + self.frame_overlap } } impl Default for Configuration { fn default() -> Self { Self::preset_test2() } } const MIN_FREQ: u32 = 28; const MAX_FREQ: u32 = 3520; const DEFAULT_SAMPLE_RATE: u32 = 11025; /// Calculates a fingerprint for a given audio samples. pub struct Fingerprinter { processor: AudioProcessor>>, } impl Fingerprinter { /// Creates a new [Fingerprinter] with the given [Configuration]. pub fn new(config: &Configuration) -> Self { let normalizer = ChromaNormalizer::new(FingerprintCalculator::new(config.classifiers.clone())); let filter = ChromaFilter::new( config.filter_coefficients.clone().into_boxed_slice(), normalizer, ); let chroma = Chroma::new( MIN_FREQ, MAX_FREQ, config.frame_size, DEFAULT_SAMPLE_RATE, filter, ); let fft = Fft::new(config.frame_size, config.frame_overlap, chroma); let processor = AudioProcessor::new( DEFAULT_SAMPLE_RATE, Box::new(fft) as Box>, ); Self { processor } } /// Resets the internal state to allow for a new fingerprint calculation. pub fn start(&mut self, sample_rate: u32, channels: u32) -> Result<(), ResetError> { self.processor.reset(sample_rate, channels)?; Ok(()) } /// Adds a new chunk of samples to the current calculation. pub fn consume(&mut self, data: &[i16]) { self.processor.consume(data) } /// Finishes the fingerprint calculation by flushing internal buffers. pub fn finish(&mut self) { self.processor.flush(); } /// Returns the fingerprint of the last consumed audio data. pub fn fingerprint(&self) -> &[u32] { self.processor.output() } } const DEFAULT_FRAME_SIZE: usize = 4096; const DEFAULT_FRAME_OVERLAP: usize = DEFAULT_FRAME_SIZE - DEFAULT_FRAME_SIZE / 3; const CLASSIFIER_TEST1: [Classifier; 16] = [ Classifier::new( Filter::new(FilterKind::Filter0, 0, 3, 15), Quantizer::new(2.10543, 2.45354, 2.69414), ), Classifier::new( Filter::new(FilterKind::Filter1, 0, 4, 14), Quantizer::new(-0.345922, 0.0463746, 0.446251), ), Classifier::new( Filter::new(FilterKind::Filter1, 4, 4, 11), Quantizer::new(-0.392132, 0.0291077, 0.443391), ), Classifier::new( Filter::new(FilterKind::Filter3, 0, 4, 14), Quantizer::new(-0.192851, 0.00583535, 0.204053), ), Classifier::new( Filter::new(FilterKind::Filter2, 8, 2, 4), Quantizer::new(-0.0771619, -0.00991999, 0.0575406), ), Classifier::new( Filter::new(FilterKind::Filter5, 6, 2, 15), Quantizer::new(-0.710437, -0.518954, -0.330402), ), Classifier::new( Filter::new(FilterKind::Filter1, 9, 2, 16), Quantizer::new(-0.353724, -0.0189719, 0.289768), ), Classifier::new( Filter::new(FilterKind::Filter3, 4, 2, 10), Quantizer::new(-0.128418, -0.0285697, 0.0591791), ), Classifier::new( Filter::new(FilterKind::Filter3, 9, 2, 16), Quantizer::new(-0.139052, -0.0228468, 0.0879723), ), Classifier::new( Filter::new(FilterKind::Filter2, 1, 3, 6), Quantizer::new(-0.133562, 0.00669205, 0.155012), ), Classifier::new( Filter::new(FilterKind::Filter3, 3, 6, 2), Quantizer::new(-0.0267, 0.00804829, 0.0459773), ), Classifier::new( Filter::new(FilterKind::Filter2, 8, 1, 10), Quantizer::new(-0.0972417, 0.0152227, 0.129003), ), Classifier::new( Filter::new(FilterKind::Filter3, 4, 4, 14), Quantizer::new(-0.141434, 0.00374515, 0.149935), ), Classifier::new( Filter::new(FilterKind::Filter5, 4, 2, 15), Quantizer::new(-0.64035, -0.466999, -0.285493), ), Classifier::new( Filter::new(FilterKind::Filter5, 9, 2, 3), Quantizer::new(-0.322792, -0.254258, -0.174278), ), Classifier::new( Filter::new(FilterKind::Filter2, 1, 8, 4), Quantizer::new(-0.0741375, -0.00590933, 0.0600357), ), ]; const CLASSIFIER_TEST2: [Classifier; 16] = [ Classifier::new( Filter::new(FilterKind::Filter0, 4, 3, 15), Quantizer::new(1.98215, 2.35817, 2.63523), ), Classifier::new( Filter::new(FilterKind::Filter4, 4, 6, 15), Quantizer::new(-1.03809, -0.651211, -0.282167), ), Classifier::new( Filter::new(FilterKind::Filter1, 0, 4, 16), Quantizer::new(-0.298702, 0.119262, 0.558497), ), Classifier::new( Filter::new(FilterKind::Filter3, 8, 2, 12), Quantizer::new(-0.105439, 0.0153946, 0.135898), ), Classifier::new( Filter::new(FilterKind::Filter3, 4, 4, 8), Quantizer::new(-0.142891, 0.0258736, 0.200632), ), Classifier::new( Filter::new(FilterKind::Filter4, 0, 3, 5), Quantizer::new(-0.826319, -0.590612, -0.368214), ), Classifier::new( Filter::new(FilterKind::Filter1, 2, 2, 9), Quantizer::new(-0.557409, -0.233035, 0.0534525), ), Classifier::new( Filter::new(FilterKind::Filter2, 7, 3, 4), Quantizer::new(-0.0646826, 0.00620476, 0.0784847), ), Classifier::new( Filter::new(FilterKind::Filter2, 6, 2, 16), Quantizer::new(-0.192387, -0.029699, 0.215855), ), Classifier::new( Filter::new(FilterKind::Filter2, 1, 3, 2), Quantizer::new(-0.0397818, -0.00568076, 0.0292026), ), Classifier::new( Filter::new(FilterKind::Filter5, 10, 1, 15), Quantizer::new(-0.53823, -0.369934, -0.190235), ), Classifier::new( Filter::new(FilterKind::Filter3, 6, 2, 10), Quantizer::new(-0.124877, 0.0296483, 0.139239), ), Classifier::new( Filter::new(FilterKind::Filter2, 1, 1, 14), Quantizer::new(-0.101475, 0.0225617, 0.231971), ), Classifier::new( Filter::new(FilterKind::Filter3, 5, 6, 4), Quantizer::new(-0.0799915, -0.00729616, 0.063262), ), Classifier::new( Filter::new(FilterKind::Filter1, 9, 2, 12), Quantizer::new(-0.272556, 0.019424, 0.302559), ), Classifier::new( Filter::new(FilterKind::Filter3, 4, 2, 14), Quantizer::new(-0.164292, -0.0321188, 0.0846339), ), ]; const CLASSIFIER_TEST3: [Classifier; 16] = [ Classifier::new( Filter::new(FilterKind::Filter0, 4, 3, 15), Quantizer::new(1.98215, 2.35817, 2.63523), ), Classifier::new( Filter::new(FilterKind::Filter4, 4, 6, 15), Quantizer::new(-1.03809, -0.651211, -0.282167), ), Classifier::new( Filter::new(FilterKind::Filter1, 0, 4, 16), Quantizer::new(-0.298702, 0.119262, 0.558497), ), Classifier::new( Filter::new(FilterKind::Filter3, 8, 2, 12), Quantizer::new(-0.105439, 0.0153946, 0.135898), ), Classifier::new( Filter::new(FilterKind::Filter3, 4, 4, 8), Quantizer::new(-0.142891, 0.0258736, 0.200632), ), Classifier::new( Filter::new(FilterKind::Filter4, 0, 3, 5), Quantizer::new(-0.826319, -0.590612, -0.368214), ), Classifier::new( Filter::new(FilterKind::Filter1, 2, 2, 9), Quantizer::new(-0.557409, -0.233035, 0.0534525), ), Classifier::new( Filter::new(FilterKind::Filter2, 7, 3, 4), Quantizer::new(-0.0646826, 0.00620476, 0.0784847), ), Classifier::new( Filter::new(FilterKind::Filter2, 6, 2, 16), Quantizer::new(-0.192387, -0.029699, 0.215855), ), Classifier::new( Filter::new(FilterKind::Filter2, 1, 3, 2), Quantizer::new(-0.0397818, -0.00568076, 0.0292026), ), Classifier::new( Filter::new(FilterKind::Filter5, 10, 1, 15), Quantizer::new(-0.53823, -0.369934, -0.190235), ), Classifier::new( Filter::new(FilterKind::Filter3, 6, 2, 10), Quantizer::new(-0.124877, 0.0296483, 0.139239), ), Classifier::new( Filter::new(FilterKind::Filter2, 1, 1, 14), Quantizer::new(-0.101475, 0.0225617, 0.231971), ), Classifier::new( Filter::new(FilterKind::Filter3, 5, 6, 4), Quantizer::new(-0.0799915, -0.00729616, 0.063262), ), Classifier::new( Filter::new(FilterKind::Filter1, 9, 2, 12), Quantizer::new(-0.272556, 0.019424, 0.302559), ), Classifier::new( Filter::new(FilterKind::Filter3, 4, 2, 14), Quantizer::new(-0.164292, -0.0321188, 0.0846339), ), ]; const CHROMA_FILTER_COEFFICIENTS: [f64; 5] = [0.25, 0.75, 1.0, 0.75, 0.25]; rusty-chromaprint-0.3.0/src/gaussian.rs000064400000000000000000000155711046102023000163160ustar 00000000000000pub fn gaussian_filter<'a>( mut input: &'a mut [f64], mut output: &'a mut [f64], sigma: f64, n: usize, ) { let w = f64::sqrt(12.0 * sigma * sigma / n as f64 + 1.0).floor() as usize; let wl = w - (w % 2 == 0) as usize; let wu = wl + 2; let fwl = wl as f64; let m = ((12.0 * sigma * sigma - n as f64 * fwl * fwl - 4.0 * n as f64 * fwl - 3.0 * n as f64) / (-4.0 * fwl - 4.0)) .round() as usize; let mut data1 = &mut input; let mut data2 = &mut output; for _ in 0..m { box_filter(data1, data2, wl); std::mem::swap(&mut data1, &mut data2); } for _ in m..n { box_filter(data1, data2, wu); std::mem::swap(&mut data1, &mut data2); } if data1.as_ptr() != output.as_ptr() { output.copy_from_slice(input); } } fn box_filter(input: &[f64], output: &mut [f64], w: usize) { let size = input.len(); if w == 0 || size == 0 { return; } let wl = w / 2; let wr = w - wl; let mut it1 = ReflectIterator::new(size); let mut it2 = ReflectIterator::new(size); for _ in 0..wl { it1.move_back(); it2.move_back(); } let mut sum = 0.0; for _ in 0..w { sum += input[it2.pos]; it2.move_forward(); } let mut out = SliceWriter::new(output); if size > w { for _ in 0..wl { out.push(sum / w as f64); sum += input[it2.pos] - input[it1.pos]; it1.move_forward(); it2.move_forward(); } for _ in 0..size - w - 1 { out.push(sum / w as f64); sum += input[it2.pos] - input[it1.pos]; it2.pos += 1; it1.pos += 1; } for _ in 0..wr + 1 { out.push(sum / w as f64); sum += input[it2.pos] - input[it1.pos]; it1.move_forward(); it2.move_forward(); } } else { for _ in 0..size { out.push(sum / w as f64); sum += input[it2.pos] - input[it1.pos]; it1.move_forward(); it2.move_forward(); } } } struct SliceWriter<'a, T> { slice: &'a mut [T], index: usize, } impl<'a, T> SliceWriter<'a, T> { fn new(slice: &'a mut [T]) -> Self { Self { slice, index: 0 } } fn push(&mut self, value: T) { self.slice[self.index] = value; self.index += 1; } } struct ReflectIterator { size: usize, pos: usize, forward: bool, } impl ReflectIterator { fn new(size: usize) -> Self { Self { size, pos: 0, forward: true, } } fn move_forward(&mut self) { if self.forward { if self.pos + 1 == self.size { self.forward = false; } else { self.pos += 1; } } else if self.pos == 0 { self.forward = true; } else { self.pos -= 1; } } fn move_back(&mut self) { if self.forward { if self.pos == 0 { self.forward = false; } else { self.pos -= 1; } } else if self.pos + 1 == self.size { self.forward = true; } else { self.pos += 1; } } #[cfg(test)] fn safe_forward_distance(&mut self) -> usize { if self.forward { return self.size - self.pos - 1; } return 0; } } #[cfg(test)] mod tests { use crate::assert_eq_float; use crate::gaussian::{box_filter, gaussian_filter, ReflectIterator}; #[test] fn reflect_iterator() { let data = [1, 2, 3, 4, 5, 6, 7, 8, 9]; let mut it = ReflectIterator::new(data.len()); for _ in 0..3 { it.move_back(); } assert_eq!(3, data[it.pos]); assert_eq!(0, it.safe_forward_distance()); it.move_forward(); assert_eq!(2, data[it.pos]); assert_eq!(0, it.safe_forward_distance()); it.move_forward(); assert_eq!(1, data[it.pos]); assert_eq!(0, it.safe_forward_distance()); it.move_forward(); assert_eq!(1, data[it.pos]); assert_eq!(8, it.safe_forward_distance()); it.move_forward(); assert_eq!(2, data[it.pos]); } #[test] fn width1() { let input = [1.0, 2.0, 4.0]; let mut output = input.clone(); box_filter(&input, &mut output, 1); assert_eq!(input.len(), output.len()); assert_eq_float!(1.0, output[0]); assert_eq_float!(2.0, output[1]); assert_eq_float!(4.0, output[2]); } #[test] fn width2() { let input = [1.0, 2.0, 4.0]; let mut output = input.clone(); box_filter(&input, &mut output, 2); assert_eq!(input.len(), output.len()); assert_eq_float!(1.0, output[0]); assert_eq_float!(1.5, output[1]); assert_eq_float!(3.0, output[2]); } #[test] fn width3() { let input = [1.0, 2.0, 4.0]; let mut output = input.clone(); box_filter(&input, &mut output, 3); assert_eq!(input.len(), output.len()); assert_eq_float!(1.333333333, output[0]); assert_eq_float!(2.333333333, output[1]); assert_eq_float!(3.333333333, output[2]); } #[test] fn width4() { let input = [1.0, 2.0, 4.0]; let mut output = input.clone(); box_filter(&input, &mut output, 4); assert_eq!(input.len(), output.len()); assert_eq_float!(1.5, output[0]); assert_eq_float!(2.0, output[1]); assert_eq_float!(2.75, output[2]); } #[test] fn width5() { let input = [1.0, 2.0, 4.0]; let mut output = input.clone(); box_filter(&input, &mut output, 5); assert_eq!(input.len(), output.len()); assert_eq_float!(2.0, output[0]); assert_eq_float!(2.4, output[1]); assert_eq_float!(2.6, output[2]); } #[test] fn gaussian1() { let mut input = [1.0, 2.0, 4.0]; let mut output = input.clone(); gaussian_filter(&mut input, &mut output, 1.6, 3); assert_eq!(input.len(), output.len()); assert_eq_float!(1.88888889, output[0]); assert_eq_float!(2.33333333, output[1]); assert_eq_float!(2.77777778, output[2]); } #[test] fn gaussian2() { let mut input = [1.0, 2.0, 4.0]; let mut output = input.clone(); gaussian_filter(&mut input, &mut output, 3.6, 4); assert_eq!(input.len(), output.len()); assert_eq_float!(2.3322449, output[0]); assert_eq_float!(2.33306122, output[1]); assert_eq_float!(2.33469388, output[2]); } } rusty-chromaprint-0.3.0/src/gradient.rs000064400000000000000000000043031046102023000162700ustar 00000000000000pub fn gradient(mut iter: impl Iterator, output: &mut Vec) { if let Some(mut f0) = iter.next() { if let Some(mut f1) = iter.next() { output.push(f1 - f0); if let Some(mut f2) = iter.next() { output.push((f2 - f0) / 2.0); for next in iter { f0 = f1; f1 = f2; f2 = next; output.push((f2 - f0) / 2.0); } output.push(f2 - f1); } else { output.push(f1 - f0); } } else { output.push(0.0); } } } #[cfg(test)] mod tests { use crate::assert_eq_float; use crate::gradient::gradient; #[test] fn empty() { let input = []; let mut output = Vec::new(); gradient(input.into_iter(), &mut output); assert_eq!(0, output.len()); } #[test] fn one_element() { let mut output = Vec::new(); let input = [1.0]; gradient(input.into_iter(), &mut output); assert_eq!(1, output.len()); assert_eq_float!(0.0, output[0]); } #[test] fn two_elements() { let mut output = Vec::new(); let input = [1.0, 2.0]; gradient(input.into_iter(), &mut output); assert_eq!(2, output.len()); assert_eq_float!(1.0, output[0]); assert_eq_float!(1.0, output[1]); } #[test] fn three_elements() { let mut output = Vec::new(); let input = [1.0, 2.0, 4.0]; gradient(input.into_iter(), &mut output); assert_eq!(3, output.len()); assert_eq_float!(1.0, output[0]); assert_eq_float!(1.5, output[1]); assert_eq_float!(2.0, output[2]); } #[test] fn four_elements() { let mut output = Vec::new(); let input = [1.0, 2.0, 4.0, 10.0]; gradient(input.into_iter(), &mut output); assert_eq!(4, output.len()); assert_eq_float!(1.0, output[0]); assert_eq_float!(1.5, output[1]); assert_eq_float!(4.0, output[2]); assert_eq_float!(6.0, output[3]); } } rusty-chromaprint-0.3.0/src/lib.rs000064400000000000000000000011151046102023000152370ustar 00000000000000//! Pure Rust port of [chromaprint](https://acoustid.org/chromaprint) pub use audio_processor::ResetError; pub use compression::FingerprintCompressor; pub use fingerprint_matcher::{match_fingerprints, MatchError, Segment}; pub use fingerprinter::{Configuration, Fingerprinter}; mod audio_processor; mod chroma; mod chroma_filter; mod chroma_normalizer; mod classifier; mod compression; mod fft; mod filter; mod fingerprint_calculator; mod fingerprint_matcher; mod fingerprinter; mod gaussian; mod gradient; mod quantize; mod rolling_image; mod stages; mod utils; rusty-chromaprint-0.3.0/src/quantize.rs000064400000000000000000000020251046102023000163320ustar 00000000000000#[derive(Debug, Clone, Copy)] pub struct Quantizer { t0: f64, t1: f64, t2: f64, } impl Quantizer { pub const fn new(t0: f64, t1: f64, t2: f64) -> Self { // assert!(t0 <= t1 && t1 <= t2); Self { t0, t1, t2 } } pub fn quantize(&self, val: f64) -> u32 { if val < self.t1 { if val < self.t0 { 0 } else { 1 } } else if val < self.t2 { 2 } else { 3 } } } #[cfg(test)] mod tests { use crate::quantize::Quantizer; #[test] fn quantization() { let q = Quantizer::new(0.0, 0.1, 0.3); assert_eq!(0, q.quantize(-0.1)); assert_eq!(1, q.quantize(0.0)); assert_eq!(1, q.quantize(0.03)); assert_eq!(2, q.quantize(0.1)); assert_eq!(2, q.quantize(0.13)); assert_eq!(3, q.quantize(0.3)); assert_eq!(3, q.quantize(0.33)); assert_eq!(3, q.quantize(1000.0)); } } rusty-chromaprint-0.3.0/src/rolling_image.rs000064400000000000000000000132471046102023000173120ustar 00000000000000use crate::filter::Image; pub struct RollingIntegralImage { max_rows: usize, columns: usize, rows: usize, data: Vec, } impl RollingIntegralImage { pub fn new(max_rows: usize) -> Self { Self { max_rows: max_rows + 1, columns: 0, rows: 0, data: Vec::new(), } } #[cfg(test)] pub fn from_data(columns: usize, data: &[D]) -> Self where D: Copy + Into, { let mut image = Self { max_rows: data.len() / columns, columns: 0, rows: 0, data: Vec::with_capacity(data.len()), }; for row in data.chunks_exact(columns) { image.add_row(row); } image } pub(crate) fn add_row(&mut self, row: &[T]) where T: Copy + Into, { if self.columns == 0 { self.columns = row.len(); self.data.resize(self.max_rows * self.columns, 0.0); } assert_eq!(self.columns, row.len()); let mut sum = 0.0; for (i, &cell) in row.iter().enumerate().take(self.columns) { sum += cell.into(); self.row_mut(self.rows)[i] = sum; } if self.rows > 0 { for i in 0..self.columns { self.row_mut(self.rows)[i] += self.row(self.rows - 1)[i]; } } self.rows += 1; } #[cfg(test)] fn columns(&self) -> usize { self.columns } pub(crate) fn rows(&self) -> usize { self.rows } fn row(&self, mut i: usize) -> &[f64] { i %= self.max_rows; &self.data[i * self.columns..][..self.columns] } fn row_mut(&mut self, mut i: usize) -> &mut [f64] { i %= self.max_rows; &mut self.data[i * self.columns..][..self.columns] } pub(crate) fn reset(&mut self) { self.data.clear(); self.rows = 0; self.columns = 0; } } impl Image for RollingIntegralImage { fn area(&self, r1: usize, c1: usize, r2: usize, c2: usize) -> f64 { assert!(r1 <= self.rows); assert!(r2 <= self.rows); if self.rows > self.max_rows { assert!(r1 > self.rows - self.max_rows); assert!(r2 > self.rows - self.max_rows); } assert!(c1 <= self.columns); assert!(c2 <= self.columns); if r1 == r2 || c1 == c2 { return 0.0; } assert!(r2 > r1); assert!(c2 > c1); if r1 == 0 { let row = self.row(r2 - 1); if c1 == 0 { row[c2 - 1] } else { row[c2 - 1] - row[c1 - 1] } } else { let row1 = self.row(r1 - 1); let row2 = self.row(r2 - 1); if c1 == 0 { row2[c2 - 1] - row1[c2 - 1] } else { row2[c2 - 1] - row1[c2 - 1] - row2[c1 - 1] + row1[c1 - 1] } } } } #[cfg(test)] mod tests { use crate::assert_eq_float; use crate::filter::Image; use crate::rolling_image::RollingIntegralImage; #[test] fn simple() { let mut image = RollingIntegralImage::new(4); image.add_row(&[1, 2, 3]); assert_eq!(3, image.columns()); assert_eq!(1, image.rows()); assert_eq_float!(1.0, image.area(0, 0, 1, 1)); assert_eq_float!(2.0, image.area(0, 1, 1, 2)); assert_eq_float!(3.0, image.area(0, 2, 1, 3)); assert_eq_float!(1.0 + 2.0 + 3.0, image.area(0, 0, 1, 3)); image.add_row(&[4, 5, 6]); assert_eq!(3, image.columns()); assert_eq!(2, image.rows()); assert_eq_float!(4.0, image.area(1, 0, 2, 1)); assert_eq_float!(5.0, image.area(1, 1, 2, 2)); assert_eq_float!(6.0, image.area(1, 2, 2, 3)); assert_eq_float!(1.0 + 2.0 + 3.0 + 4.0 + 5.0 + 6.0, image.area(0, 0, 2, 3)); image.add_row(&[7, 8, 9]); assert_eq!(3, image.columns()); assert_eq!(3, image.rows()); image.add_row(&[10, 11, 12]); assert_eq!(3, image.columns()); assert_eq!(4, image.rows()); assert_eq_float!( (1.0 + 2.0 + 3.0) + (4.0 + 5.0 + 6.0) + (7.0 + 8.0 + 9.0) + (10.0 + 11.0 + 12.0), image.area(0, 0, 4, 3) ); image.add_row(&[13, 14, 15]); assert_eq!(3, image.columns()); assert_eq!(5, image.rows()); assert_eq_float!(4.0, image.area(1, 0, 2, 1)); assert_eq_float!(5.0, image.area(1, 1, 2, 2)); assert_eq_float!(6.0, image.area(1, 2, 2, 3)); assert_eq_float!(13.0, image.area(4, 0, 5, 1)); assert_eq_float!(14.0, image.area(4, 1, 5, 2)); assert_eq_float!(15.0, image.area(4, 2, 5, 3)); assert_eq_float!( (4.0 + 5.0 + 6.0) + (7.0 + 8.0 + 9.0) + (10.0 + 11.0 + 12.0) + (13.0 + 14.0 + 15.0), image.area(1, 0, 5, 3) ); image.add_row(&[16, 17, 18]); assert_eq!(3, image.columns()); assert_eq!(6, image.rows()); assert_eq_float!(7.0, image.area(2, 0, 3, 1)); assert_eq_float!(8.0, image.area(2, 1, 3, 2)); assert_eq_float!(9.0, image.area(2, 2, 3, 3)); assert_eq_float!(16.0, image.area(5, 0, 6, 1)); assert_eq_float!(17.0, image.area(5, 1, 6, 2)); assert_eq_float!(18.0, image.area(5, 2, 6, 3)); assert_eq_float!( (7.0 + 8.0 + 9.0) + (10.0 + 11.0 + 12.0) + (13.0 + 14.0 + 15.0) + (16.0 + 17.0 + 18.0), image.area(2, 0, 6, 3) ); } } rusty-chromaprint-0.3.0/src/stages.rs000064400000000000000000000022031046102023000157560ustar 00000000000000pub trait Stage { type Output: ?Sized; fn output(&self) -> &Self::Output; } impl Stage for &mut C { type Output = C::Output; fn output(&self) -> &Self::Output { (**self).output() } } pub trait AudioConsumer: Stage { fn reset(&mut self); fn consume(&mut self, data: &[T]); fn flush(&mut self); } impl Stage for Box { type Output = S::Output; fn output(&self) -> &Self::Output { (**self).output() } } impl + ?Sized> AudioConsumer for Box { fn reset(&mut self) { (**self).reset(); } fn consume(&mut self, data: &[T]) { (**self).consume(data); } fn flush(&mut self) { (**self).flush(); } } pub trait FeatureVectorConsumer: Stage { fn consume(&mut self, features: &[f64]); fn reset(&mut self); } impl FeatureVectorConsumer for &mut C { fn consume(&mut self, features: &[f64]) { (**self).consume(features); } fn reset(&mut self) { (**self).reset(); } } rusty-chromaprint-0.3.0/src/utils.rs000064400000000000000000000016341046102023000156370ustar 00000000000000use std::path::Path; #[doc(hidden)] #[macro_export] macro_rules! assert_eq_float { ($a:expr, $b:expr) => { assert_eq_float!($a, $b, 0.00001); }; ($a:expr, $b:expr, $eps:expr) => { assert!(($a - $b).abs() < $eps); }; } #[doc(hidden)] #[macro_export] macro_rules! assert_eq_float_slice { ($a:expr, $b:expr) => { $crate::assert_eq_float_slice!($a, $b, 0.00001); }; ($a:expr, $b:expr, $eps:expr) => { let a = $a; let b = $b; assert_eq!(a.len(), b.len()); for (a, b) in std::iter::zip(a, b) { $crate::assert_eq_float!(a, b, $eps); } }; } #[allow(unused)] pub(crate) fn read_s16le(path: impl AsRef) -> Vec { std::fs::read(path) .unwrap() .chunks_exact(2) .map(|chunk| i16::from_le_bytes([chunk[0], chunk[1]])) .collect::>() }