summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJustin Worthe <justin@worthe-it.co.za>2017-07-08 13:09:25 +0200
committerJustin Worthe <justin@worthe-it.co.za>2017-07-08 13:09:25 +0200
commitafc6119fadbaf928b91c62a4f75d1798414c8048 (patch)
tree09d696e2ca6ad238d112d966dbe4d066768caa5e /src
parent2f7ec2a25679598862aa5e27218ba02e776cfc85 (diff)
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.
Diffstat (limited to 'src')
-rw-r--r--src/audio.rs37
-rw-r--r--src/gui.rs32
-rw-r--r--src/transforms.rs131
3 files changed, 94 insertions, 106 deletions
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<pa::PortAudio, pa::Error> {
@@ -29,24 +29,13 @@ pub fn get_default_device(pa: &pa::PortAudio) -> Result<u32, pa::Error> {
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<Vec<f64>>) -> Result<pa::Stream<pa::NonBlocking, pa::Input<f32>>, pa::Error> {
+pub fn start_listening_default(pa: &pa::PortAudio, sender: Sender<Vec<f32>>) -> Result<pa::Stream<pa::NonBlocking, pa::Input<f32>>, 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<Vec<f64>>) -> Result<pa::Stream<pa::NonBlocking, pa::Input<f32>>, pa::Error> {
+ sender: Sender<Vec<f32>>) -> Result<pa::Stream<pa::NonBlocking, pa::Input<f32>>, 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::<f32>::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<f64>,
+ fundamental_frequency: Option<f32>,
pitch: String,
- error: Option<f64>,
- signal: Vec<f64>,
- correlation: Vec<f64>
+ error: Option<f32>,
+ signal: Vec<f32>,
+ correlation: Vec<f32>
}
pub fn start_gui() -> Result<(), String> {
@@ -137,7 +137,7 @@ fn set_dropdown_items(dropdown: &gtk::ComboBoxText, microphones: Vec<(u32, Strin
dropdown.set_active_id(Some(format!("{}", default_mic).as_ref()));
}
-fn connect_dropdown_choose_microphone(mic_sender: Sender<Vec<f64>>, state: Rc<RefCell<ApplicationState>>) {
+fn connect_dropdown_choose_microphone(mic_sender: Sender<Vec<f32>>, state: Rc<RefCell<ApplicationState>>) {
let dropdown = state.borrow().ui.dropdown.clone();
start_listening_current_dropdown_value(&dropdown, mic_sender.clone(), state.clone());
dropdown.connect_changed(move |dropdown: &gtk::ComboBoxText| {
@@ -145,7 +145,7 @@ fn connect_dropdown_choose_microphone(mic_sender: Sender<Vec<f64>>, state: Rc<Re
});
}
-fn start_listening_current_dropdown_value(dropdown: &gtk::ComboBoxText, mic_sender: Sender<Vec<f64>>, state: Rc<RefCell<ApplicationState>>) {
+fn start_listening_current_dropdown_value(dropdown: &gtk::ComboBoxText, mic_sender: Sender<Vec<f32>>, state: Rc<RefCell<ApplicationState>>) {
match state.borrow_mut().pa_stream {
Some(ref mut stream) => {stream.stop().ok();},
_ => {}
@@ -161,7 +161,7 @@ fn start_listening_current_dropdown_value(dropdown: &gtk::ComboBoxText, mic_send
state.borrow_mut().pa_stream = stream;
}
-fn start_processing_audio(mic_receiver: Receiver<Vec<f64>>, cross_thread_state: Arc<RwLock<CrossThreadState>>) {
+fn start_processing_audio(mic_receiver: Receiver<Vec<f32>>, cross_thread_state: Arc<RwLock<CrossThreadState>>) {
thread::spawn(move || {
loop {
let mut samples = None;
@@ -226,7 +226,7 @@ fn setup_pitch_error_indicator_callbacks(state: Rc<RefCell<ApplicationState>>, 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<RefCell<ApplicationState>>, 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<RefCell<ApplicationState>
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<RefCell<ApplicationState>>
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<RefCell<ApplicationState>>
//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<f64>) -> Vec<f64> {
- let mean_input = input.iter().sum::<f64>()/input.len() as f64;
- input.iter().map(|x|x-mean_input).collect()
-}
-
-pub fn correlation(input: &Vec<f64>) -> Vec<f64> {
- 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<f32>) -> Vec<f32> {
+ let mean = signal.iter().sum::<f32>()/signal.len() as f32;
+ signal.iter().map(|x| x - mean).collect()
+}
+
+pub fn correlation(signal: &Vec<f32>) -> Vec<f32> {
+ (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<f64>, sample_rate: f64) -> Option<f64> {
- let intensities = remove_mean_offset(&input);
+pub fn find_fundamental_frequency_correlation(signal: &Vec<f32>, sample_rate: f32) -> Option<f32> {
+ 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<f64>, sample_rate: f64
}
}
-fn refine_fundamentals(correlation: &Vec<f64>, 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<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);
-
- 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<f64>, fundamental: f64) -> bool {
+fn is_noise(correlation: &Vec<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);
@@ -81,22 +76,20 @@ fn is_noise(correlation: &Vec<f64>, fundamental: f64) -> bool {
value_at_point > 2.0*score
}
-fn score_guess(correlation: &Vec<f64>, period: f64, data_points: usize) -> f64 {
- let mut score = 0.0;
- for i in 1..data_points {
+fn score_guess(correlation: &Vec<f32>, 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<f64>, x: f64) -> f64 {
+fn interpolate(correlation: &Vec<f32>, 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<f64>, 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<f64> {
+ fn sample_sinusoud(amplitude: f32, frequency: f32, phase: f32) -> Vec<f32> {
(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<f64>) -> Vec<f64> {
+pub fn align_to_rising_edge(samples: &Vec<f32>) -> Vec<f32> {
remove_mean_offset(&samples)
.iter()
.skip_while(|x| !x.is_sign_negative())