summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--benches/transforms.rs10
-rw-r--r--src/audio.rs8
-rw-r--r--src/correlation.rs158
-rw-r--r--src/emscripten_api.rs32
-rw-r--r--src/gui.rs30
-rw-r--r--src/lib.rs6
-rw-r--r--src/model.rs32
-rw-r--r--src/pitch.rs89
-rw-r--r--src/signal.rs48
-rw-r--r--src/transforms.rs245
-rw-r--r--web/main.js4
11 files changed, 361 insertions, 301 deletions
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<f32> {
}
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<u32, pa::Error> {
Ok(default_input_index)
}
-pub fn start_listening_default(pa: &pa::PortAudio, sender: Sender<Vec<f32>>) -> Result<pa::Stream<pa::NonBlocking, pa::Input<f32>>, pa::Error> {
+pub fn start_listening_default(pa: &pa::PortAudio, sender: Sender<Signal>) -> 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<f32>>) -> Result<pa::Stream<pa::NonBlocking, pa::Input<f32>>, pa::Error> {
+ sender: Sender<Signal>) -> 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;
@@ -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<f32>
+}
+
+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<Pitch> {
+ 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<f32> = (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: &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<f32>>, state: Rc<RefCell<ApplicationState>>) {
+fn connect_dropdown_choose_microphone(mic_sender: Sender<Signal>, state: Rc<RefCell<ApplicationState>>) {
let dropdown = state.borrow().ui.dropdown.clone();
start_listening_current_dropdown_value(&dropdown, mic_sender.clone(), &state);
dropdown.connect_changed(move |dropdown: &gtk::ComboBoxText| {
@@ -134,7 +135,7 @@ fn connect_dropdown_choose_microphone(mic_sender: Sender<Vec<f32>>, state: Rc<Re
});
}
-fn start_listening_current_dropdown_value(dropdown: &gtk::ComboBoxText, mic_sender: Sender<Vec<f32>>, state: &Rc<RefCell<ApplicationState>>) {
+fn start_listening_current_dropdown_value(dropdown: &gtk::ComboBoxText, mic_sender: Sender<Signal>, state: &Rc<RefCell<ApplicationState>>) {
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: &gtk::ComboBoxText, mic_send
state.borrow_mut().pa_stream = stream;
}
-fn start_processing_audio(mic_receiver: Receiver<Vec<f32>>, cross_thread_state: Arc<RwLock<Model>>) {
+fn start_processing_audio(mic_receiver: Receiver<Signal>, cross_thread_state: Arc<RwLock<Model>>) {
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<RefCell<ApplicationState>>, 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<RefCell<ApplicationState>>,
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<RefCell<ApplicationState
let canvas = &state.borrow().ui.oscilloscope_chart;
canvas.connect_draw(move |canvas, context| {
if let Ok(cross_thread_state) = cross_thread_state.read() {
- let signal = &cross_thread_state.signal;
+ let samples = &cross_thread_state.signal.aligned_to_rising_edge();
let width = f64::from(canvas.get_allocated_width());
// Set as a constant so signal won't change size based on
@@ -242,7 +242,7 @@ 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 samples.iter().enumerate() {
let x = i as f64 * width / len;
let y = mid_height - (f64::from(intensity) * mid_height / max);
context.line_to(x, y);
@@ -269,15 +269,15 @@ fn setup_correlation_drawing_area_callbacks(state: &Rc<RefCell<ApplicationState>
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<RefCell<ApplicationState>
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<f32>,
- pub pitch: String,
- pub error: Option<f32>,
- pub signal: Vec<f32>,
- pub correlation: Vec<f32>
+ pub pitch: Option<Pitch>,
+ 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<f32>,
+ 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<f32> {
+ let mean = samples.iter().sum::<f32>()/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<f32> {
- let mean = signal.iter().sum::<f32>()/signal.len() as f32;
- signal.iter().map(|x| x - mean).collect()
-}
-
-pub fn correlation(signal: &[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(signal: &[f32], sample_rate: f32) -> Option<f32> {
- 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<f32> {
- (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<f32> = 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<f32> {
- 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);
});
}