From da50c0294696c3a327db4b2a0a089d7977df488e Mon Sep 17 00:00:00 2001 From: Justin Worthe Date: Tue, 26 Dec 2017 17:00:03 +0200 Subject: Refactored to use more extensive typing --- benches/transforms.rs | 10 ++- src/audio.rs | 8 +- src/correlation.rs | 158 ++++++++++++++++++++++++++++++++ src/emscripten_api.rs | 32 ++++--- src/gui.rs | 30 +++---- src/lib.rs | 6 +- src/model.rs | 32 +++---- src/pitch.rs | 89 ++++++++++++++++++ src/signal.rs | 48 ++++++++++ src/transforms.rs | 245 -------------------------------------------------- web/main.js | 4 +- 11 files changed, 361 insertions(+), 301 deletions(-) create mode 100644 src/correlation.rs create mode 100644 src/pitch.rs create mode 100644 src/signal.rs delete mode 100644 src/transforms.rs diff --git a/benches/transforms.rs b/benches/transforms.rs index 8771492..2ed88b5 100644 --- a/benches/transforms.rs +++ b/benches/transforms.rs @@ -2,6 +2,8 @@ extern crate bencher; extern crate rusty_microphone; +use rusty_microphone::signal::Signal; +use rusty_microphone::correlation::Correlation; use bencher::Bencher; @@ -24,11 +26,13 @@ fn sample_sinusoud(amplitude: f32, frequency: f32, phase: f32) -> Vec { } fn bench_correlation_on_sine_wave(b: &mut Bencher) { - let frequency = 440.0f32; //concert A - let samples = sample_sinusoud(1.0, frequency, 0.0); + let signal = Signal::new( + &sample_sinusoud(1.0, 440.0f32, 0.0), + SAMPLE_RATE + ); b.iter(|| { - rusty_microphone::transforms::find_fundamental_frequency(&samples, SAMPLE_RATE) + Correlation::from_signal(&signal); }) } benchmark_group!(transforms, bench_correlation_on_sine_wave); diff --git a/src/audio.rs b/src/audio.rs index 01273d7..ddd7925 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -2,6 +2,8 @@ use portaudio as pa; use std::sync::mpsc::*; +use signal::Signal; + pub const SAMPLE_RATE: f32 = 44100.0; // I want to use the frames constant in contexts where I need to cast // it to f32 (eg for generating a sine wave). Therefore its type must @@ -31,13 +33,13 @@ pub fn get_default_device(pa: &pa::PortAudio) -> Result { Ok(default_input_index) } -pub fn start_listening_default(pa: &pa::PortAudio, sender: Sender>) -> Result>, pa::Error> { +pub fn start_listening_default(pa: &pa::PortAudio, sender: Sender) -> Result>, pa::Error> { let default = get_default_device(pa)?; start_listening(pa, default, sender) } pub fn start_listening(pa: &pa::PortAudio, device_index: u32, - sender: Sender>) -> Result>, pa::Error> { + sender: Sender) -> Result>, pa::Error> { let device_info = try!(pa.device_info(pa::DeviceIndex(device_index))); let latency = device_info.default_low_input_latency; @@ -53,7 +55,7 @@ pub fn start_listening(pa: &pa::PortAudio, device_index: u32, // This callback A callback to pass to the non-blocking stream. let callback = move |pa::InputStreamCallbackArgs { buffer, .. }| { - match sender.send(Vec::from(buffer)) { + match sender.send(Signal::new(buffer, SAMPLE_RATE)) { Ok(_) => pa::Continue, Err(_) => pa::Complete } diff --git a/src/correlation.rs b/src/correlation.rs new file mode 100644 index 0000000..835cc19 --- /dev/null +++ b/src/correlation.rs @@ -0,0 +1,158 @@ +use signal::Signal; +use pitch::Pitch; + +#[derive(Debug, Default, Clone)] +pub struct Correlation { + pub value: Vec +} + +impl Correlation { + pub fn from_signal(signal: &Signal) -> Correlation { + let samples = &signal.samples; + Correlation { + value: (0..samples.len()).map(|offset| { + samples.iter().take(samples.len() - offset) + .zip(samples.iter().skip(offset)) + .map(|(sig_i, sig_j)| sig_i * sig_j) + .sum() + }).collect() + } + } + + pub fn find_fundamental_frequency(&self, signal: &Signal) -> Option { + if signal.is_silence() { + // silence + return None; + } + + let first_peak_end = match self.value.iter().position(|&c| c < 0.0) { + Some(p) => p, + None => { + // musical signals will drop below 0 at some point + return None + } + }; + + let peak = self.value.iter() + .enumerate() + .skip(first_peak_end) + .fold((first_peak_end, 0.0), |(xi, xmag), (yi, &ymag)| if ymag > xmag { (yi, ymag) } else { (xi, xmag) }); + + let (peak_index, _) = peak; + + let refined_peak_index = self.refine_fundamentals(peak_index as f32 - 0.5, peak_index as f32 + 0.5); + + if self.is_noise(refined_peak_index) { + None + } + else { + Some(Pitch::new(signal.sample_rate / refined_peak_index)) + } + } + + fn refine_fundamentals(&self, low_bound: f32, high_bound: f32) -> f32 { + let data_points = 2 * self.value.len() / high_bound.ceil() as usize; + let range = high_bound - low_bound; + let midpoint = (low_bound + high_bound) / 2.0; + + if (range * data_points as f32) < 1.0 { + midpoint + } + else { + let low_guess = self.score_guess(low_bound, data_points); + let high_guess = self.score_guess(high_bound, data_points); + + if high_guess > low_guess { + self.refine_fundamentals(midpoint, high_bound) + } + else { + self.refine_fundamentals(low_bound, midpoint) + } + } + } + + fn score_guess(&self, period: f32, data_points: usize) -> f32 { + (1..data_points).map(|i| { + let expected_sign = if i % 2 == 0 { 1.0 } else { -1.0 }; + let x = i as f32 * period / 2.0; + let weight = 0.5 * i as f32; + expected_sign * weight * self.interpolate(x) + }).sum() + } + + fn interpolate(&self, x: f32) -> f32 { + if x.floor() < 0.0 { + self.value[0] + } + else if x.ceil() >= self.value.len() as f32 { + self.value[self.value.len()-1] + } + else { + let x0 = x.floor(); + let y0 = self.value[x0 as usize]; + let x1 = x.ceil(); + let y1 = self.value[x1 as usize]; + + if x0 as usize == x1 as usize { + y0 + } + else { + (y0*(x1-x) + y1*(x-x0)) / (x1-x0) + } + } + } + + fn is_noise(&self, fundamental: f32) -> bool { + let value_at_point = self.interpolate(fundamental); + let score_data_points = 2 * self.value.len() / fundamental.ceil() as usize; + let score = self.score_guess(fundamental, score_data_points); + + value_at_point > 2.0*score + } + +} + + +#[cfg(test)] +mod tests { + use super::*; + use std::f32::consts::PI; + + const SAMPLE_RATE: f32 = 44100.0; + const FRAMES: u16 = 512; + + fn frequency_resolution() -> f32 { + SAMPLE_RATE / 2.0 / f32::from(FRAMES) + } + + fn sin_arg(f: f32, t: f32) -> f32 { + 2.0 as f32 * PI * f * t + } + + fn sample_sinusoid(amplitude: f32, frequency: f32) -> Signal { + let samples: Vec = (0..FRAMES) + .map(|x| { + let t = f32::from(x) / SAMPLE_RATE; + sin_arg(frequency, t).sin() * amplitude + }).collect(); + + Signal::new(&samples, SAMPLE_RATE) + } + + #[test] + fn correlation_on_sine_wave() { + let frequency = 440.0f32; //concert A + + let signal = sample_sinusoid(1.0, frequency); + let fundamental = Correlation::from_signal(&signal).find_fundamental_frequency(&signal).expect("Find fundamental returned None"); + assert!((fundamental.hz-frequency).abs() < frequency_resolution(), "expected={}, actual={}", frequency, fundamental); + } + + #[test] + fn interpolate_half_way() { + let corr = Correlation { + value: vec!(0.0, 1.0) + }; + assert_eq!(0.5, corr.interpolate(0.5)) + } +} diff --git a/src/emscripten_api.rs b/src/emscripten_api.rs index eeede98..e296818 100644 --- a/src/emscripten_api.rs +++ b/src/emscripten_api.rs @@ -1,4 +1,6 @@ -use transforms; +use model::Model; +use signal::Signal; +use pitch::Pitch; use std::os::raw::c_char; use std::ffi::CString; @@ -6,38 +8,42 @@ use std::slice; use std::f32; #[no_mangle] -pub extern "C" fn find_fundamental_frequency(signal: *const f32, signal_length: isize, sample_rate: f32) -> f32 { +pub extern "C" fn find_fundamental_frequency(signal_ptr: *const f32, signal_length: isize, sample_rate: f32) -> f32 { let signal_slice = unsafe { - &slice::from_raw_parts(signal, signal_length as usize) + &slice::from_raw_parts(signal_ptr, signal_length as usize) }; - - transforms::find_fundamental_frequency(&signal_slice, sample_rate).unwrap_or(f32::NAN) + let signal = Signal::new(signal_slice, sample_rate); + let model = Model::from_signal(signal); + + model.pitch.map_or(f32::NAN, |p| p.hz) } #[no_mangle] pub extern "C" fn hz_to_cents_error(hz: f32) -> f32 { - transforms::hz_to_cents_error(hz) + let pitch = Pitch::new(hz); + pitch.cents_error() } #[no_mangle] pub extern "C" fn hz_to_pitch(hz: f32) -> *mut c_char { - let pitch = transforms::hz_to_pitch(hz); - CString::new(pitch) + let pitch = Pitch::new(hz); + CString::new(format!("{}", pitch)) .unwrap() .into_raw() } #[no_mangle] -pub extern "C" fn correlation(signal: *mut f32, signal_length: isize) { +pub extern "C" fn correlation(signal_ptr: *mut f32, signal_length: isize, sample_rate: f32) { let signal_slice = unsafe { - &slice::from_raw_parts(signal, signal_length as usize) + &slice::from_raw_parts(signal_ptr, signal_length as usize) }; - let correlated_signal = transforms::correlation(&signal_slice); + let signal = Signal::new(signal_slice, sample_rate); + let model = Model::from_signal(signal); unsafe { - for (i, cor) in correlated_signal.iter().enumerate() { - *signal.offset(i as isize) = *cor; + for (i, cor) in model.correlation.value.iter().enumerate() { + *signal_ptr.offset(i as isize) = *cor; } } } diff --git a/src/gui.rs b/src/gui.rs index 10f788d..387ad86 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -12,6 +12,7 @@ use std::sync::mpsc::*; use model::Model; use audio::SAMPLE_RATE; +use signal::Signal; const FPS: u32 = 60; @@ -126,7 +127,7 @@ fn set_dropdown_items(dropdown: >k::ComboBoxText, microphones: Vec<(u32, Strin dropdown.set_active_id(Some(format!("{}", default_mic).as_ref())); } -fn connect_dropdown_choose_microphone(mic_sender: Sender>, state: Rc>) { +fn connect_dropdown_choose_microphone(mic_sender: Sender, state: Rc>) { let dropdown = state.borrow().ui.dropdown.clone(); start_listening_current_dropdown_value(&dropdown, mic_sender.clone(), &state); dropdown.connect_changed(move |dropdown: >k::ComboBoxText| { @@ -134,7 +135,7 @@ fn connect_dropdown_choose_microphone(mic_sender: Sender>, state: Rc>, state: &Rc>) { +fn start_listening_current_dropdown_value(dropdown: >k::ComboBoxText, mic_sender: Sender, state: &Rc>) { if let Some(ref mut stream) = state.borrow_mut().pa_stream { stream.stop().ok(); } @@ -149,13 +150,13 @@ fn start_listening_current_dropdown_value(dropdown: >k::ComboBoxText, mic_send state.borrow_mut().pa_stream = stream; } -fn start_processing_audio(mic_receiver: Receiver>, cross_thread_state: Arc>) { +fn start_processing_audio(mic_receiver: Receiver, cross_thread_state: Arc>) { thread::spawn(move || { - while let Ok(samples) = mic_receiver.recv() { + while let Ok(signal) = mic_receiver.recv() { //just in case we hit performance difficulties, clear out the channel - while mic_receiver.try_recv().ok() != None {} + while mic_receiver.try_recv().is_ok() {} - let new_model = Model::from_signal(&samples, SAMPLE_RATE); + let new_model = Model::from_signal(signal); match cross_thread_state.write() { Ok(mut model) => { @@ -173,8 +174,7 @@ fn setup_pitch_label_callbacks(state: Rc>, cross_threa gtk::timeout_add(1000/FPS, move || { let ui = &state.borrow().ui; if let Ok(cross_thread_state) = cross_thread_state.read() { - let pitch = &cross_thread_state.pitch; - ui.pitch_label.set_label(pitch.as_ref()); + ui.pitch_label.set_label(&cross_thread_state.pitch_display()); ui.pitch_error_indicator.queue_draw(); ui.oscilloscope_chart.queue_draw(); ui.correlation_chart.queue_draw(); @@ -193,7 +193,7 @@ fn setup_pitch_error_indicator_callbacks(state: &Rc>, let line_indicator_height = 20.0; let color_indicator_height = f64::from(canvas.get_allocated_height()) - line_indicator_height; - match cross_thread_state.read().map(|state| state.error) { + match cross_thread_state.read().map(|state| state.pitch.map(|p| p.cents_error())) { Ok(Some(error)) => { let error_line_x = midpoint + f64::from(error) * midpoint / 50.0; context.new_path(); @@ -227,7 +227,7 @@ fn setup_oscilloscope_drawing_area_callbacks(state: &Rc if let Ok(cross_thread_state) = cross_thread_state.read() { let correlation = &cross_thread_state.correlation; - let len = correlation.len() as f64; - let max = match correlation.first() { + let len = correlation.value.len() as f64; + let max = match correlation.value.first() { Some(&c) => f64::from(c), None => 1.0 }; context.new_path(); context.move_to(0.0, height); - for (i, &val) in correlation.iter().enumerate() { + for (i, &val) in correlation.value.iter().enumerate() { let x = i as f64 * width / len; let y = height/2.0 - (f64::from(val) * height / max / 2.0); context.line_to(x, y); @@ -285,7 +285,7 @@ fn setup_correlation_drawing_area_callbacks(state: &Rc context.stroke(); //draw the fundamental - if let Some(fundamental) = cross_thread_state.fundamental_frequency { + if let Some(fundamental) = cross_thread_state.pitch.map(|p| p.hz) { context.new_path(); let fundamental_x = f64::from(SAMPLE_RATE) / f64::from(fundamental) * width / len; context.move_to(fundamental_x, 0.0); diff --git a/src/lib.rs b/src/lib.rs index 74aeaf1..c2b73d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,7 @@ -pub mod transforms; +pub mod model; +pub mod signal; +pub mod correlation; +pub mod pitch; #[cfg(not(target_os = "emscripten"))] extern crate gtk; @@ -15,4 +18,3 @@ pub mod audio; #[cfg(target_os = "emscripten")] pub mod emscripten_api; -pub mod model; diff --git a/src/model.rs b/src/model.rs index 7fad354..89255b9 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,12 +1,12 @@ -use transforms; +use signal::Signal; +use correlation::Correlation; +use pitch::Pitch; #[derive(Default)] pub struct Model { - pub fundamental_frequency: Option, - pub pitch: String, - pub error: Option, - pub signal: Vec, - pub correlation: Vec + pub pitch: Option, + pub signal: Signal, + pub correlation: Correlation } impl Model { @@ -14,22 +14,18 @@ impl Model { Model::default() } - pub fn from_signal(signal: &[f32], sample_rate: f32) -> Model { - let correlation = transforms::correlation(signal); - let fundamental = transforms::find_fundamental_frequency(signal, sample_rate); - let pitch = fundamental.map_or( - String::new(), - transforms::hz_to_pitch - ); - - let error = fundamental.map(transforms::hz_to_cents_error); + pub fn from_signal(signal: Signal) -> Model { + let correlation = Correlation::from_signal(&signal); + let pitch = correlation.find_fundamental_frequency(&signal); Model { - fundamental_frequency: fundamental, pitch: pitch, - error: error, - signal: transforms::align_to_rising_edge(signal), + signal: signal, correlation: correlation } } + + pub fn pitch_display(&self) -> String { + self.pitch.map_or(String::new(), |p| format!("{}", p)) + } } diff --git a/src/pitch.rs b/src/pitch.rs new file mode 100644 index 0000000..0944bbf --- /dev/null +++ b/src/pitch.rs @@ -0,0 +1,89 @@ +use std::fmt; +use std::f32; + +#[derive(Debug, Clone, Copy)] +pub struct Pitch { + pub hz: f32 +} + +impl Pitch { + pub fn new(hz: f32) -> Pitch { + Pitch { + hz: hz + } + } + + fn midi_number(&self) -> f32 { + 69.0 + 12.0 * (self.hz / 440.0).log2() + } + + pub fn cents_error(&self) -> f32 { + if !self.hz.is_finite() { + return f32::NAN; + } + + let midi_number = self.midi_number(); + let cents = (midi_number - midi_number.floor()) * 100.0; + if cents >= 50.0 { + cents - 100.0 + } + else { + cents + } + } +} + +impl fmt::Display for Pitch { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.hz <= 0.0 || !self.hz.is_finite() { + write!(f, "") + } else { + let pitch_names = [ + "C", + "C♯", + "D", + "E♭", + "E", + "F", + "F♯", + "G", + "G♯", + "A", + "B♭", + "B" + ]; + + //midi_number of 0 is C-1. + let rounded_pitch = self.midi_number().round() as i32; + let name = pitch_names[rounded_pitch as usize % pitch_names.len()]; + let octave = rounded_pitch / pitch_names.len() as i32 - 1; + + write!(f, "{: <2}{}", name, octave) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a4_is_correct() { + assert_eq!(format!("{}", Pitch::new(440.0)), "A 4"); + } + + #[test] + fn a2_is_correct() { + assert_eq!(format!("{}", Pitch::new(110.0)), "A 2"); + } + + #[test] + fn c4_is_correct() { + assert_eq!(format!("{}", Pitch::new(261.63)), "C 4"); + } + + #[test] + fn f5_is_correct() { + assert_eq!(format!("{}", Pitch::new(698.46)), "F 5"); + } +} diff --git a/src/signal.rs b/src/signal.rs new file mode 100644 index 0000000..ce1dd78 --- /dev/null +++ b/src/signal.rs @@ -0,0 +1,48 @@ +#[derive(Debug, Clone)] +pub struct Signal { + pub samples: Vec, + pub sample_rate: f32 +} + +impl Signal { + pub fn empty() -> Signal { + Signal::default() + } + + pub fn new(samples: &[f32], sample_rate: f32) -> Signal { + Signal { + samples: Signal::remove_mean_offset(samples), + sample_rate: sample_rate + } + } + + fn remove_mean_offset(samples: &[f32]) -> Vec { + let mean = samples.iter().sum::()/samples.len() as f32; + samples.iter().map(|x| x - mean).collect() + } + + pub fn aligned_to_rising_edge(&self) -> &[f32] { + let rising_edge = self.samples + .iter() + .enumerate() + .skip_while(|&(_,x)| !x.is_sign_negative()) + .skip_while(|&(_,x)| x.is_sign_negative()) + .map(|(i,_)| i) + .next().unwrap_or(0); + &self.samples[rising_edge..] + } + + pub fn is_silence(&self) -> bool { + self.samples.iter().all(|&x| x.abs() < 0.05) + } + +} + +impl Default for Signal { + fn default() -> Signal { + Signal { + samples: Vec::new(), + sample_rate: 44100.0 + } + } +} diff --git a/src/transforms.rs b/src/transforms.rs deleted file mode 100644 index a5df637..0000000 --- a/src/transforms.rs +++ /dev/null @@ -1,245 +0,0 @@ -use std::f32; - -fn remove_mean_offset(signal: &[f32]) -> Vec { - let mean = signal.iter().sum::()/signal.len() as f32; - signal.iter().map(|x| x - mean).collect() -} - -pub fn correlation(signal: &[f32]) -> Vec { - (0..signal.len()).map(|offset| { - signal.iter().take(signal.len() - offset) - .zip(signal.iter().skip(offset)) - .map(|(sig_i, sig_j)| sig_i * sig_j) - .sum() - }).collect() -} - -pub fn find_fundamental_frequency(signal: &[f32], sample_rate: f32) -> Option { - let normalized_signal = remove_mean_offset(signal); - - if normalized_signal.iter().all(|&x| x.abs() < 0.05) { - // silence - return None; - } - - let correlation = correlation(&normalized_signal); - - let first_peak_end = match correlation.iter().position(|&c| c < 0.0) { - Some(p) => p, - None => { - // musical signals will drop below 0 at some point - return None - } - }; - - let peak = correlation.iter() - .enumerate() - .skip(first_peak_end) - .fold((first_peak_end, 0.0), |(xi, xmag), (yi, &ymag)| if ymag > xmag { (yi, ymag) } else { (xi, xmag) }); - - let (peak_index, _) = peak; - - let refined_peak_index = refine_fundamentals(&correlation, peak_index as f32 - 0.5, peak_index as f32 + 0.5); - - if is_noise(&correlation, refined_peak_index) { - None - } - else { - Some(sample_rate / refined_peak_index) - } -} - -fn refine_fundamentals(correlation: &[f32], low_bound: f32, high_bound: f32) -> f32 { - let data_points = 2 * correlation.len() / high_bound.ceil() as usize; - let range = high_bound - low_bound; - let midpoint = (low_bound + high_bound) / 2.0; - - if (range * data_points as f32) < 1.0 { - midpoint - } - else { - let low_guess = score_guess(correlation, low_bound, data_points); - let high_guess = score_guess(correlation, high_bound, data_points); - - if high_guess > low_guess { - refine_fundamentals(correlation, midpoint, high_bound) - } - else { - refine_fundamentals(correlation, low_bound, midpoint) - } - } -} - - -fn is_noise(correlation: &[f32], fundamental: f32) -> bool { - let value_at_point = interpolate(correlation, fundamental); - let score_data_points = 2 * correlation.len() / fundamental.ceil() as usize; - let score = score_guess(correlation, fundamental, score_data_points); - - value_at_point > 2.0*score -} - -fn score_guess(correlation: &[f32], period: f32, data_points: usize) -> f32 { - (1..data_points).map(|i| { - let expected_sign = if i % 2 == 0 { 1.0 } else { -1.0 }; - let x = i as f32 * period / 2.0; - let weight = 0.5 * i as f32; - expected_sign * weight * interpolate(correlation, x) - }).sum() -} - -fn interpolate(correlation: &[f32], x: f32) -> f32 { - if x.floor() < 0.0 { - correlation[0] - } - else if x.ceil() >= correlation.len() as f32 { - correlation[correlation.len()-1] - } - else { - let x0 = x.floor(); - let y0 = correlation[x0 as usize]; - let x1 = x.ceil(); - let y1 = correlation[x1 as usize]; - - if x0 as usize == x1 as usize { - y0 - } - else { - (y0*(x1-x) + y1*(x-x0)) / (x1-x0) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::f32::consts::PI; - - const SAMPLE_RATE: f32 = 44100.0; - const FRAMES: u16 = 512; - - fn frequency_resolution() -> f32 { - SAMPLE_RATE / 2.0 / f32::from(FRAMES) - } - - fn sin_arg(f: f32, t: f32, phase: f32) -> f32 { - 2.0 as f32 * PI * f * t + phase - } - - fn sample_sinusoud(amplitude: f32, frequency: f32, phase: f32) -> Vec { - (0..FRAMES) - .map(|x| { - let t = f32::from(x) / SAMPLE_RATE; - sin_arg(frequency, t, phase).sin() * amplitude - }).collect() - } - - #[test] - fn correlation_on_sine_wave() { - let frequency = 440.0f32; //concert A - - let samples = sample_sinusoud(1.0, frequency, 0.0); - let fundamental = find_fundamental_frequency(&samples, SAMPLE_RATE).expect("Find fundamental returned None"); - assert!((fundamental-frequency).abs() < frequency_resolution(), "expected={}, actual={}", frequency, fundamental); - } - - #[test] - fn correlation_on_two_sine_waves() { - //Unfortunately, real signals won't be this neat - let samples1a = sample_sinusoud(2.0, 440.0, 0.0); - let samples2a = sample_sinusoud(1.0, 880.0, 0.0); - let expected_fundamental = 440.0; - - let samples: Vec = samples1a.iter().zip(samples2a.iter()) - .map(|(a, b)| a+b) - .collect(); - - let fundamental = find_fundamental_frequency(&samples, SAMPLE_RATE).expect("Find fundamental returned None"); - - assert!((fundamental-expected_fundamental).abs() < frequency_resolution(), "expected_fundamental={}, actual={}", expected_fundamental, fundamental); - } - - #[test] - fn interpolate_half_way() { - assert_eq!(0.5, interpolate(&vec!(0.0, 1.0), 0.5)) - } -} - -fn hz_to_midi_number(hz: f32) -> f32 { - 69.0 + 12.0 * (hz / 440.0).log2() -} - -pub fn hz_to_cents_error(hz: f32) -> f32 { - if !hz.is_finite() { - return f32::NAN; - } - - let midi_number = hz_to_midi_number(hz); - let cents = (midi_number - midi_number.floor()) * 100.0; - if cents >= 50.0 { - cents - 100.0 - } - else { - cents - } -} - -pub fn hz_to_pitch(hz: f32) -> String { - if hz <= 0.0 || !hz.is_finite() { - return "".to_string(); - } - - let pitch_names = [ - "C", - "C♯", - "D", - "E♭", - "E", - "F", - "F♯", - "G", - "G♯", - "A", - "B♭", - "B" - ]; - - let midi_number = hz_to_midi_number(hz); - //midi_number of 0 is C1. - - let rounded_pitch = midi_number.round() as i32; - let name = pitch_names[rounded_pitch as usize % pitch_names.len()]; - let octave = rounded_pitch / pitch_names.len() as i32 - 1; //0 is C1 - - format!("{: <2}{}", name, octave) -} - -#[test] -fn a4_is_correct() { - assert_eq!(hz_to_pitch(440.0), "A 4"); -} - -#[test] -fn a2_is_correct() { - assert_eq!(hz_to_pitch(110.0), "A 2"); -} - -#[test] -fn c4_is_correct() { - assert_eq!(hz_to_pitch(261.63), "C 4"); -} - -#[test] -fn f5_is_correct() { - assert_eq!(hz_to_pitch(698.46), "F 5"); -} - - -pub fn align_to_rising_edge(samples: &[f32]) -> Vec { - remove_mean_offset(samples) - .iter() - .skip_while(|x| !x.is_sign_negative()) - .skip_while(|x| x.is_sign_negative()) - .cloned() - .collect() -} diff --git a/web/main.js b/web/main.js index 97609e3..02e94fc 100644 --- a/web/main.js +++ b/web/main.js @@ -129,9 +129,9 @@ var hzToPitch = function(hz) { return wrapped(hz); }; -function correlation(data) { +function correlation(data, samplingRate) { return jsArrayToF32ArrayPtrMutateInPlace(data, function(dataPtr, dataLength) { - Module._correlation(dataPtr, dataLength); + Module._correlation(dataPtr, dataLength, samplingRate); }); } -- cgit v1.2.3