├── .gitignore ├── .gitmodules ├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── Cargo.toml ├── src ├── extractors │ ├── mod.rs │ ├── bark_loudness.rs │ ├── energy.rs │ ├── rms.rs │ ├── spectral_centroid.rs │ ├── power_spectrum.rs │ ├── spectral_flatness.rs │ ├── zcr.rs │ ├── spectral_kurtosis.rs │ ├── amp_spectrum.rs │ └── spectral_rolloff.rs ├── utils │ ├── mod.rs │ └── test.rs └── lib.rs ├── LICENSE.md ├── examples └── white-noise.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | target/ 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/utils/gauge"] 2 | path = src/utils/gauge 3 | url = https://github.com/meyda/gauge 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "meyda" 3 | version = "0.1.0" 4 | authors = ["jakubfiala "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | rustfft = "6.0.1" 9 | 10 | [dev-dependencies] 11 | approx = "0.5.1" 12 | serde = "1.0.7" 13 | serde_derive = "1.0.7" 14 | serde_json = "1.0" 15 | rand = "0.9.0" 16 | -------------------------------------------------------------------------------- /src/extractors/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod energy; 2 | /** 3 | * Extractors index 4 | */ 5 | pub mod rms; 6 | pub mod zcr; 7 | 8 | pub mod amp_spectrum; 9 | pub mod power_spectrum; 10 | 11 | pub mod spectral_centroid; 12 | pub mod spectral_flatness; 13 | pub mod spectral_kurtosis; 14 | pub mod spectral_rolloff; 15 | 16 | pub mod bark_loudness; 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | name: CI 4 | 5 | jobs: 6 | build_and_test: 7 | name: Rust project 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | submodules: recursive 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | toolchain: stable 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: build 19 | - uses: actions-rs/cargo@v1 20 | with: 21 | command: test 22 | -------------------------------------------------------------------------------- /src/extractors/bark_loudness.rs: -------------------------------------------------------------------------------- 1 | use crate::extractors::amp_spectrum; 2 | use crate::utils; 3 | 4 | pub fn compute(signal: &Vec, sample_rate: f64) -> Vec { 5 | let mut amp_spec: Vec = amp_spectrum::compute(signal); 6 | 7 | let original_len = amp_spec.len(); 8 | 9 | let bark_limits: Vec = 10 | utils::bark_limits(amp_spec.len(), sample_rate, amp_spec.len() as f64 / 2.0); 11 | 12 | let loudnesses = bark_limits[1..] 13 | .iter() 14 | .map(|&lim| { 15 | let current_limit = lim - (original_len - amp_spec.len()); 16 | 17 | let end = match current_limit { 18 | e if amp_spec.len() >= e => e, 19 | _ => amp_spec.len(), 20 | }; 21 | 22 | amp_spec.drain(0..end).sum::().powf(0.23) 23 | }) 24 | .collect(); 25 | 26 | loudnesses 27 | } 28 | -------------------------------------------------------------------------------- /src/extractors/energy.rs: -------------------------------------------------------------------------------- 1 | pub fn compute(signal: &Vec) -> f64 { 2 | let energy = signal 3 | .iter() 4 | .fold(0_f64, |acc, &sample| acc + sample.abs().powi(2)); 5 | 6 | return energy; 7 | } 8 | 9 | #[cfg(test)] 10 | mod tests { 11 | use super::compute; 12 | use crate::utils::test; 13 | use std::f64; 14 | 15 | const FLOAT_PRECISION: f64 = 0.000_000_010; 16 | 17 | fn test_against(dataset: &test::data::TestDataSet) -> () { 18 | let energy = compute(&dataset.signal); 19 | assert_relative_eq!( 20 | energy, 21 | dataset.features.energy, 22 | epsilon = f64::EPSILON, 23 | max_relative = FLOAT_PRECISION 24 | ); 25 | } 26 | 27 | #[test] 28 | fn test_energy() { 29 | let datasets = test::data::get_all(); 30 | 31 | for dataset in datasets.iter() { 32 | test_against(dataset); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/extractors/rms.rs: -------------------------------------------------------------------------------- 1 | pub fn compute(signal: &Vec) -> f64 { 2 | let sum = signal 3 | .iter() 4 | .fold(0_f64, |acc, &sample| acc + sample.powi(2)); 5 | let mean = sum / signal.len() as f64; 6 | 7 | mean.sqrt() 8 | } 9 | 10 | #[cfg(test)] 11 | mod tests { 12 | use super::compute; 13 | use crate::utils::test; 14 | use std::f64; 15 | 16 | const FLOAT_PRECISION: f64 = 0.000_000_010; 17 | 18 | fn test_against(dataset: &test::data::TestDataSet) -> () { 19 | let rms = compute(&dataset.signal); 20 | assert_relative_eq!( 21 | rms, 22 | dataset.features.rms, 23 | epsilon = f64::EPSILON, 24 | max_relative = FLOAT_PRECISION 25 | ); 26 | } 27 | 28 | #[test] 29 | fn test_rms() { 30 | let datasets = test::data::get_all(); 31 | 32 | for dataset in datasets.iter() { 33 | test_against(dataset); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/extractors/spectral_centroid.rs: -------------------------------------------------------------------------------- 1 | use crate::extractors::amp_spectrum; 2 | use crate::utils; 3 | 4 | pub fn compute(signal: &Vec) -> f64 { 5 | let amp_spec: Vec = amp_spectrum::compute(signal); 6 | 7 | utils::mu(1, &_spec) 8 | } 9 | 10 | #[cfg(test)] 11 | mod tests { 12 | use super::compute; 13 | use crate::utils::test; 14 | use std::f64; 15 | 16 | const FLOAT_PRECISION: f64 = 0.000_001_000; 17 | 18 | fn test_against(dataset: &test::data::TestDataSet) -> () { 19 | let sc = compute(&dataset.signal); 20 | 21 | assert_relative_eq!( 22 | sc, 23 | dataset.features.spectralCentroid, 24 | epsilon = f64::EPSILON, 25 | max_relative = FLOAT_PRECISION 26 | ); 27 | } 28 | 29 | #[test] 30 | fn test_spectral_centroid() { 31 | let datasets = test::data::get_all(); 32 | 33 | for dataset in datasets.iter() { 34 | test_against(dataset); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/extractors/power_spectrum.rs: -------------------------------------------------------------------------------- 1 | use crate::extractors::amp_spectrum; 2 | 3 | pub fn compute(signal: &Vec) -> Vec { 4 | let amp_spec: Vec = amp_spectrum::compute(signal); 5 | 6 | let pow_spec: Vec = amp_spec.iter().map(|bin| bin.powi(2)).into_iter().collect(); 7 | 8 | return pow_spec; 9 | } 10 | 11 | #[cfg(test)] 12 | mod tests { 13 | use super::compute; 14 | use crate::utils::test; 15 | use std::f64; 16 | 17 | const FLOAT_PRECISION: f64 = 0.333_333; 18 | 19 | fn test_against(dataset: &test::data::TestDataSet) -> () { 20 | let power_spec = compute(&dataset.signal); 21 | 22 | test::data::approx_compare_vec( 23 | &power_spec, 24 | &dataset.features.powerSpectrum, 25 | FLOAT_PRECISION, 26 | ); 27 | } 28 | 29 | #[test] 30 | fn test_power_spectrum() { 31 | let datasets = test::data::get_all(); 32 | 33 | for dataset in datasets.iter() { 34 | test_against(dataset); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Meyda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/extractors/spectral_flatness.rs: -------------------------------------------------------------------------------- 1 | use crate::extractors::amp_spectrum; 2 | 3 | pub fn compute(signal: &Vec) -> f64 { 4 | let amp_spec: Vec = amp_spectrum::compute(signal); 5 | 6 | let fraction = amp_spec 7 | .iter() 8 | .fold((0.0, 0.0), |acc, &x| (acc.0 + x.ln(), acc.1 + x)); 9 | 10 | (fraction.0 / amp_spec.len() as f64).exp() * amp_spec.len() as f64 / fraction.1 11 | } 12 | 13 | #[cfg(test)] 14 | mod tests { 15 | use super::compute; 16 | use crate::utils::test; 17 | use std::f64; 18 | 19 | const FLOAT_PRECISION: f64 = 0.001_000_000; 20 | 21 | fn test_against(dataset: &test::data::TestDataSet) -> () { 22 | let sf = compute(&dataset.signal); 23 | 24 | assert_relative_eq!( 25 | sf, 26 | dataset.features.spectralFlatness, 27 | epsilon = f64::EPSILON, 28 | max_relative = FLOAT_PRECISION 29 | ); 30 | } 31 | 32 | #[test] 33 | fn test_spectral_flatness() { 34 | let datasets = test::data::get_all(); 35 | 36 | for dataset in datasets.iter() { 37 | test_against(dataset); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/extractors/zcr.rs: -------------------------------------------------------------------------------- 1 | pub fn compute(signal: &Vec) -> f64 { 2 | // here stats is a tuple, first element being the previous sample and next element being the ZCR accumulator 3 | let zcr_tuple = signal.iter().fold((0_f64, 0_f64), |stats, &sample| { 4 | ( 5 | sample, 6 | stats.1 7 | + if stats.0.signum() != sample.signum() { 8 | 1_f64 9 | } else { 10 | 0_f64 11 | }, 12 | ) 13 | }); 14 | 15 | return zcr_tuple.1; 16 | } 17 | 18 | #[cfg(test)] 19 | mod tests { 20 | use super::compute; 21 | use crate::utils::test; 22 | use std::f64; 23 | 24 | const FLOAT_PRECISION: f64 = 0.000_000_010; 25 | 26 | fn test_against(dataset: &test::data::TestDataSet) -> () { 27 | let zcr = compute(&dataset.signal); 28 | assert_relative_eq!( 29 | zcr, 30 | dataset.features.zcr, 31 | epsilon = f64::EPSILON, 32 | max_relative = FLOAT_PRECISION 33 | ); 34 | } 35 | 36 | #[test] 37 | fn test_zcr() { 38 | let datasets = test::data::get_all(); 39 | 40 | for dataset in datasets.iter() { 41 | test_against(dataset); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/white-noise.rs: -------------------------------------------------------------------------------- 1 | extern crate meyda; 2 | extern crate rand; 3 | 4 | use rand::Rng; 5 | 6 | fn main() { 7 | const BUFFER_SIZE: usize = 1024; 8 | const SAMPLE_RATE: f64 = 44100.0; 9 | 10 | // create a vector of white noise 11 | let mut generator = rand::thread_rng(); 12 | let signal: Vec = vec![0; BUFFER_SIZE] 13 | .iter() 14 | .map(|&_sample| generator.gen_range(-1_f64..1_f64)) 15 | .collect(); 16 | 17 | // compute features 18 | let rms = meyda::get_rms(&signal); 19 | let energy = meyda::get_energy(&signal); 20 | let zcr = meyda::get_zcr(&signal); 21 | // let power_spectrum = meyda::get_power_spectrum(&signal); 22 | let spectral_centroid = meyda::get_spectral_centroid(&signal); 23 | let spectral_flatness = meyda::get_spectral_flatness(&signal); 24 | let spectral_kurtosis = meyda::get_spectral_kurtosis(&signal); 25 | let spectral_rolloff = meyda::get_spectral_rolloff(&signal, SAMPLE_RATE, Some(0.95)); 26 | let bark_loudness = meyda::get_bark_loudness(&signal, SAMPLE_RATE); 27 | 28 | println!("RMS is {} \n energy is {:?}, zcr is {:?},\n spectral centroid is {},\n spectral flatness is {},\n spectral kurtosis is {},\n spectral rolloff is {},\n Bark loudness is {:?}", rms, energy, zcr, spectral_centroid, spectral_flatness, spectral_kurtosis, 29 | spectral_rolloff, bark_loudness); 30 | } 31 | -------------------------------------------------------------------------------- /src/extractors/spectral_kurtosis.rs: -------------------------------------------------------------------------------- 1 | use crate::extractors::amp_spectrum; 2 | use crate::utils; 3 | 4 | pub fn compute(signal: &Vec) -> f64 { 5 | let amp_spec: Vec = amp_spectrum::compute(signal); 6 | 7 | let mus: Vec = (1..5) 8 | .map(|x| utils::mu(x as i32, &_spec)) 9 | .into_iter() 10 | .collect(); 11 | 12 | let numerator = 13 | -3.0 * mus[0].powf(4.0) + 6.0 * mus[0] * mus[1] - 4.0 * mus[0] * mus[2] + mus[3]; 14 | 15 | let denominator = (mus[1] - mus[0].powf(2.0)).sqrt().powf(4.0); 16 | 17 | numerator / denominator 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use super::compute; 23 | use crate::utils::test; 24 | use std::f64; 25 | 26 | const FLOAT_PRECISION: f64 = 0.000_010_000; 27 | 28 | fn test_against(dataset: &test::data::TestDataSet) -> () { 29 | let sk = compute(&dataset.signal); 30 | 31 | assert_relative_eq!( 32 | sk, 33 | dataset.features.spectralKurtosis, 34 | epsilon = f64::EPSILON, 35 | max_relative = FLOAT_PRECISION 36 | ); 37 | } 38 | 39 | #[test] 40 | fn test_spectral_kurtosis() { 41 | let datasets = test::data::get_all(); 42 | 43 | for dataset in datasets.iter() { 44 | test_against(dataset); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/extractors/amp_spectrum.rs: -------------------------------------------------------------------------------- 1 | use rustfft::{num_complex::Complex, FftPlanner}; 2 | 3 | pub fn compute(signal: &Vec) -> Vec { 4 | let fft_len = signal.len(); 5 | let fft = FftPlanner::new().plan_fft_forward(fft_len); 6 | 7 | let mut fft_buffer: Vec<_> = signal 8 | .iter() 9 | .map(|&sample| Complex::new(sample, 0_f64)) 10 | .collect(); 11 | 12 | fft.process(&mut fft_buffer); 13 | 14 | let amp_spectrum: Vec = fft_buffer 15 | .iter() 16 | .take(fft_buffer.len() / 2) 17 | .map(|bin| { 18 | let tmp = bin.re.powf(2_f64) + bin.im.powf(2_f64); 19 | tmp.sqrt() 20 | }) 21 | .collect(); 22 | 23 | return amp_spectrum; 24 | } 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | use super::compute; 29 | use crate::utils::test; 30 | use std::f64; 31 | 32 | const FLOAT_PRECISION: f64 = 0.333_333; 33 | 34 | fn test_against(dataset: &test::data::TestDataSet) -> () { 35 | let amp_spec = compute(&dataset.signal); 36 | 37 | test::data::approx_compare_vec( 38 | &_spec, 39 | &dataset.features.amplitudeSpectrum, 40 | FLOAT_PRECISION, 41 | ); 42 | } 43 | 44 | #[test] 45 | fn test_amplitude_spectrum() { 46 | let datasets = test::data::get_all(); 47 | 48 | for dataset in datasets.iter() { 49 | test_against(dataset); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/extractors/spectral_rolloff.rs: -------------------------------------------------------------------------------- 1 | use crate::extractors::amp_spectrum; 2 | 3 | pub fn compute(signal: &Vec, sample_rate: f64, rolloff_point: Option) -> f64 { 4 | let amp_spec: Vec = amp_spectrum::compute(signal); 5 | 6 | let nyq_bin = sample_rate / (2.0 * (amp_spec.len() as f64 - 1.0)); 7 | let mut integral: f64 = amp_spec.iter().sum(); 8 | 9 | let threshold = match rolloff_point { 10 | Some(rf) => rf * integral, 11 | None => 0.99 * integral, 12 | }; 13 | 14 | let mut reader = amp_spec.len() as i32 - 1; 15 | 16 | while integral > threshold && reader >= 0 { 17 | integral -= amp_spec[reader as usize]; 18 | reader -= 1; 19 | } 20 | 21 | (reader + 1) as f64 * nyq_bin 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use super::compute; 27 | use crate::utils::test; 28 | use std::f64; 29 | 30 | const FLOAT_PRECISION: f64 = 0.000_000_010; 31 | 32 | fn test_against(dataset: &test::data::TestDataSet) -> () { 33 | let sr = compute(&dataset.signal, dataset.info.sampleRate as f64, None); 34 | 35 | assert_relative_eq!( 36 | sr, 37 | dataset.features.spectralRolloff, 38 | epsilon = f64::EPSILON, 39 | max_relative = FLOAT_PRECISION 40 | ); 41 | } 42 | 43 | #[test] 44 | fn test_spectral_rolloff() { 45 | let datasets = test::data::get_all(); 46 | 47 | for dataset in datasets.iter() { 48 | test_against(dataset); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod test; 2 | pub type Hz = f64; 3 | 4 | pub fn mu(exp: i32, vector: &Vec) -> f64 { 5 | let fraction = vector.iter() 6 | .enumerate() 7 | .fold((0.0, 0.0), 8 | |acc, x| { 9 | let num_inc = (x.0 as f64).powi(exp) * x.1.abs(); 10 | let den_inc = x.1; 11 | 12 | (acc.0 + num_inc, acc.1 + den_inc) 13 | }); 14 | 15 | fraction.0 / fraction.1 16 | } 17 | 18 | fn bark_map(freq: Hz) -> Hz { 19 | 13.0 * (freq / 1315.8).atan() + 3.5 * (freq / 7518.0).powi(2).atan() 20 | } 21 | 22 | fn bark_scale(length: usize, sample_rate: Hz, buffer_size: f64) -> Vec { 23 | vec![0.0 as Hz; length] 24 | .iter() 25 | .enumerate() 26 | .map(|s| bark_map(s.0 as Hz * sample_rate / buffer_size)) 27 | .collect() 28 | } 29 | 30 | fn find_limit(ref scale: &Vec, bark_freq: Hz) -> usize { 31 | let found = scale.iter().position(|&freq| freq >= bark_freq); 32 | 33 | match found { 34 | Some(lim) => lim, 35 | None => scale.len() - 1, 36 | } 37 | } 38 | 39 | pub fn bark_limits(spec_size: usize, sample_rate: f64, buffer_size: f64) -> Vec { 40 | let num_bands = 24; 41 | 42 | let scale = bark_scale(spec_size, sample_rate as Hz, buffer_size); 43 | 44 | let band_width: Hz = match scale.last() { 45 | Some(&freq) => freq / num_bands as f64, 46 | None => 0.0, 47 | }; 48 | 49 | let limits = 0..num_bands; 50 | 51 | limits 52 | .map(|band| { 53 | match band { 54 | 0 => band, // first limit is 0 55 | _ => find_limit(&scale, band as Hz * band_width), 56 | } 57 | }) 58 | .collect() 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # meyda-rs 2 | 3 | [![Build Status](https://travis-ci.org/meyda/meyda-rs.svg?branch=master)](https://travis-ci.org/meyda/meyda-rs) 4 | [![Code coverage](https://codecov.io/github/meyda/meyda-rs/coverage.svg?branch=master)](https://codecov.io/gh/meyda/meyda-rs) 5 | 6 | _It's like [meyda](https://github.com/hughrawlinson/meyda), but for Rust._ 7 | 8 | This project is heavily WIP and it's not wise to use it in production yet. 9 | 10 | The plan is to initially provide a set of pure functions which operate on 64-bit float vectors, each vector being a frame of audio. 11 | 12 | Later on, `meyda-rs` should support file loading, configuration, overlapping frames, etc. to approach the API of Meyda. 13 | 14 | ## Usage 15 | 16 | This example creates a 1024-sample frame of white noise, and calculates its features. 17 | 18 | ```rust 19 | extern crate meyda; 20 | extern crate rand; 21 | 22 | use rand::Rng; 23 | 24 | fn main() { 25 | const BUFFER_SIZE: usize = 1024; 26 | const SAMPLE_RATE: f64 = 44100.0; 27 | 28 | // create a vector of white noise 29 | let mut generator = rand::thread_rng(); 30 | let signal: Vec = vec![0; BUFFER_SIZE] 31 | .iter() 32 | .map(|&_sample| generator.gen_range(-1_f64..1_f64)) 33 | .collect(); 34 | 35 | // compute features 36 | let rms = meyda::get_rms(&signal); 37 | let energy = meyda::get_energy(&signal); 38 | let zcr = meyda::get_zcr(&signal); 39 | // let power_spectrum = meyda::get_power_spectrum(&signal); 40 | let spectral_centroid = meyda::get_spectral_centroid(&signal); 41 | let spectral_flatness = meyda::get_spectral_flatness(&signal); 42 | let spectral_kurtosis = meyda::get_spectral_kurtosis(&signal); 43 | let spectral_rolloff = meyda::get_spectral_rolloff(&signal, SAMPLE_RATE, Some(0.95)); 44 | let bark_loudness = meyda::get_bark_loudness(&signal, SAMPLE_RATE); 45 | 46 | println!("RMS is {} \n energy is {:?}, zcr is {:?},\n spectral centroid is {},\n spectral flatness is {},\n spectral kurtosis is {},\n spectral rolloff is {},\n Bark loudness is {:?}", rms, energy, zcr, spectral_centroid, spectral_flatness, spectral_kurtosis, 47 | spectral_rolloff, bark_loudness); 48 | } 49 | 50 | ``` 51 | 52 | ## Development 53 | 54 | Contributions are always welcome. The library _should_ be test-driven and all new features _should_ have accompanying tests. 55 | 56 | Tests can be run with `cargo test` – each extractor function _should_ have a test module in the same file, and should make use of [meyda/gauge](https://github.com/meyda/gauge), which is submodule'd in `/src/utils`. The deserialized gauge data is provided by `utils::test` as a vector of `TestDataSet` structures. 57 | -------------------------------------------------------------------------------- /src/utils/test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | pub mod data { 3 | extern crate serde; 4 | extern crate serde_json; 5 | 6 | use std::f64; 7 | use std::fs::File; 8 | use std::io::prelude::*; 9 | use std::path::Path; 10 | 11 | #[allow(non_snake_case)] 12 | #[derive(Serialize, Deserialize)] 13 | pub struct TestDataInfo { 14 | pub description: String, 15 | pub sampleRate: i32, 16 | pub bufferSize: i32, 17 | } 18 | 19 | #[allow(non_snake_case)] 20 | #[derive(Serialize, Deserialize)] 21 | pub struct TestDataFeatures { 22 | pub energy: f64, 23 | pub loudness: Vec, 24 | pub mfcc: Vec, 25 | pub perceptualSharpness: f64, 26 | pub perceptualSpread: f64, 27 | pub powerSpectrum: Vec, 28 | pub rms: f64, 29 | pub spectralCentroid: f64, 30 | pub spectralFlatness: f64, 31 | pub spectralKurtosis: f64, 32 | pub spectralRolloff: f64, 33 | pub spectralSkewness: f64, 34 | pub spectralSlope: f64, 35 | pub spectralSpread: f64, 36 | pub amplitudeSpectrum: Vec, 37 | pub zcr: f64, 38 | } 39 | 40 | #[allow(non_snake_case)] 41 | #[derive(Serialize, Deserialize)] 42 | pub struct TestDataSet { 43 | pub info: TestDataInfo, 44 | pub signal: Vec, 45 | pub features: TestDataFeatures, 46 | } 47 | 48 | pub fn load_dataset(dataset_path: &str) -> TestDataSet { 49 | let path = Path::new(dataset_path); 50 | let display = path.display(); 51 | 52 | let mut file = match File::open(&path) { 53 | Err(why) => panic!("couldn't open {}: {}", display, why), 54 | Ok(file) => file, 55 | }; 56 | 57 | let mut json_str = String::new(); 58 | match file.read_to_string(&mut json_str) { 59 | Err(why) => panic!("couldn't read {}: {}", display, why), 60 | Ok(_) => (), 61 | } 62 | 63 | let data: TestDataSet = match serde_json::from_str(&json_str) { 64 | Ok(d) => d, 65 | Err(err) => panic!("{:?}", err), 66 | }; 67 | 68 | data 69 | } 70 | 71 | pub fn get_all() -> Vec { 72 | let paths = ["./src/utils/gauge/gauge01.json"]; 73 | 74 | paths.iter().map(|p| load_dataset(p)).collect() 75 | } 76 | 77 | pub fn approx_compare_vec(vec1: &Vec, vec2: &Vec, precision: f64) -> () { 78 | #[allow(unused_variables)] 79 | let zipped: Vec<_> = vec1 80 | .iter() 81 | .zip(vec2.iter()) 82 | .inspect(|x| { 83 | assert_relative_eq!(x.0, x.1, max_relative = precision, epsilon = f64::EPSILON) 84 | }) 85 | .collect(); 86 | 87 | () 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Meyda is a Rust library for audio feature extraction. It's based on [the 2 | //! original library in Javascript](https://meyda.js.org). It implements a 3 | //! variety of well known audio feature extraction algorithms. 4 | 5 | #[cfg(test)] 6 | #[macro_use] 7 | extern crate serde_derive; 8 | #[cfg(test)] 9 | #[macro_use] 10 | extern crate approx; 11 | 12 | mod extractors; 13 | mod utils; 14 | 15 | /// A type alias representing hertz 16 | pub type Hz = utils::Hz; 17 | 18 | /// 19 | /// Root Mean Square 20 | /// 21 | /// The root mean square of the waveform, that corresponds to its loudness. 22 | /// Corresponds to the `Energy` feature in YAAFE, adapted from Loy’s 23 | /// Musimathics. 24 | /// 25 | /// RMS is used for information about the loudness of the sound. 26 | /// 27 | /// G. Loy, Musimathics: The Mathematical Foundations of Music, Volume 1. The 28 | /// MIT Press, 2006. 29 | /// 30 | /// ### Params: 31 | /// 32 | /// - `signal` The signal vector (`Vec`) 33 | /// 34 | /// Returns the root mean square of the signal. 35 | pub fn get_rms(signal: &Vec) -> f64 { 36 | extractors::rms::compute(signal) 37 | } 38 | 39 | /// Energy 40 | /// 41 | /// The infinite integral of the squared signal. According to Lathi. 42 | /// 43 | /// B. P. Lathi, Modern Digital and Analog Communication Systems 3e Osece. 44 | /// Oxford University Press, 3rd ed., 1998. 45 | /// 46 | /// ### Params: 47 | /// 48 | /// - `signal` The signal vector (`Vec`) 49 | pub fn get_energy(signal: &Vec) -> f64 { 50 | extractors::energy::compute(signal) 51 | } 52 | 53 | /// Zero Crossing Rate 54 | /// 55 | /// The number of times that the signal crosses the zero value in the buffer. 56 | /// 57 | /// Used for differentiating between percussive and pitched sounds. Percussive 58 | /// sounds will have a random ZCR across buffers, whereas pitch sounds will 59 | /// return a more constant value. 60 | /// 61 | /// ### Params: 62 | /// 63 | /// - `signal` The signal vector (`Vec`) 64 | pub fn get_zcr(signal: &Vec) -> f64 { 65 | extractors::zcr::compute(signal) 66 | } 67 | 68 | /// Amplitude Spectrum 69 | /// 70 | /// This is also known as the magnitude spectrum. By calculating the Fast 71 | /// Fourier Transform (FFT), we get the signal represented in the frequency 72 | /// domain. The output is an array, where each index is a frequency bin (i.e. 73 | /// containing information about a range of frequencies) containing a complex 74 | /// value (real and imaginary). The amplitude spectrum takes this complex array 75 | /// and computes the amplitude of each index. The result is the distribution of 76 | /// frequencies in the signal along with their corresponding strength. If you 77 | /// want to learn more about Fourier Transform, and the differences between 78 | /// time-domain to frequency-domain analysis, [this 79 | /// article](https://www.mathworks.com/help/signal/examples/practical-introduction-to-frequency-domain-analysis.html) 80 | /// is a good place to start. 81 | /// 82 | /// ### Params: 83 | /// 84 | /// - `signal` The signal vector (`Vec`) 85 | pub fn get_amp_spectrum(signal: &Vec) -> Vec { 86 | extractors::amp_spectrum::compute(signal) 87 | } 88 | 89 | /// Power Spectrum 90 | /// 91 | /// This is the `amplitudeSpectrum` squared. 92 | /// 93 | /// ### Params: 94 | /// 95 | /// - `signal` The signal vector (`Vec`) 96 | pub fn get_power_spectrum(signal: &Vec) -> Vec { 97 | extractors::power_spectrum::compute(signal) 98 | } 99 | 100 | /// Spectral Centroid 101 | /// 102 | /// An indicator of the "brightness" of a given sound, representing the spectral 103 | /// centre of gravity. If you were to take the spectrum, make a wooden block out 104 | /// of it and try to balance it on your finger (across the X axis), the spectral 105 | /// centroid would be the frequency that your finger "touches" when it 106 | /// successfully balances. 107 | /// 108 | /// ### Params: 109 | /// 110 | /// - `signal` The signal vector (`Vec`) 111 | pub fn get_spectral_centroid(signal: &Vec) -> f64 { 112 | extractors::spectral_centroid::compute(signal) 113 | } 114 | 115 | /// Spectral Flatness 116 | /// 117 | /// The flatness of the spectrum. It is computed using the ratio between the 118 | /// geometric and arithmetic means. 119 | /// 120 | /// ### Params: 121 | /// 122 | /// - `signal` The signal vector (`Vec`) 123 | pub fn get_spectral_flatness(signal: &Vec) -> f64 { 124 | extractors::spectral_flatness::compute(signal) 125 | } 126 | 127 | /// Spectral Kurtosis 128 | /// 129 | /// A measure of how quickly the spectrum of a signal is changing. It is 130 | /// calculated by computing the difference between the current spectrum and that 131 | /// of the previous frame. 132 | /// 133 | /// ### Params: 134 | /// 135 | /// - `signal` The signal vector (`Vec`) 136 | pub fn get_spectral_kurtosis(signal: &Vec) -> f64 { 137 | extractors::spectral_kurtosis::compute(signal) 138 | } 139 | 140 | /// Spectral Rolloff 141 | /// 142 | /// The frequency below which is contained a given % of the energy of the 143 | /// spectrum. 144 | /// 145 | /// ### Params: 146 | /// 147 | /// - `signal` The signal vector (`Vec`) 148 | /// - `sample_rate` The sample rate of the signal (Hz) 149 | /// - `rolloff_point` the given percentage (default: 0.99) 150 | pub fn get_spectral_rolloff( 151 | signal: &Vec, 152 | sample_rate: f64, 153 | rolloff_point: Option, 154 | ) -> f64 { 155 | extractors::spectral_rolloff::compute(signal, sample_rate, rolloff_point) 156 | } 157 | 158 | /// Specific Bark Loudness 159 | /// 160 | /// Humans' perception of frequency is non-linear. The [Bark 161 | /// Scale](https://en.wikipedia.org/wiki/Bark_scale) was developed in order to 162 | /// have a scale on which equal distances correspond to equal distances of 163 | /// frequency perception. 164 | /// 165 | /// Returns the loudness of the input sound on each step (often referred to as 166 | /// bands) of this scale (`.specific`). There are 24 bands overall. 167 | /// 168 | /// Takes a signal vector `signal` (`Vec`) and the sample rate of the 169 | /// signal `sample_rate` (`f64`), and returns the Loudness in each Bark band 170 | /// `Vec`. 171 | /// 172 | /// ### Params: 173 | /// 174 | /// - `signal` The signal vector (`Vec`) 175 | /// - `sample_rate` The sample rate of the signal (Hz) 176 | pub fn get_bark_loudness(signal: &Vec, sample_rate: f64) -> Vec { 177 | extractors::bark_loudness::compute(signal, sample_rate) 178 | } 179 | --------------------------------------------------------------------------------