From f7b208ff5c3c9d77465e745bc9ded1d87ed32335 Mon Sep 17 00:00:00 2001 From: Justin Worthe Date: Mon, 3 Jul 2017 20:57:16 +0200 Subject: Added silence and noise detection Also updated GUI to present note and error in a friendlier way --- src/gui.rs | 20 +++++++++---- src/transforms.rs | 85 +++++++++++++++++++++++++++++-------------------------- 2 files changed, 60 insertions(+), 45 deletions(-) (limited to 'src') diff --git a/src/gui.rs b/src/gui.rs index 6ab2b50..41668a3 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -106,7 +106,7 @@ fn create_window(microphones: Vec<(u32, String)>) -> RustyUi { vbox.add(&pitch_label); let pitch_error_indicator = gtk::DrawingArea::new(); - pitch_error_indicator.set_size_request(600, 50); + pitch_error_indicator.set_size_request(600, 70); vbox.add(&pitch_error_indicator); let oscilloscope_chart = gtk::DrawingArea::new(); @@ -224,18 +224,28 @@ fn setup_pitch_error_indicator_callbacks(state: Rc>, c 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; + let line_indicator_height = 20.0; + let color_indicator_height = canvas.get_allocated_height() as f64 - line_indicator_height; + + + let error_line_x = midpoint + error * midpoint / 50.0; + context.new_path(); + context.move_to(error_line_x, 0.0); + context.line_to(error_line_x, line_indicator_height); + context.stroke(); + //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.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.rectangle(midpoint, 0.0, width, height); + context.rectangle(midpoint, line_indicator_height, width, color_indicator_height+line_indicator_height); context.fill(); gtk::Inhibit(false) @@ -251,7 +261,7 @@ fn setup_oscilloscope_drawing_area_callbacks(state: Rc let len = 512.0; //Set as a constant so signal won't change size based on zero point. let height = canvas.get_allocated_height() as f64; let mid_height = height / 2.0; - let max = signal.iter().map(|x| x.abs()).fold(0.0, |max, x| if max > x { max } else { x }); + let max = 1.0; context.new_path(); context.move_to(0.0, mid_height); diff --git a/src/transforms.rs b/src/transforms.rs index 7bcdf7c..eeb175e 100644 --- a/src/transforms.rs +++ b/src/transforms.rs @@ -13,16 +13,20 @@ impl FrequencyBucket { } } -pub fn fft(input: &Vec, sample_rate: f64) -> Vec { - let frames = input.len(); +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 fft(input: &Vec, sample_rate: f64) -> Vec { + let mut intensities = remove_mean_offset(&input); - let mut intensities = input.iter().map(|x|x-mean_input).collect::>(); + let frames = intensities.len(); let plan = dft::Plan::new(dft::Operation::Forward, frames); - dft::transform(&mut intensities, &plan); - let frequency_resolution = sample_rate / 2.0 / frames as f64; + dft::transform(&mut intensities, &plan); + intensities.iter().enumerate().map(|(index, &value)| { let index = index as f64; FrequencyBucket { @@ -33,21 +37,6 @@ pub fn fft(input: &Vec, sample_rate: f64) -> Vec { }).collect() } -pub fn find_fundamental_frequency(frequency_domain: &Vec) -> Option { - //TODO look at all significant frequencies, find fundamental - //TODO return None is none of them are significant - let positive_buckets = frequency_domain.iter().filter(|x| x.intensity > 0.0).cloned().collect::>(); - let average_intensity = positive_buckets.iter().map(|x| x.intensity).sum::() / frequency_domain.len() as f64; - let significant_buckets = positive_buckets.iter().filter(|x| x.intensity > average_intensity).cloned().collect::>(); - - let max_bucket = significant_buckets.iter() - .fold(None as Option<::transforms::FrequencyBucket>, |max, next| - if max.is_none() || max.clone().unwrap().intensity < next.intensity { Some(next.clone()) } else { max } - ).unwrap(); - - Some(max_bucket.ave_freq()) -} - pub fn correlation(input: &Vec) -> Vec { let mut correlation = Vec::with_capacity(input.len()); for offset in 0..input.len() { @@ -62,7 +51,13 @@ pub fn correlation(input: &Vec) -> Vec { } pub fn find_fundamental_frequency_correlation(input: &Vec, sample_rate: f64) -> Option { - let correlation = correlation(&input); + let intensities = remove_mean_offset(&input); + + if intensities.iter().all(|&x| x.abs() < 0.1) { + return None; + } + + let correlation = correlation(&intensities); let mut first_peak_width = 0; for offset in 0..correlation.len() { @@ -83,8 +78,13 @@ pub fn find_fundamental_frequency_correlation(input: &Vec, sample_rate: f64 let (peak_index, _) = peak; let refined_peak_index = refine_fundamentals(&correlation, peak_index as f64); - - Some(sample_rate / refined_peak_index) + + if is_noise(&correlation, refined_peak_index) { + None + } + else { + Some(sample_rate / refined_peak_index) + } } fn refine_fundamentals(correlation: &Vec, initial_guess: f64) -> f64 { @@ -107,15 +107,19 @@ fn refine_fundamentals(correlation: &Vec, initial_guess: f64) -> f64 { (low_bound + high_bound) / 2.0 } +fn is_noise(correlation: &Vec, fundamental: f64) -> 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: &Vec, period: f64, data_points: usize) -> f64 { let mut score = 0.0; - for i in 0..data_points { - if i % 2 == 0 { - score += i as f64 * interpolate(&correlation, i as f64 * period / 2.0); - } - else { - score -= i as f64 * interpolate(&correlation, i as f64 * period / 2.0); - } + for i in 1..data_points { + 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 } @@ -135,7 +139,7 @@ fn interpolate(correlation: &Vec, x: f64) -> f64 { let x1 = x.ceil(); let y1 = correlation[x1 as usize]; - if (x0 as usize == x1 as usize) { + if x0 as usize == x1 as usize { y0 } else { @@ -244,18 +248,18 @@ pub fn hz_to_cents_error(hz: f64) -> f64 { pub fn hz_to_pitch(hz: f64) -> String { let pitch_names = [ - "C", + "C ", "C#", - "D", + "D ", "Eb", - "E", - "F", + "E ", + "F ", "F#", - "G", + "G ", "G#", - "A", + "A ", "Bb", - "B" + "B " ]; let midi_number = hz_to_midi_number(hz); @@ -273,8 +277,9 @@ pub fn hz_to_pitch(hz: f64) -> String { //don't need to adjust pitch here, that's already been done by the round above cents -= 100; } - - format!("{}{} {:+}", name, octave, cents) + + format!("{}{}", name, octave) +// format!("{}{} {:+}", name, octave, cents) } #[test] -- cgit v1.2.3