From d9c2952c36f272faba767081bf4c96cc4aa868e2 Mon Sep 17 00:00:00 2001 From: Justin Worthe Date: Sat, 25 Mar 2017 12:46:14 +0200 Subject: Added a graphical indication of the pith's sharpness or flatness I'm not 100% happy with the precision of the correlation function at this point. I'm hoping that with a bit more work, I can get it to be more precise, and then the indicator will be more meaningful. --- src/gui.rs | 41 ++++++++++++++++++++++++++++++++++++++--- src/transforms.rs | 22 ++++++++++++++++++---- 2 files changed, 56 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/gui.rs b/src/gui.rs index 8ec87d9..e1901c1 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -13,6 +13,7 @@ use std::sync::mpsc::*; struct RustyUi { dropdown: gtk::ComboBoxText, pitch_label: gtk::Label, + pitch_error_indicator: gtk::DrawingArea, freq_chart: gtk::DrawingArea, correlation_chart: gtk::DrawingArea } @@ -25,6 +26,7 @@ struct ApplicationState { struct CrossThreadState { pitch: String, + error: f64, freq_spectrum: Vec<::transforms::FrequencyBucket>, correlation: Vec } @@ -43,6 +45,7 @@ pub fn start_gui() -> Result<(), String> { let cross_thread_state = Arc::new(RwLock::new(CrossThreadState { pitch: String::new(), + error: 0.0, freq_spectrum: Vec::new(), correlation: Vec::new() })); @@ -53,6 +56,7 @@ pub fn start_gui() -> Result<(), String> { start_processing_audio(mic_receiver, cross_thread_state.clone()); setup_pitch_label_callbacks(state.clone(), cross_thread_state.clone()); + setup_pitch_error_indicator_callbacks(state.clone(), cross_thread_state.clone()); setup_freq_drawing_area_callbacks(state.clone(), cross_thread_state.clone()); setup_correlation_drawing_area_callbacks(state.clone(), cross_thread_state.clone()); @@ -78,6 +82,10 @@ fn create_window(microphones: Vec<(u32, String)>) -> RustyUi { let pitch_label = gtk::Label::new(None); layout_box.add(&pitch_label); + let pitch_error_indicator = gtk::DrawingArea::new(); + pitch_error_indicator.set_size_request(600, 50); + layout_box.add(&pitch_error_indicator); + let freq_chart = gtk::DrawingArea::new(); freq_chart.set_size_request(600, 400); layout_box.add(&freq_chart); @@ -91,6 +99,7 @@ fn create_window(microphones: Vec<(u32, String)>) -> RustyUi { RustyUi { dropdown: dropdown, pitch_label: pitch_label, + pitch_error_indicator: pitch_error_indicator, freq_chart: freq_chart, correlation_chart: correlation_chart } @@ -141,9 +150,9 @@ fn start_processing_audio(mic_receiver: Receiver>, cross_thread_state: let frequency_domain = ::transforms::fft(&samples, 44100.0); let correlation = ::transforms::correlation(&samples); let fundamental = ::transforms::find_fundamental_frequency_correlation(&samples, 44100.0); - let pitch = match fundamental { - Some(fundamental) => ::transforms::hz_to_pitch(fundamental), - None => "".to_string() + let (pitch, error) = match fundamental { + Some(fundamental) => (::transforms::hz_to_pitch(fundamental), ::transforms::hz_to_cents_error(fundamental)), + None => ("".to_string(), 0.0) }; match cross_thread_state.write() { @@ -151,6 +160,7 @@ fn start_processing_audio(mic_receiver: Receiver>, cross_thread_state: state.pitch = pitch; state.freq_spectrum = frequency_domain; state.correlation = correlation; + state.error = error }, Err(_) => {} }; @@ -163,6 +173,7 @@ fn setup_pitch_label_callbacks(state: Rc>, cross_threa let ref pitch = cross_thread_state.read().unwrap().pitch; let ref ui = state.borrow().ui; ui.pitch_label.set_label(pitch.as_ref()); + ui.pitch_error_indicator.queue_draw(); ui.correlation_chart.queue_draw(); ui.freq_chart.queue_draw(); @@ -170,6 +181,30 @@ fn setup_pitch_label_callbacks(state: Rc>, cross_threa }); } +fn setup_pitch_error_indicator_callbacks(state: Rc>, cross_thread_state: Arc>) { + let outer_state = state.clone(); + let ref canvas = outer_state.borrow().ui.pitch_error_indicator; + canvas.connect_draw(move |ref canvas, ref context| { + let error = cross_thread_state.read().unwrap().error; + let width = canvas.get_allocated_width() as f64; + let midpoint = width / 2.0; + let height = canvas.get_allocated_height() as f64; + + //flat on the left + context.set_source_rgb(0.0, 0.0, if error < 0.0 {-error/50.0} else {0.0}); + context.rectangle(0.0, 0.0, midpoint, 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.rectangle(midpoint, 0.0, width, height); + context.fill(); + + gtk::Inhibit(false) + }); +} + + fn setup_freq_drawing_area_callbacks(state: Rc>, cross_thread_state: Arc>) { let outer_state = state.clone(); let ref canvas = outer_state.borrow().ui.freq_chart; diff --git a/src/transforms.rs b/src/transforms.rs index 9eee406..0ff3e17 100644 --- a/src/transforms.rs +++ b/src/transforms.rs @@ -77,9 +77,7 @@ pub fn find_fundamental_frequency_correlation(input: &Vec, sample_rate: f64 .fold((0, 0.0 as f64), |(xi, xmag), (yi, &ymag)| if ymag > xmag { (yi, ymag) } else { (xi, xmag) }); let (peak_index, _) = peak; - - let peak_period = peak_index as f64 / sample_rate; - Some(peak_period.recip()) + Some(sample_rate / peak_index as f64) } #[cfg(test)] @@ -160,6 +158,21 @@ mod tests { } } +pub fn hz_to_midi_number(hz: f64) -> f64 { + 69.0 + 12.0 * (hz / 440.0).log2() +} + +pub fn hz_to_cents_error(hz: f64) -> f64 { + let midi_number = hz_to_midi_number(hz); + let cents = (midi_number * 100.0).round() % 100.0; + if cents >= 50.0 { + cents - 100.0 + } + else { + cents + } +} + pub fn hz_to_pitch(hz: f64) -> String { let pitch_names = [ "C", @@ -176,7 +189,7 @@ pub fn hz_to_pitch(hz: f64) -> String { "B" ]; - let midi_number = 69.0 + 12.0 * (hz / 440.0).log2(); + let midi_number = hz_to_midi_number(hz); //midi_number of 0 is C-1. let rounded_pitch = midi_number.round() as i32; @@ -188,6 +201,7 @@ pub fn hz_to_pitch(hz: f64) -> String { let mut cents = ((midi_number * 100.0).round() % 100.0) as i32; if cents >= 50 { + //don't need to adjust pitch here, that's already been done by the round above cents -= 100; } -- cgit v1.2.3