diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000000..0432fbc8cc2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,433 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "apodize" +version = "1.0.0" +source = "git+https://github.com/hotg-ai/apodize?rev=41baaee092a9b49e26442d946046a8c3ec3d9ccf#41baaee092a9b49e26442d946046a8c3ec3d9ccf" +dependencies = [ + "libm", +] + +[[package]] +name = "approx" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "072df7202e63b127ab55acfe16ce97013d5b97bf160489336d3f1840fd78e99e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "audio_float_conversion" +version = "0.8.0" +dependencies = [ + "hotg-rune-proc-blocks", + "pretty_assertions", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ctor" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "diff" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" + +[[package]] +name = "fft" +version = "0.8.0" +dependencies = [ + "hotg-rune-proc-blocks", + "hound", + "libm", + "mel", + "nalgebra", + "normalize", + "pretty_assertions", + "sonogram", +] + +[[package]] +name = "hertz" +version = "0.3.0" +source = "git+https://github.com/hotg-ai/hertz?rev=707d6d3c239663f04cc22f33391fbe54833189db#707d6d3c239663f04cc22f33391fbe54833189db" +dependencies = [ + "libm", +] + +[[package]] +name = "hotg-rune-core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f49d3938dbb26cfcfad99dcedce6c287b70b4519dcd4ab2b6b12a1ea022cb4" +dependencies = [ + "log", + "serde", +] + +[[package]] +name = "hotg-rune-proc-block-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572c7e954b0bec352339e0e6bc96edbcff33e44ea668c3dbbe0ae4b133d3cf88" +dependencies = [ + "hotg-rune-core", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", +] + +[[package]] +name = "hotg-rune-proc-blocks" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5119da997d4b2e19072720ea81bc62d83ef7f39e04f8eba0f935dea0265f651c" +dependencies = [ + "hotg-rune-core", + "hotg-rune-proc-block-macros", + "serde", +] + +[[package]] +name = "hound" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" + +[[package]] +name = "image-normalization" +version = "0.8.0" +dependencies = [ + "hotg-rune-proc-blocks", + "num-traits", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "label" +version = "0.8.0" +dependencies = [ + "hotg-rune-proc-blocks", +] + +[[package]] +name = "libm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", + "serde", +] + +[[package]] +name = "mel" +version = "0.3.0" +source = "git+https://github.com/hotg-ai/mel?rev=017694ee3143c11ea9b75ba6cd27fe7c8a69a867#017694ee3143c11ea9b75ba6cd27fe7c8a69a867" +dependencies = [ + "apodize", + "hertz", + "num", + "num-traits", +] + +[[package]] +name = "modulo" +version = "0.8.0" +dependencies = [ + "hotg-rune-proc-blocks", + "num-traits", +] + +[[package]] +name = "most_confident_indices" +version = "0.8.0" +dependencies = [ + "hotg-rune-proc-blocks", +] + +[[package]] +name = "nalgebra" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d506eb7e08d6329505faa8a3a00a5dcc6de9f76e0c77e4b75763ae3c770831ff" +dependencies = [ + "approx", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", +] + +[[package]] +name = "noise-filtering" +version = "0.8.0" +dependencies = [ + "hotg-rune-proc-blocks", + "libm", + "paste", +] + +[[package]] +name = "normalize" +version = "0.8.0" +dependencies = [ + "hotg-rune-proc-blocks", +] + +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "output_vt100" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" +dependencies = [ + "winapi", +] + +[[package]] +name = "paste" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" + +[[package]] +name = "pretty_assertions" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cab0e7c02cf376875e9335e0ba1da535775beb5450d21e1dffca068818ed98b" +dependencies = [ + "ansi_term", + "ctor", + "diff", + "output_vt100", +] + +[[package]] +name = "proc-macro2" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "simba" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b7840f121a46d63066ee7a99fc81dcabbc6105e437cae43528cea199b5a05f" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", +] + +[[package]] +name = "sonogram" +version = "0.4.4" +source = "git+https://github.com/hotg-ai/sonogram?rev=009bc0cba44267d8a0807e43c9bb0712f0f334ea#009bc0cba44267d8a0807e43c9bb0712f0f334ea" +dependencies = [ + "libm", + "num", +] + +[[package]] +name = "syn" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "typenum" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index bdaba833748..ba5f59728ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1 +1,11 @@ [workspace] +members = [ + "fft", + "audio_float_conversion", + "image-normalization", + "label", + "modulo", + "most_confident_indices", + "noise-filtering", + "normalize", +] diff --git a/audio_float_conversion/Cargo.toml b/audio_float_conversion/Cargo.toml new file mode 100644 index 00000000000..618d219e43d --- /dev/null +++ b/audio_float_conversion/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "audio_float_conversion" +version = "0.8.0" +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +hotg-rune-proc-blocks = "^0.8.0" + +[dev-dependencies] +pretty_assertions = "0.7.2" diff --git a/audio_float_conversion/src/lib.rs b/audio_float_conversion/src/lib.rs new file mode 100644 index 00000000000..99fc5189e22 --- /dev/null +++ b/audio_float_conversion/src/lib.rs @@ -0,0 +1,91 @@ +#![no_std] + +extern crate alloc; + +#[cfg(test)] +#[macro_use] +extern crate std; + +use hotg_rune_proc_blocks::{ProcBlock, Transform, Tensor}; + +// TODO: Add Generics + +#[derive(Debug, Clone, PartialEq, ProcBlock)] +#[transform(inputs = [i16; _], outputs = [f32; _])] +pub struct AudioFloatConversion { + i16_max_as_float: f32, +} + +const I16_MAX_AS_FLOAT: f32 = i16::MAX as f32; + +impl AudioFloatConversion { + pub const fn new() -> Self { + AudioFloatConversion { + i16_max_as_float: I16_MAX_AS_FLOAT, + } + } + + fn check_input_dimensions(&self, dimensions: &[usize]) { + assert_eq!( + dimensions.len(), + 1, + "This proc block only supports 1D outputs (requested output: {:?})", + dimensions + ); + } +} + +impl Default for AudioFloatConversion { + fn default() -> Self { AudioFloatConversion::new() } +} + +impl Transform> for AudioFloatConversion { + type Output = Tensor; + + fn transform(&mut self, input: Tensor) -> Self::Output { + self.check_input_dimensions(input.dimensions()); + input.map(|_dims, &value| { + (value as f32 / I16_MAX_AS_FLOAT).clamp(-1.0, 1.0) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn handle_empty() { + let mut pb = AudioFloatConversion::new(); + let input = Tensor::new_vector(vec![0; 15]); + + let got = pb.transform(input); + + assert_eq!(got.dimensions(), &[15]); + } + + #[test] + fn does_it_match() { + let max = i16::MAX; + let min = i16::MIN; + + let mut pb = AudioFloatConversion::new(); + let input = Tensor::new_vector(vec![0, max / 2, min / 2]); + + let got = pb.transform(input); + + assert_eq!(got.elements()[0..3], [0.0, 0.49998474, -0.50001526]); + } + #[test] + fn does_clutch_work() { + let max = i16::MAX; + let min = i16::MIN; + + let mut pb = AudioFloatConversion::new(); + let input = Tensor::new_vector(vec![max, min, min + 1]); + + let got = pb.transform(input); + + assert_eq!(got.elements()[0..3], [1.0, -1.0, -1.0]); + } +} diff --git a/fft/Cargo.toml b/fft/Cargo.toml new file mode 100644 index 00000000000..ecd2f634603 --- /dev/null +++ b/fft/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "fft" +version = "0.8.0" +authors = ["The Rune Developers "] +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +hotg-rune-proc-blocks = "^0.8.0" +hound = "3.4" +libm = "0.2.1" +# See https://github.com/hotg-ai/rune/pull/107#issuecomment-825806000 +mel = { git = "https://github.com/hotg-ai/mel", rev = "017694ee3143c11ea9b75ba6cd27fe7c8a69a867", default-features = false } +nalgebra = { version = "0.29", default-features = false, features = ["alloc"] } +normalize = { path = "../normalize", version = "^0.8.0" } +sonogram = {git = "https://github.com/hotg-ai/sonogram", rev = "009bc0cba44267d8a0807e43c9bb0712f0f334ea" } + +[dev-dependencies] +pretty_assertions = "0.7.2" diff --git a/fft/src/lib.rs b/fft/src/lib.rs new file mode 100644 index 00000000000..0d7d2dfa345 --- /dev/null +++ b/fft/src/lib.rs @@ -0,0 +1,137 @@ +#![no_std] + +extern crate alloc; + +#[cfg(test)] +#[macro_use] +extern crate std; +#[cfg(test)] +#[macro_use] +extern crate pretty_assertions; + +/// A type alias for [`ShortTimeFourierTransform`] which uses the camel case +/// version of this crate. +pub type Fft = ShortTimeFourierTransform; + +use alloc::{sync::Arc, vec::Vec}; +use hotg_rune_proc_blocks::{ProcBlock, Transform, Tensor}; +use sonogram::SpecOptionsBuilder; +use nalgebra::DMatrix; + +#[derive(Debug, Clone, PartialEq, ProcBlock)] +pub struct ShortTimeFourierTransform { + sample_rate: u32, + bins: usize, + window_overlap: f32, +} + +const DEFAULT_SAMPLE_RATE: u32 = 16000; +const DEFAULT_BINS: usize = 480; +const DEFAULT_WINDOW_OVERLAP: f32 = 0.6666667; + +impl ShortTimeFourierTransform { + pub const fn new() -> Self { + ShortTimeFourierTransform { + sample_rate: DEFAULT_SAMPLE_RATE, + bins: DEFAULT_BINS, + window_overlap: DEFAULT_WINDOW_OVERLAP, + } + } + + fn transform_inner(&mut self, input: Vec) -> [u32; 1960] { + // Build the spectrogram computation engine + let mut spectrograph = SpecOptionsBuilder::new(49, 241) + .set_window_fn(sonogram::hann_function) + .load_data_from_memory(input, self.sample_rate as u32) + .build(); + + // Compute the spectrogram giving the number of bins in a window and the + // overlap between neighbour windows. + spectrograph.compute(self.bins, self.window_overlap); + + let spectrogram = spectrograph.create_in_memory(false); + + let filter_count: usize = 40; + let power_spectrum_size = 241; + let window_size = 480; + let sample_rate_usize: usize = 16000; + + // build up the mel filter matrix + let mut mel_filter_matrix = + DMatrix::::zeros(filter_count, power_spectrum_size); + for (row, col, coefficient) in mel::enumerate_mel_scaling_matrix( + sample_rate_usize, + window_size, + power_spectrum_size, + filter_count, + ) { + mel_filter_matrix[(row, col)] = coefficient; + } + + let spectrogram = spectrogram.into_iter().map(f64::from); + let power_spectrum_matrix_unflipped: DMatrix = + DMatrix::from_iterator(49, power_spectrum_size, spectrogram); + let power_spectrum_matrix_transposed = + power_spectrum_matrix_unflipped.transpose(); + let mut power_spectrum_vec: Vec<_> = + power_spectrum_matrix_transposed.row_iter().collect(); + power_spectrum_vec.reverse(); + let power_spectrum_matrix: DMatrix = + DMatrix::from_rows(&power_spectrum_vec); + let mel_spectrum_matrix = &mel_filter_matrix * &power_spectrum_matrix; + let mel_spectrum_matrix = mel_spectrum_matrix.map(libm::sqrt); + + let min_value = mel_spectrum_matrix + .data + .as_vec() + .iter() + .fold(f64::INFINITY, |a, &b| a.min(b)); + let max_value = mel_spectrum_matrix + .data + .as_vec() + .iter() + .fold(f64::NEG_INFINITY, |a, &b| a.max(b)); + + let res: Vec = mel_spectrum_matrix + .data + .as_vec() + .iter() + .map(|freq| 65536.0 * (freq - min_value) / (max_value - min_value)) + .map(|freq| freq as u32) + .collect(); + + let mut out = [0; 1960]; + out.copy_from_slice(&res[..1960]); + out + } +} + +impl Default for ShortTimeFourierTransform { + fn default() -> Self { ShortTimeFourierTransform::new() } +} + +impl Transform> for ShortTimeFourierTransform { + type Output = Tensor; + + fn transform(&mut self, input: Tensor) -> Self::Output { + let input = input.elements().to_vec(); + let stft = self.transform_inner(input); + Tensor::new_row_major(Arc::new(stft), alloc::vec![1, stft.len()]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let mut fft_pb = ShortTimeFourierTransform::new(); + fft_pb.set_sample_rate(16000); + let input = Tensor::new_vector(vec![0; 16000]); + + let got = fft_pb.transform(input); + + assert_eq!(got.dimensions(), &[1, 1960]); + } +} diff --git a/image-normalization/Cargo.toml b/image-normalization/Cargo.toml new file mode 100644 index 00000000000..333e784f8a1 --- /dev/null +++ b/image-normalization/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "image-normalization" +version = "0.8.0" +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +num-traits = { version = "0.2.14", default-features = false } +hotg-rune-proc-blocks = "^0.8.0" diff --git a/image-normalization/src/lib.rs b/image-normalization/src/lib.rs new file mode 100644 index 00000000000..5a7c1268004 --- /dev/null +++ b/image-normalization/src/lib.rs @@ -0,0 +1,76 @@ +#![no_std] + +#[cfg(test)] +#[macro_use] +extern crate alloc; + +use num_traits::{Bounded, ToPrimitive}; +use hotg_rune_proc_blocks::{ProcBlock, Transform, Tensor}; + +/// A normalization routine which takes some tensor of integers and fits their +/// values to the range `[0, 1]` as `f32`'s. +#[derive(Debug, Default, Clone, PartialEq, ProcBlock)] +#[non_exhaustive] +#[transform(inputs = [u8; _], outputs = [f32; _])] +#[transform(inputs = [i8; _], outputs = [f32; _])] +#[transform(inputs = [u16; _], outputs = [f32; _])] +#[transform(inputs = [i16; _], outputs = [f32; _])] +#[transform(inputs = [u32; _], outputs = [f32; _])] +#[transform(inputs = [i32; _], outputs = [f32; _])] +pub struct ImageNormalization {} + +impl ImageNormalization { + fn check_input_dimensions(&self, dimensions: &[usize]) { + match *dimensions { + [_, _, _, 3] => {}, + [_, _, _, channels] => panic!( + "The number of channels should be either 1 or 3, found {}", + channels + ), + _ => panic!("The image normalization proc block only supports outputs of the form [frames, rows, columns, channels], found {:?}", dimensions), + } + } +} + +impl Transform> for ImageNormalization +where + T: Bounded + ToPrimitive + Copy, +{ + type Output = Tensor; + + fn transform(&mut self, input: Tensor) -> Self::Output { + self.check_input_dimensions(input.dimensions()); + input.map(|_, &value| normalize(value).expect("Cast should never fail")) + } +} + +fn normalize(value: T) -> Option +where + T: Bounded + ToPrimitive, +{ + let min = T::min_value().to_f32()?; + let max = T::max_value().to_f32()?; + let value = value.to_f32()?; + debug_assert!(min <= value && value <= max); + + Some((value - min) / (max - min)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalizing_with_default_distribution_is_noop() { + let dims = vec![1, 1, 1, 3]; + let input: Tensor = + Tensor::new_row_major(vec![0, 127, 255].into(), dims.clone()); + let mut norm = ImageNormalization::default(); + let should_be: Tensor = + Tensor::new_row_major(vec![0.0, 127.0 / 255.0, 1.0].into(), dims); + + let got = norm.transform(input); + + assert_eq!(got, should_be); + } +} diff --git a/label/Cargo.toml b/label/Cargo.toml new file mode 100644 index 00000000000..7b3faacddae --- /dev/null +++ b/label/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "label" +version = "0.8.0" +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +hotg-rune-proc-blocks = "^0.8.0" diff --git a/label/src/lib.rs b/label/src/lib.rs new file mode 100644 index 00000000000..e3e009b94ea --- /dev/null +++ b/label/src/lib.rs @@ -0,0 +1,93 @@ +#![no_std] + +extern crate alloc; + +use core::{convert::TryInto, fmt::Debug}; + +use alloc::vec::Vec; +use hotg_rune_proc_blocks::{Tensor, Transform, ProcBlock}; + +/// A proc block which, when given a set of indices, will return their +/// associated labels. +/// +/// # Examples +/// +/// ```rust +/// # use label::Label; +/// # use hotg_rune_proc_blocks::{Transform, Tensor}; +/// let mut proc_block = Label::default(); +/// proc_block.set_labels(["zero", "one", "two", "three"]); +/// let input = Tensor::new_vector(vec![3, 1, 2]); +/// +/// let got = proc_block.transform(input); +/// +/// assert_eq!(got.elements(), &["three", "one", "two"]); +/// ``` +#[derive(Debug, Default, Clone, PartialEq, ProcBlock)] +pub struct Label { + labels: Vec<&'static str>, +} + +impl Transform> for Label +where + T: Copy + TryInto, + >::Error: Debug, +{ + type Output = Tensor<&'static str>; + + fn transform(&mut self, input: Tensor) -> Self::Output { + let view = input + .view::<1>() + .expect("This proc block only supports 1D inputs"); + + let indices = view.elements().iter().copied().map(|ix| { + ix.try_into() + .expect("Unable to convert the index to a usize") + }); + + // Note: We use a more cumbersome match statement instead of unwrap() + // to provide the user with more useful error messages + indices + .map(|ix| match self.labels.get(ix) { + Some(&label) => label, + None => panic!("Index out of bounds: there are {} labels but label {} was requested", self.labels.len(), ix) + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic] + fn only_works_with_1d_inputs() { + let mut proc_block = Label::default(); + let input: Tensor = Tensor::zeroed(alloc::vec![1, 2, 3]); + + let _ = proc_block.transform(input); + } + + #[test] + #[should_panic = "Index out of bounds: there are 2 labels but label 42 was requested"] + fn label_index_out_of_bounds() { + let mut proc_block = Label::default(); + proc_block.set_labels(["first", "second"]); + let input = Tensor::new_vector(alloc::vec![0_usize, 42]); + + let _ = proc_block.transform(input); + } + + #[test] + fn get_the_correct_labels() { + let mut proc_block = Label::default(); + proc_block.set_labels(["zero", "one", "two", "three"]); + let input = Tensor::new_vector(alloc::vec![3, 1, 2]); + let should_be = Tensor::new_vector(alloc::vec!["three", "one", "two"]); + + let got = proc_block.transform(input); + + assert_eq!(got, should_be); + } +} diff --git a/modulo/Cargo.toml b/modulo/Cargo.toml new file mode 100644 index 00000000000..a38b2da1e18 --- /dev/null +++ b/modulo/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "modulo" +version = "0.8.0" +authors = ["The Rune Developers "] +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +num-traits = { version = "0.2.14", default-features = false } +hotg-rune-proc-blocks = "^0.8.0" diff --git a/modulo/src/lib.rs b/modulo/src/lib.rs new file mode 100644 index 00000000000..9871e517306 --- /dev/null +++ b/modulo/src/lib.rs @@ -0,0 +1,60 @@ +#![no_std] + +use num_traits::{FromPrimitive, ToPrimitive}; +use hotg_rune_proc_blocks::{Tensor, Transform, ProcBlock}; + +pub fn modulo(modulus: f32, values: &mut [T]) +where + T: ToPrimitive + FromPrimitive, +{ + for item in values { + let float = item.to_f32().unwrap(); + *item = T::from_f32(float % modulus).unwrap(); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, ProcBlock)] +pub struct Modulo { + modulus: f32, +} + +impl Modulo { + pub fn new() -> Self { Modulo { modulus: 1.0 } } +} + +impl Default for Modulo { + fn default() -> Self { Modulo::new() } +} + +impl<'a, T> Transform> for Modulo +where + T: ToPrimitive + FromPrimitive, +{ + type Output = Tensor; + + fn transform(&mut self, input: Tensor) -> Tensor { + let modulus = self.modulus; + + input.map(|_, item| { + let float = item.to_f32().unwrap(); + T::from_f32(float % modulus).unwrap() + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mod_360() { + let number = 42 + 360; + let mut m = Modulo::new(); + m.set_modulus(360.0); + let input = Tensor::single(number); + + let got = m.transform(input); + + assert_eq!(got, Tensor::single(42_i64)); + } +} diff --git a/most_confident_indices/Cargo.toml b/most_confident_indices/Cargo.toml new file mode 100644 index 00000000000..071a6550248 --- /dev/null +++ b/most_confident_indices/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "most_confident_indices" +version = "0.8.0" +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +hotg-rune-proc-blocks = "^0.8.0" diff --git a/most_confident_indices/src/lib.rs b/most_confident_indices/src/lib.rs new file mode 100644 index 00000000000..701bd5c9d02 --- /dev/null +++ b/most_confident_indices/src/lib.rs @@ -0,0 +1,120 @@ +#![no_std] + +extern crate alloc; + +use core::{convert::TryInto, fmt::Debug}; + +use alloc::vec::Vec; +use hotg_rune_proc_blocks::{ProcBlock, Transform, Tensor}; + +/// A proc block which, when given a list of confidences, will return the +/// indices of the top N most confident values. +/// +/// Will return a 1-element [`Tensor`] by default. +#[derive(Debug, Clone, PartialEq, ProcBlock)] +pub struct MostConfidentIndices { + /// The number of indices to return. + count: usize, +} + +impl MostConfidentIndices { + pub fn new(count: usize) -> Self { MostConfidentIndices { count } } + + fn check_input_dimensions(&self, dimensions: &[usize]) { + match simplify_dimensions(dimensions) { + [count] => assert!( + self.count <= *count, + "Unable to take the top {} values from a {}-item input", + self.count, + *count + ), + other => panic!( + "This proc-block only works with 1D inputs, but found {:?}", + other + ), + } + } +} + +impl Default for MostConfidentIndices { + fn default() -> Self { MostConfidentIndices::new(1) } +} + +impl Transform> for MostConfidentIndices { + type Output = Tensor; + + fn transform(&mut self, input: Tensor) -> Self::Output { + let elements = input.elements(); + self.check_input_dimensions(input.dimensions()); + + let mut indices_and_confidence: Vec<_> = + elements.iter().copied().enumerate().collect(); + + indices_and_confidence.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); + + indices_and_confidence + .into_iter() + .map(|(index, _confidence)| index.try_into().unwrap()) + .take(self.count) + .collect() + } +} + +fn simplify_dimensions(mut dimensions: &[usize]) -> &[usize] { + while let Some(rest) = dimensions.strip_prefix(&[1]) { + dimensions = rest; + } + while let Some(rest) = dimensions.strip_suffix(&[1]) { + dimensions = rest; + } + + if dimensions.is_empty() { + // The input dimensions were just a series of 1's (e.g. [1, 1, ... , 1]) + &[1] + } else { + dimensions + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic] + fn only_works_with_1d() { + let mut proc_block = MostConfidentIndices::default(); + let input: Tensor = Tensor::zeroed(alloc::vec![1, 2, 3]); + + let _ = proc_block.transform(input); + } + + #[test] + fn tensors_equivalent_to_1d_are_okay_too() { + let mut proc_block = MostConfidentIndices::default(); + let input: Tensor = Tensor::zeroed(alloc::vec![1, 5, 1, 1, 1]); + + let _ = proc_block.transform(input); + } + + #[test] + #[should_panic] + fn count_must_be_less_than_input_size() { + let mut proc_block = MostConfidentIndices::new(42); + let input = Tensor::new_vector(alloc::vec![0, 0, 1, 2]); + + let _ = proc_block.transform(input); + } + + #[test] + fn get_top_3_values() { + let mut proc_block = MostConfidentIndices::new(3); + let input = + Tensor::new_vector(alloc::vec![0.0, 0.5, 10.0, 3.5, -200.0]); + let should_be = Tensor::new_vector(alloc::vec![2, 3, 1]); + + let got = proc_block.transform(input); + + assert_eq!(got, should_be); + } +} diff --git a/noise-filtering/Cargo.toml b/noise-filtering/Cargo.toml new file mode 100644 index 00000000000..4a3a78b9006 --- /dev/null +++ b/noise-filtering/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "noise-filtering" +version = "0.8.0" +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +hotg-rune-proc-blocks = "^0.8.0" +libm = "0.2.1" +paste = "1.0.5" diff --git a/noise-filtering/src/gain_control.rs b/noise-filtering/src/gain_control.rs new file mode 100644 index 00000000000..439ea5c2bfe --- /dev/null +++ b/noise-filtering/src/gain_control.rs @@ -0,0 +1,244 @@ +//! A gain control routine ported from the [TensorFlow function][tf]. +//! +//! [tf]: https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/experimental/microfrontend/lib/pcan_gain_control.c + +use alloc::vec::Vec; +use hotg_rune_proc_blocks::Tensor; + +const WIDE_DYNAMIC_FUNCTION_BITS: usize = 32; +const WIDE_DYNAMIC_FUNCTION_LUT_SIZE: usize = + 4 * WIDE_DYNAMIC_FUNCTION_BITS - 3; +const PCAN_SNR_BITS: i32 = 12; +const PCAN_OUTPUT_BITS: usize = 6; +const SMOOTHING_BITS: u16 = 10; +const CORRECTION_BITS: i32 = -1; + +#[derive(Debug, Clone, PartialEq)] +pub struct GainControl { + config: Config, + state: State, +} + +impl GainControl { + defered_builder_methods! { + config.strength: f32; + config.offset: f32; + config.gain_bits: i32; + } + + pub fn transform( + &mut self, + input: Tensor, + noise_estimate: &[u32], + ) -> Tensor { + self.state.transform(input, noise_estimate) + } +} + +impl Default for GainControl { + fn default() -> Self { + let config = Config::default(); + let state = State::new(config, SMOOTHING_BITS, CORRECTION_BITS); + + GainControl { config, state } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +struct Config { + strength: f32, + offset: f32, + gain_bits: i32, +} + +impl Config { + builder_methods!(strength: f32, offset: f32, gain_bits: i32); +} + +impl Default for Config { + fn default() -> Self { + Config { + strength: 0.95, + offset: 80.0, + gain_bits: 21, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +struct State { + gain_lut: Vec, + snr_shift: i32, +} + +impl State { + fn new(config: Config, smoothing_bits: u16, correction_bits: i32) -> Self { + let mut gain_lut = vec![0; WIDE_DYNAMIC_FUNCTION_LUT_SIZE]; + let snr_shift = config.gain_bits - correction_bits - PCAN_SNR_BITS; + let input_bits = smoothing_bits as i32 - correction_bits; + + gain_lut[0] = gain_lookup(config, input_bits, 0); + gain_lut[1] = gain_lookup(config, input_bits, 1); + + for interval in 2..=WIDE_DYNAMIC_FUNCTION_BITS { + let x_0: u32 = 1_u32 << (interval - 1); + let x_1 = x_0 + (x_0 >> 1); + let x_2 = if interval == WIDE_DYNAMIC_FUNCTION_BITS { + x_0 + (x_0 - 1) + } else { + 2 * x_0 + }; + + let y_0 = gain_lookup(config, input_bits, x_0); + let y_1 = gain_lookup(config, input_bits, x_1); + let y_2 = gain_lookup(config, input_bits, x_2); + + let diff_1 = y_1 - y_0; + let diff_2 = y_2 - y_0; + let a_1 = 4 * diff_1 - diff_2; + let a_2 = diff_2 - a_1; + + gain_lut[4 * interval - 6] = y_0; + gain_lut[4 * interval - 6 + 1] = a_1; + gain_lut[4 * interval - 6 + 2] = a_2; + } + + State { + gain_lut, + snr_shift, + } + } + + fn transform( + &mut self, + mut input: Tensor, + noise_estimate: &[u32], + ) -> Tensor { + let elements = input.make_elements_mut(); + + for (i, element) in elements.iter_mut().enumerate() { + let gain = + wide_dynamic_function(noise_estimate[i], &self.gain_lut) as u32; + let signal = *element; + let snr = (signal as u64 * gain as u64) >> self.snr_shift; + *element = shrink(snr as u32); + } + + input + } +} + +fn shrink(snr: u32) -> u32 { + if snr < (2_u32 << PCAN_SNR_BITS) { + snr.wrapping_mul(snr) + >> (2 + 2 * PCAN_SNR_BITS - PCAN_OUTPUT_BITS as i32) + } else { + (snr >> (PCAN_SNR_BITS - PCAN_OUTPUT_BITS as i32)) + .wrapping_sub(1 << PCAN_OUTPUT_BITS as i32) + } +} + +fn most_significant_bit(number: u32) -> usize { + 32 - number.leading_zeros() as usize +} + +fn wide_dynamic_function(x: u32, lookup_table: &[i16]) -> i16 { + if x <= 2 { + return lookup_table[x as usize]; + } + + let interval = most_significant_bit(x) as i16; + + let index_offset = 4 * interval as usize - 6; + + let frac = if interval < 11 { + x << (11 - interval) + } else { + x >> (interval - 11) + }; + let frac = (frac & 0x3ff) as i16; + + let mut result = (lookup_table[index_offset + 2] as i32 * frac as i32) >> 5; + result += ((lookup_table[index_offset + 1] as u32) << 5) as i32; + result *= frac as i32; + result = (result + (1_i32 << 14)) >> 15; + result += lookup_table[index_offset] as i32; + + result as i16 +} + +fn gain_lookup(config: Config, input_bits: i32, x: u32) -> i16 { + let x = (x as f32) / (1 << input_bits) as f32; + let gain = (1 << config.gain_bits) as f32 + * libm::powf(x + config.offset, -config.strength); + + let gain = f32::min(gain, i16::max_value() as f32); + + (gain + 0.5) as i16 +} + +#[cfg(test)] +mod tests { + use super::*; + + /// https://github.com/tensorflow/tensorflow/blob/0f6d728b920e9b0286171bdfec9917d8486ac08b/tensorflow/lite/experimental/microfrontend/lib/pcan_gain_control_test.cc#L43-L63 + #[test] + fn test_pcan_gain_control() { + let mut gain_control = GainControl::default(); + gain_control.set_strength(0.95).set_offset(80.0); + let input = Tensor::new_vector(vec![241137, 478104]); + // Note: we get this from a the noise reduction step + let noise_estimate = vec![6321887, 31248341]; + + let got = gain_control.transform(input, &noise_estimate); + + let should_be = Tensor::new_vector(vec![3578, 1533]); + assert_eq!(got, should_be); + } + + #[test] + fn initialize_state() { + let config = Config { + strength: 0.95, + offset: 80.0, + gain_bits: 21, + }; + + let got = State::new(config, SMOOTHING_BITS, CORRECTION_BITS); + + let should_be = State { + snr_shift: 10, + gain_lut: vec![ + 32636, 32636, 32635, 0, 0, 0, 32635, 1, -2, 0, 32634, 1, -2, 0, + 32633, -5, 2, 0, 32630, -6, 0, 0, 32624, -12, 0, 0, 32612, -23, + -2, 0, 32587, -48, 0, 0, 32539, -96, 0, 0, 32443, -190, 0, 0, + 32253, -378, 4, 0, 31879, -739, 18, 0, 31158, -1409, 62, 0, + 29811, -2567, 202, 0, 27446, -4301, 562, 0, 23707, -6265, 1230, + 0, 18672, -7458, 1952, 0, 13166, -7030, 2212, 0, 8348, -5342, + 1868, 0, 4874, -3459, 1282, 0, 2697, -2025, 774, 0, 1446, + -1120, 436, 0, 762, -596, 232, 0, 398, -313, 122, 0, 207, -164, + 64, 0, 107, -85, 34, 0, 56, -45, 18, 0, 29, -22, 8, 0, 15, -13, + 6, 0, 8, -8, 4, 0, 4, -2, 0, + ], + }; + + assert_eq!(got, should_be); + } + + #[test] + fn known_wide_dynamic_function_results() { + let config = Config { + strength: 0.95, + offset: 80.0, + gain_bits: 21, + }; + let state = State::new(config, SMOOTHING_BITS, CORRECTION_BITS); + + let inputs = vec![(6321887, 990), (31248341, 219)]; + + for (input, should_be) in inputs { + let got = wide_dynamic_function(input, &state.gain_lut); + assert_eq!(got, should_be); + } + } +} diff --git a/noise-filtering/src/lib.rs b/noise-filtering/src/lib.rs new file mode 100644 index 00000000000..9e80bfa7f6c --- /dev/null +++ b/noise-filtering/src/lib.rs @@ -0,0 +1,64 @@ +#![no_std] + +#[macro_use] +extern crate alloc; + +#[macro_use] +mod macros; +mod gain_control; +mod noise_reduction; + +pub use noise_reduction::NoiseReduction; +pub use gain_control::GainControl; + +use hotg_rune_proc_blocks::{ProcBlock, Transform, Tensor}; + +#[derive(Debug, Default, Clone, PartialEq, ProcBlock)] +pub struct NoiseFiltering { + #[proc_block(skip)] + gain_control: GainControl, + #[proc_block(skip)] + noise_reduction: NoiseReduction, +} + +impl NoiseFiltering { + defered_builder_methods! { + gain_control.strength: f32; + gain_control.offset: f32; + gain_control.gain_bits: i32; + noise_reduction.smoothing_bits: u32; + noise_reduction.even_smoothing: f32; + noise_reduction.odd_smoothing: f32; + noise_reduction.min_signal_remaining: f32; + } +} + +impl Transform> for NoiseFiltering { + type Output = Tensor; + + fn transform(&mut self, input: Tensor) -> Tensor { + let cleaned = self.noise_reduction.transform(input); + + let amplified = self + .gain_control + .transform(cleaned, &self.noise_reduction.noise_estimate()) + .map(|_, energy| libm::log2((*energy as f64) + 1.0)); + + let min_value = amplified + .elements() + .to_vec() + .iter() + .fold(f64::INFINITY, |a, &b| a.min(b)); + + let max_value = amplified + .elements() + .to_vec() + .iter() + .fold(f64::NEG_INFINITY, |a, &b| a.max(b)); + + amplified.map(|_, energy| { + ((255.0 * (energy - min_value) / (max_value - min_value)) - 128.0) + as i8 + }) + } +} diff --git a/noise-filtering/src/macros.rs b/noise-filtering/src/macros.rs new file mode 100644 index 00000000000..330cbe06ca3 --- /dev/null +++ b/noise-filtering/src/macros.rs @@ -0,0 +1,41 @@ +macro_rules! builder_methods { + ($( $property:ident : $type:ty ),* $(,)?) => { + $( + paste::paste! { + pub fn [< set_ $property >](&mut self, $property: $type) -> &mut Self { + self.$property = $property; + self + } + } + )* + + $( + paste::paste! { + pub const fn $property(&self) -> $type { + self.$property + } + } + )* + }; +} + +macro_rules! defered_builder_methods { + ($( $component:ident . $property:ident : $type:ty; )*) => { + $( + paste::paste! { + pub fn [< set_ $property >](&mut self, $property: $type) -> &mut Self { + self.$component.[< set_ $property >]($property); + self + } + } + )* + + $( + paste::paste! { + pub fn $property(&self) -> $type { + self.$component.$property() + } + } + )* + }; +} diff --git a/noise-filtering/src/noise_reduction.rs b/noise-filtering/src/noise_reduction.rs new file mode 100644 index 00000000000..a032e94cbeb --- /dev/null +++ b/noise-filtering/src/noise_reduction.rs @@ -0,0 +1,161 @@ +//! A noise reduction routine inspired by the [TensorFlow function][tf]. +//! +//! [tf]: https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/experimental/microfrontend/lib/noise_reduction.c + +extern crate alloc; + +use alloc::vec::Vec; +use hotg_rune_proc_blocks::Tensor; + +const NOISE_REDUCTION_BITS: usize = 14; + +#[derive(Debug, Clone, PartialEq)] +pub struct NoiseReduction { + smoothing_bits: u32, + even_smoothing: u16, + odd_smoothing: u16, + min_signal_remaining: u16, + estimate: Vec, +} + +macro_rules! scaled_builder_methods { + ($( $property:ident : $type:ty ),* $(,)?) => { + $( + paste::paste! { + pub fn [< with_ $property >](mut self, $property: $type) -> Self { + self.[< set_ $property >]($property); + self + } + } + )* + + $( + paste::paste! { + pub fn [< set_ $property >](&mut self, $property: $type) { + self.$property = scale($property); + } + } + )* + + $( + paste::paste! { + pub fn $property(&self) -> $type { + unscale(self.$property) + } + } + )* + }; +} + +impl NoiseReduction { + builder_methods!(smoothing_bits: u32); + + scaled_builder_methods!( + even_smoothing: f32, + odd_smoothing: f32, + min_signal_remaining: f32, + ); + + pub fn noise_estimate(&self) -> &[u32] { &self.estimate } + + pub fn transform(&mut self, mut input: Tensor) -> Tensor { + // make sure we have the right estimate buffer size and panic if we + // don't. This works because the input and output have the same + // dimensions. + match input.dimensions() { + [1, len, ] => self.estimate.resize(*len, 0), + other => panic!( + "This transform only supports outputs of the form [1, _], not {:?}", + other + ), + } + + let signal = input.make_elements_mut(); + + for (i, value) in signal.iter_mut().enumerate() { + let smoothing = if i % 2 == 0 { + self.even_smoothing as u64 + } else { + self.odd_smoothing as u64 + }; + + let one_minus_smoothing = 1 << NOISE_REDUCTION_BITS; + + // update the estimate of the noise + let signal_scaled_up = (*value << self.smoothing_bits) as u64; + let mut estimate = ((signal_scaled_up * smoothing) + + (self.estimate[i] as u64 * one_minus_smoothing)) + >> NOISE_REDUCTION_BITS; + self.estimate[i] = estimate as u32; + + // Make sure that we can't get a negative value for the signal + // estimate + estimate = core::cmp::min(estimate, signal_scaled_up); + + let floor = (*value as u64 * self.min_signal_remaining as u64) + >> NOISE_REDUCTION_BITS; + let subtracted = + (signal_scaled_up - estimate) >> self.smoothing_bits; + + *value = core::cmp::max(floor, subtracted) as u32; + } + + input + } +} + +impl Default for NoiseReduction { + fn default() -> Self { + NoiseReduction { + smoothing_bits: 10, + even_smoothing: scale(0.025), + odd_smoothing: scale(0.06), + min_signal_remaining: scale(0.05), + estimate: vec![], + } + } +} + +fn scale(number: f32) -> u16 { + let scale_factor: f32 = (1 << NOISE_REDUCTION_BITS) as f32; + (number * scale_factor) as u16 +} + +fn unscale(number: u16) -> f32 { + let scale_factor: f32 = (1 << NOISE_REDUCTION_BITS) as f32; + number as f32 / scale_factor +} + +#[cfg(test)] +mod tests { + use alloc::sync::Arc; + + use super::*; + + /// https://github.com/tensorflow/tensorflow/blob/5dcfc51118817f27fad5246812d83e5dccdc5f72/tensorflow/lite/experimental/microfrontend/lib/noise_reduction_test.cc#L41-L59 + #[test] + fn test_noise_reduction_estimate() { + let mut state = NoiseReduction::default(); + let input = + Tensor::new_row_major(Arc::new([247311, 508620]), vec![1, 2]); + let should_be = vec![6321887, 31248341]; + + let _ = state.transform(input); + + assert_eq!(state.estimate, should_be); + } + + /// https://github.com/tensorflow/tensorflow/blob/5dcfc51118817f27fad5246812d83e5dccdc5f72/tensorflow/lite/experimental/microfrontend/lib/noise_reduction_test.cc#L61-L79 + #[test] + fn test_noise_reduction() { + let mut state = NoiseReduction::default(); + let input = + Tensor::new_row_major(Arc::new([247311, 508620]), vec![1, 2]); + let should_be = + Tensor::new_row_major(Arc::new([241137, 478104]), vec![1, 2]); + + let got = state.transform(input); + + assert_eq!(got, should_be); + } +} diff --git a/normalize/.gitignore b/normalize/.gitignore new file mode 100644 index 00000000000..2f7896d1d13 --- /dev/null +++ b/normalize/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/normalize/Cargo.toml b/normalize/Cargo.toml new file mode 100644 index 00000000000..da0b2d0e513 --- /dev/null +++ b/normalize/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "normalize" +version = "0.8.0" +authors = ["The Rune Developers "] +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +hotg-rune-proc-blocks = "^0.8.0" diff --git a/normalize/src/lib.rs b/normalize/src/lib.rs new file mode 100644 index 00000000000..e775d15e92f --- /dev/null +++ b/normalize/src/lib.rs @@ -0,0 +1,110 @@ +#![no_std] + +#[cfg(test)] +extern crate std; + +use core::{ + fmt::Debug, + ops::{Div, Sub}, +}; +use hotg_rune_proc_blocks::{Transform, Tensor}; + +pub fn normalize(input: &mut [T]) +where + T: PartialOrd + Div + Sub + Copy, +{ + if let Some((min, max)) = min_max(input.iter()) { + if min != max { + let range = max - min; + + for item in input { + *item = (*item - min) / range; + } + } + } +} + +/// Normalize the input to the range `[0, 1]`. +#[derive( + Debug, Default, Clone, Copy, PartialEq, hotg_rune_proc_blocks::ProcBlock, +)] +#[non_exhaustive] +#[transform(inputs = [f32; 1], outputs = [f32; 1])] +#[transform(inputs = [f32; 2], outputs = [f32; 2])] +#[transform(inputs = [f32; 3], outputs = [f32; 3])] +pub struct Normalize { + unused: &'static str, +} + +impl Transform> for Normalize +where + T: PartialOrd + Div + Sub + Copy, +{ + type Output = Tensor; + + fn transform(&mut self, mut input: Tensor) -> Tensor { + normalize(input.make_elements_mut()); + input + } +} + +impl Transform<[T; N]> for Normalize +where + T: PartialOrd + Div + Sub + Copy, +{ + type Output = [T; N]; + + fn transform(&mut self, mut input: [T; N]) -> [T; N] { + normalize(&mut input); + input + } +} + +fn min_max<'a, I, T>(items: I) -> Option<(T, T)> +where + I: IntoIterator + 'a, + T: PartialOrd + Copy + 'a, +{ + items.into_iter().fold(None, |bounds, &item| match bounds { + Some((min, max)) => { + let min = if item < min { item } else { min }; + let max = if max < item { item } else { max }; + Some((min, max)) + }, + None => Some((item, item)), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let input = Tensor::from([0.0, 1.0, 2.0]); + let mut pb = Normalize::default(); + + let output = pb.transform(input); + + assert_eq!(output, [0.0, 0.5, 1.0]); + } + + #[test] + fn it_accepts_vectors() { + let input = [0.0, 1.0, 2.0]; + let mut pb = Normalize::default(); + + let _ = pb.transform(input); + } + + #[test] + fn handle_empty() { + let input: [f32; 384] = [0.0; 384]; + let mut pb = Normalize::default(); + + let output = pb.transform(input); + + assert_eq!(output, input); + assert_eq!(output.len(), 384); + } +}