From afc6119fadbaf928b91c62a4f75d1798414c8048 Mon Sep 17 00:00:00 2001 From: Justin Worthe Date: Sat, 8 Jul 2017 13:09:25 +0200 Subject: Refactoring of code to be more functional If tests break on travis after this, it's because I reenabled some portaudio tests. I'm not sure if travis actually has sound available on their build servers. --- src/audio.rs | 37 +++++++-------- src/gui.rs | 32 ++++++------- src/transforms.rs | 131 ++++++++++++++++++++++++++---------------------------- 3 files changed, 94 insertions(+), 106 deletions(-) (limited to 'src') diff --git a/src/audio.rs b/src/audio.rs index c7cea54..0c85666 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -3,7 +3,7 @@ use portaudio as pa; use std::sync::mpsc::*; -pub const SAMPLE_RATE: f64 = 44100.0; +pub const SAMPLE_RATE: f32 = 44100.0; pub const FRAMES: usize = 1024; pub fn init() -> Result { @@ -29,24 +29,13 @@ pub fn get_default_device(pa: &pa::PortAudio) -> Result { Ok(default_input_index) } -#[test] -#[ignore] -fn get_device_list_returns_devices() { - let pa = init().expect("Could not init portaudio"); - let devices = get_device_list(&pa).expect("Getting devices had an error"); - - // all machines should have at least one input stream, even if - // that's just a virtual stream with a name like "default". - assert!(devices.len() > 0); -} - -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; @@ -55,15 +44,17 @@ pub fn start_listening(pa: &pa::PortAudio, device_index: u32, let input_params = pa::StreamParameters::::new(pa::DeviceIndex(device_index), 1, true, latency); // Check that the stream format is supported. - try!(pa.is_input_format_supported(input_params, SAMPLE_RATE)); + try!(pa.is_input_format_supported(input_params, SAMPLE_RATE as f64)); // Construct the settings with which we'll open our stream. - let stream_settings = pa::InputStreamSettings::new(input_params, SAMPLE_RATE, FRAMES as u32); + let stream_settings = pa::InputStreamSettings::new(input_params, SAMPLE_RATE as f64, FRAMES as u32); // This callback A callback to pass to the non-blocking stream. let callback = move |pa::InputStreamCallbackArgs { buffer, .. }| { - sender.send(buffer.iter().map(|&s| s as f64).collect()).ok(); - pa::Continue + match sender.send(Vec::from(buffer)) { + Ok(_) => pa::Continue, + Err(_) => pa::Complete //this happens when receiver is dropped + } }; let mut stream = try!(pa.open_non_blocking_stream(stream_settings, callback)); @@ -73,11 +64,15 @@ pub fn start_listening(pa: &pa::PortAudio, device_index: u32, } #[test] -#[ignore] fn start_listening_returns_successfully() { + // Just a note on unit tests here, portaudio doesn't seem to + // respond well to being initialized many times, and starts + // throwing errors. let pa = init().expect("Could not init portaudio"); + let devices = get_device_list(&pa).expect("Getting devices had an error"); - let device = devices.first().expect("Should have at least one device"); + assert!(devices.len() > 0); + let (sender, _) = channel(); - start_listening(&pa, device.0, sender).expect("Error starting listening to first channel"); + start_listening_default(&pa, sender).expect("Error starting listening to first channel"); } diff --git a/src/gui.rs b/src/gui.rs index 53e9a51..16921d4 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -29,11 +29,11 @@ struct ApplicationState { } struct CrossThreadState { - fundamental_frequency: Option, + fundamental_frequency: Option, pitch: String, - error: Option, - signal: Vec, - correlation: Vec + error: Option, + signal: Vec, + correlation: Vec } pub fn start_gui() -> Result<(), String> { @@ -137,7 +137,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.clone()); dropdown.connect_changed(move |dropdown: >k::ComboBoxText| { @@ -145,7 +145,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>) { match state.borrow_mut().pa_stream { Some(ref mut stream) => {stream.stop().ok();}, _ => {} @@ -161,7 +161,7 @@ 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 || { loop { let mut samples = None; @@ -226,7 +226,7 @@ fn setup_pitch_error_indicator_callbacks(state: Rc>, c let color_indicator_height = canvas.get_allocated_height() as f64 - line_indicator_height; if let Ok(Some(error)) = cross_thread_state.read().map(|state| state.error) { - let error_line_x = midpoint + error * midpoint / 50.0; + let error_line_x = midpoint + error as f64 * midpoint / 50.0; context.new_path(); context.move_to(error_line_x, 0.0); context.line_to(error_line_x, line_indicator_height); @@ -234,12 +234,12 @@ fn setup_pitch_error_indicator_callbacks(state: Rc>, c //flat on the left - context.set_source_rgb(0.0, 0.0, if error < 0.0 {-error/50.0} else {0.0}); + context.set_source_rgb(0.0, 0.0, if error < 0.0 {-error as f64/50.0} else {0.0}); context.rectangle(0.0, line_indicator_height, midpoint, color_indicator_height+line_indicator_height); context.fill(); //sharp on the right - context.set_source_rgb(if error > 0.0 {error/50.0} else {0.0}, 0.0, 0.0); + context.set_source_rgb(if error > 0.0 {error as f64/50.0} else {0.0}, 0.0, 0.0); context.rectangle(midpoint, line_indicator_height, width, color_indicator_height+line_indicator_height); context.fill(); } @@ -263,9 +263,9 @@ fn setup_oscilloscope_drawing_area_callbacks(state: Rc context.new_path(); context.move_to(0.0, mid_height); - for (i, intensity) in signal.iter().enumerate() { + for (i, &intensity) in signal.iter().enumerate() { let x = i as f64 * width / len; - let y = mid_height - (intensity * mid_height / max); + let y = mid_height - (intensity as f64 * mid_height / max); context.line_to(x, y); } @@ -293,15 +293,15 @@ fn setup_correlation_drawing_area_callbacks(state: Rc> let ref correlation = cross_thread_state.correlation; let len = correlation.len() as f64; let max = match correlation.first() { - Some(&c) => c, + Some(&c) => c as f64, None => 1.0 }; context.new_path(); context.move_to(0.0, height); - for (i, val) in correlation.iter().enumerate() { + for (i, &val) in correlation.iter().enumerate() { let x = i as f64 * width / len; - let y = height/2.0 - (val * height / max / 2.0); + let y = height/2.0 - (val as f64 * height / max / 2.0); context.line_to(x, y); } context.stroke(); @@ -309,7 +309,7 @@ fn setup_correlation_drawing_area_callbacks(state: Rc> //draw the fundamental if let Some(fundamental) = cross_thread_state.fundamental_frequency { context.new_path(); - let fundamental_x = ::audio::SAMPLE_RATE / fundamental * width / len; + let fundamental_x = ::audio::SAMPLE_RATE as f64 / fundamental as f64 * width / len; context.move_to(fundamental_x, 0.0); context.line_to(fundamental_x, height); context.stroke(); diff --git a/src/transforms.rs b/src/transforms.rs index 1eb8c10..c4a5b5c 100644 --- a/src/transforms.rs +++ b/src/transforms.rs @@ -1,49 +1,43 @@ -pub fn remove_mean_offset(input: &Vec) -> Vec { - let mean_input = input.iter().sum::()/input.len() as f64; - input.iter().map(|x|x-mean_input).collect() -} - -pub fn correlation(input: &Vec) -> Vec { - let mut correlation = Vec::with_capacity(input.len()); - for offset in 0..input.len() { - let mut c = 0.0; - for i in 0..input.len()-offset { - let j = i+offset; - c += input[i] * input[j]; - } - correlation.push(c); - } - correlation +pub fn remove_mean_offset(signal: &Vec) -> Vec { + let mean = signal.iter().sum::()/signal.len() as f32; + signal.iter().map(|x| x - mean).collect() +} + +pub fn correlation(signal: &Vec) -> 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_correlation(input: &Vec, sample_rate: f64) -> Option { - let intensities = remove_mean_offset(&input); +pub fn find_fundamental_frequency_correlation(signal: &Vec, sample_rate: f32) -> Option { + let normalized_signal = remove_mean_offset(&signal); - if intensities.iter().all(|&x| x.abs() < 0.1) { + if normalized_signal.iter().all(|&x| x.abs() < 0.1) { + // silence return None; } - let correlation = correlation(&intensities); + let correlation = correlation(&normalized_signal); - let mut first_peak_width = 0; - for offset in 0..correlation.len() { - if correlation[offset] < 0.0 { - first_peak_width = offset; - break; + 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 } - } - if first_peak_width == 0 { - return None; - } - + }; + let peak = correlation.iter() .enumerate() - .skip(first_peak_width) - .fold((first_peak_width, 0.0 as f64), |(xi, xmag), (yi, &ymag)| if ymag > xmag { (yi, ymag) } else { (xi, xmag) }); + .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 f64); + 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 @@ -53,27 +47,28 @@ pub fn find_fundamental_frequency_correlation(input: &Vec, sample_rate: f64 } } -fn refine_fundamentals(correlation: &Vec, initial_guess: f64) -> f64 { - let mut low_bound = initial_guess - 0.5; - let mut high_bound = initial_guess + 0.5; - - for _ in 0..5 { - let data_points = 2 * correlation.len() / high_bound.ceil() as usize; +fn refine_fundamentals(correlation: &Vec, 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); - - let midpoint = (low_bound + high_bound) / 2.0; + if high_guess > low_guess { - low_bound = midpoint; + refine_fundamentals(&correlation, midpoint, high_bound) } else { - high_bound = midpoint; + refine_fundamentals(&correlation, low_bound, midpoint) } } - (low_bound + high_bound) / 2.0 } -fn is_noise(correlation: &Vec, fundamental: f64) -> bool { +fn is_noise(correlation: &Vec, 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); @@ -81,22 +76,20 @@ fn is_noise(correlation: &Vec, fundamental: f64) -> bool { value_at_point > 2.0*score } -fn score_guess(correlation: &Vec, period: f64, data_points: usize) -> f64 { - let mut score = 0.0; - for i in 1..data_points { +fn score_guess(correlation: &Vec, period: f32, data_points: usize) -> f32 { + (1..data_points).map(|i| { let expected_sign = if i % 2 == 0 { 1.0 } else { -1.0 }; - score += expected_sign * 0.5 * i as f64 * interpolate(&correlation, i as f64 * period / 2.0); - } - score + 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: &Vec, x: f64) -> f64 { +fn interpolate(correlation: &Vec, x: f32) -> f32 { if x < 0.0 { - println!("<0"); correlation[0] } - else if x >= correlation.len() as f64 { - println!(">len"); + else if x >= correlation.len() as f32 { correlation[correlation.len()-1] } else { @@ -117,30 +110,30 @@ fn interpolate(correlation: &Vec, x: f64) -> f64 { #[cfg(test)] mod tests { use super::*; - use std::f64::consts::PI; + use std::f32::consts::PI; - const SAMPLE_RATE: f64 = 44100.0; + const SAMPLE_RATE: f32 = 44100.0; const FRAMES: usize = 512; - fn frequency_resolution() -> f64 { - SAMPLE_RATE / 2.0 / FRAMES as f64 + fn frequency_resolution() -> f32 { + SAMPLE_RATE / 2.0 / FRAMES as f32 } - fn sin_arg(f: f64, t: f64, phase: f64) -> f64 { - 2.0 as f64 * PI * f * t + phase + fn sin_arg(f: f32, t: f32, phase: f32) -> f32 { + 2.0 as f32 * PI * f * t + phase } - fn sample_sinusoud(amplitude: f64, frequency: f64, phase: f64) -> Vec { + fn sample_sinusoud(amplitude: f32, frequency: f32, phase: f32) -> Vec { (0..FRAMES) .map(|x| { - let t = x as f64 / SAMPLE_RATE; + let t = x as f32 / SAMPLE_RATE; sin_arg(frequency, t, phase).sin() * amplitude }).collect() } #[test] fn correlation_on_sine_wave() { - let frequency = 440.0 as f64; //concert A + let frequency = 440.0 as f32; //concert A let samples = sample_sinusoud(1.0, frequency, 0.0); let fundamental = find_fundamental_frequency_correlation(&samples, SAMPLE_RATE).expect("Find fundamental returned None"); @@ -169,11 +162,11 @@ mod tests { } } -pub fn hz_to_midi_number(hz: f64) -> f64 { +pub fn hz_to_midi_number(hz: f32) -> f32 { 69.0 + 12.0 * (hz / 440.0).log2() } -pub fn hz_to_cents_error(hz: f64) -> f64 { +pub fn hz_to_cents_error(hz: f32) -> f32 { let midi_number = hz_to_midi_number(hz); let cents = (midi_number * 100.0).round() % 100.0; if cents >= 50.0 { @@ -184,7 +177,7 @@ pub fn hz_to_cents_error(hz: f64) -> f64 { } } -pub fn hz_to_pitch(hz: f64) -> String { +pub fn hz_to_pitch(hz: f32) -> String { let pitch_names = [ "C ", "C#", @@ -204,7 +197,7 @@ pub fn hz_to_pitch(hz: f64) -> String { //midi_number of 0 is C-1. let rounded_pitch = midi_number.round() as i32; - let name = pitch_names[rounded_pitch as usize % pitch_names.len()].to_string(); + let name = pitch_names[rounded_pitch as usize % pitch_names.len()]; let octave = rounded_pitch / pitch_names.len() as i32 - 1; //0 is C-1 if octave < 0 { return "< C 1".to_string(); @@ -234,7 +227,7 @@ fn f5_is_correct() { } -pub fn align_to_rising_edge(samples: &Vec) -> Vec { +pub fn align_to_rising_edge(samples: &Vec) -> Vec { remove_mean_offset(&samples) .iter() .skip_while(|x| !x.is_sign_negative()) -- cgit v1.2.3