diff --git a/LICENSE.txt b/LICENSE.txt
index 3345269..7cd919c 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2014 Chris Wilson
+Copyright (c) 2014-2015 Chris Wilson - modified by Mark Marijnissen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index a0d0440..d281e32 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,190 @@
-# Simple pitch detection
+# Pitch Detector
-I whipped this app up to start experimenting with pitch detection, and also to test live audio input. It used to perform a naive (zero-crossing based) pitch detection algorithm; now it uses a naively-implemented auto-correlation algorithm in realtime, so it should work well with most monophonic waveforms (although strong harmonics will throw it off a bit). It works well with whistling (which has a clear, simple waveform); it also works pretty well to tune my guitar.
+Based on Chris Wilson's work, an improved pitch detector. The Pitch Detector calculates auto-correlation score for a range of frequencies.
-Live instance hosted on https://webaudiodemos.appspot.com/pitchdetect/.
+See demo at http://lab.madebymark.nl/pitch-detector/example/.
-Check it out, feel free to fork, submit pull requests, etc. MIT-Licensed - party on.
+## Examples
--Chris
+* The **Y-Axis** is the auto-correlation score.
+* The **X-Axis** is the frequency range, from high (22
+Khz) to low (83 Hz).
+* The **Red bar** is the signal strength (RMS). The little dark red line is the minimal RMS.
+
+Detect best auto-correlation of all frequencies:
+
+![Auto-Correlation scores](example/example1.png)
+
+Detect the first peak auto-correlation, which is the highest frequency. Auto-correlation often detects lower octaves (and harmonies), so we can just stop after the first peak.
+
+![Auto-Correlation scores, detect the first peak correlation](example/example2.png)
+
+Detect a sudden increase in correlation:
+
+![Auto-Correlation scores, detect the first increase in correlation](example/example3.png)
+
+## Usage
+
+* `pitchdetector.js` contains the PitchDetector (logic only)
+* `pitchdetectorcanvas.js` allows you to visualize pitch detection on a canvas.
+* `example/gui.js` is a playground to test and tweak the pitch detector.
+
+Drop `pitchdetector.js` in your page, or require the CommonJS module using Webpack or Browserify.
+
+First, create a PitchDetector:
+```javascript
+var detector = new PitchDetector({
+ // Audio Context (Required)
+ context: new AudioContext(),
+
+ // Input AudioNode (Required)
+ input: audioBufferNode, // default: Microphone input
+
+ // Output AudioNode (Optional)
+ output: AudioNode, // default: no output
+
+ // interpolate frequency (Optional)
+ //
+ // Auto-correlation is calculated for different (discrete) signal periods
+ // The true frequency is often in-beween two periods.
+ //
+ // We can interpolate (very hacky) by looking at neighbours of the best
+ // auto-correlation period and shifting the frequency a bit towards the
+ // highest neighbour.
+ interpolateFrequency: true, // default: true
+
+ // Callback on pitch detection (Optional)
+ onDetect: function(stats, pitchDetector) {
+ stats.frequency // 440
+ stats.detected // --> true
+ stats.worst_correlation // 0.03 - local minimum, not global minimum!
+ stats.best_correlation // 0.98
+ stats.worst_period // 80
+ stats.best_period // 100
+ stats.time // 2.2332 - audioContext.currentTime
+ stats.rms // 0.02
+ },
+
+ // Debug Callback for visualisation (Optional)
+ onDebug: function(stats, pitchDetector) { },
+
+ // Minimal signal strength (RMS, Optional)
+ minRms: 0.01,
+
+ // Detect pitch only with minimal correlation of: (Optional)
+ minCorrelation: 0.9,
+
+ // Detect pitch only if correlation increases with at least: (Optional)
+ minCorreationIncrease: 0.5,
+
+ // Note: you cannot use minCorrelation and minCorreationIncrease
+ // at the same time!
+
+ // Signal Normalization (Optional)
+ normalize: "rms", // or "peak". default: undefined
+
+ // Only detect pitch once: (Optional)
+ stopAfterDetection: false,
+
+ // Buffer length (Optional)
+ length: 1024, // default 1024
+
+ // Limit range (Optional):
+ minNote: 69, // by MIDI note number
+ maxNote: 80,
+
+ minFrequency: 440, // by Frequency in Hz
+ maxFrequency: 20000,
+
+ minPeriod: 2, // by period (i.e. actual distance of calculation in audio buffer)
+ maxPeriod: 512, // --> convert to frequency: frequency = sampleRate / period
+
+ // Start right away
+ start: true, // default: false
+})
+```
+
+Then, start the pitch detection. It is tied to RequestAnimationFrame
+```javascript
+detector.start()
+```
+
+If you're done, you can stop or destroy the detector:
+```javascript
+detector.stop()
+detector.destroy()
+```
+
+You can also query the latest detected pitch:
+```javascript
+detector.getFrequency() // --> 440hz
+detector.getNoteNumber() // --> 69
+detector.getNoteString() // --> "A4"
+detector.getPeriod() // --> 100
+detector.getDetune() // --> 0
+detector.getCorrelation() // --> 0.95
+detector.getCorrelationIncrease() // --> 0.95
+
+// or raw data
+detector.stats = {
+ stats.frequency
+ stats.detected
+ stats.worst_correlation
+ stats.best_correlation
+ stats.worst_period
+ stats.best_period
+ stats.rms
+}
+```
+
+## Tips & Tricks
+
+### Always use an optimization
+
+* `minCorrelation` is the most reliable
+* `minCorreationIncrease` can sometimes give better results.
+
+### Use RMS or Peak normalization with minCorrelationIncrease
+
+The increase in correlation strongly depends on signal volume. Therefore, normalizing using `RMS` or `Peak` can make `minCorrelationIncrease` work much better.
+
+### Set a frequency range
+
+If you know what you're looking or, set a frequency range.
+
+**Warning:** `minCorrelationIncrease` needs a large frequency range to detect a difference. The frequency range must be large enough to include both a low and high auto-correlation.
+
+## Ideas to improve algorithm:
+
+* Draw an "optimal" auto-correlation shape, and calculate mean squared error (MSE) from measured auto-correlation score. When MSE is low enough, a pitch is detected. (or a combination of pitches!)
+* Implement DTMF demodulation as example
+* Learn a shape by recording auto-correlation scores of the perfect example. The resulting shape is the average of all recordes samples. Calculate standard deviation to see if the signal can be detected reliably.
+
+## Changelog
+
+### 0.2.0 (26/02/2015)
+
+* Used ScriptProcessingNode for faster analysis. Callbacks are still tied to the requestAnimationFrame.
+* Extracted the Canvas draw function into a seperate file.
+
+### 0.1.0 (25/02/2015)
+
+* Extract core logic (pitchdetector.js) from the GUI code (example/gui.js)
+* Add a new heuristic: detect a sudden increase in auto-correlation (when approaching the target frequency).
+* Added signal normalization (peak or rms)
+* Updated canvas visualization to draw correlation scores for every frequency.
+
+## Contribute
+
+I first want to check if the original author, Chris Wilson, is willing to pull my fork. So please check out the original version at https://github.com/cwilso/PitchDetect.
+
+## Credits
+
+Original code from [Chris Wilson](https://github.com/cwilso), improvements (see changelog) by [Mark Marijnissen](https://github.com/markmarijnissen)
+
+## Contact
+- @markmarijnissen
+- http://www.madebymark.nl
+- info@madebymark.nl
+
+© 2015 - Mark Marijnissen & Chris Wilson
diff --git a/example/example1.png b/example/example1.png
new file mode 100644
index 0000000..1fb4438
Binary files /dev/null and b/example/example1.png differ
diff --git a/example/example2.png b/example/example2.png
new file mode 100644
index 0000000..f43b112
Binary files /dev/null and b/example/example2.png differ
diff --git a/example/example3.png b/example/example3.png
new file mode 100644
index 0000000..726294f
Binary files /dev/null and b/example/example3.png differ
diff --git a/img/forkme.png b/example/forkme.png
similarity index 100%
rename from img/forkme.png
rename to example/forkme.png
diff --git a/example/gui.js b/example/gui.js
new file mode 100644
index 0000000..8125815
--- /dev/null
+++ b/example/gui.js
@@ -0,0 +1,288 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2014 Chris Wilson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+window.AudioContext = window.AudioContext || window.webkitAudioContext;
+
+$(function(){
+ // Global Variables
+ var audioContext = new AudioContext();
+ var osc = null;
+ var options = { start: true };
+ var needsReset = true;
+ var pitchDetector = null;
+ var theBuffer = null;
+
+ // Form Input Elements
+ var inputs = {
+ input: $('#input'),
+ notes: $('#notes'),
+ output: $('#output'),
+ length: $('#length'),
+ minRms: $('#minrms'),
+ normalize: $('#normalize'),
+ detection: $('#detection'),
+ minCorrelationIncrease: $('#strength'),
+ minCorrelation: $('#correlation'),
+ range: $('#range'),
+ min: $('#min'),
+ max: $('#max'),
+ draw: $('#draw'),
+ stopAfterDetection: $('#stopAfterDetection')
+ };
+
+
+ var data = JSON.parse(localStorage.getItem('pitch-detector-settings')) || {};
+ for(var x in data){
+ inputs[x].val(data[x]);
+ }
+
+ // GUI Elements
+ var gui = {
+ detector: $('#detector'),
+ canvas: $('#waveform'),
+ pitch: $('#pitch'),
+ note: $('#note'),
+ detuneBox: $('#detune'),
+ detune: $('#detune_amt')
+ };
+
+ // Canvas Element
+ canvasEl = $("#waveform").get(0);
+ canvas = canvasEl.getContext("2d");
+ window.savePic = function(){
+ window.open(canvasEl.toDataURL("image/png"));
+ };
+
+ // Show/Hide Stuff on Form Change
+ inputs.input.change(function(e){
+ needsReset = true;
+ var val = inputs.input.val();
+ if(val === 'mic') {
+ $('#notes').removeClass('invisible');
+ } else {
+ $('#notes').addClass('invisible');
+ }
+ });
+
+ inputs.output.change(function(e){
+ needsReset = true;
+ });
+
+ inputs.length.change(function(e){
+ needsReset = true;
+ });
+
+ inputs.range.change(function(e){
+ var val = inputs.range.val();
+ if(val !== 'none') {
+ $('.range').removeClass('hidden');
+ } else {
+ $('.range').addClass('hidden');
+ }
+ });
+
+ inputs.detection.change(function(e){
+ var val = inputs.detection.val();
+ $('.strength').addClass('hidden');
+ $('.correlation').addClass('hidden');
+ if(val === 'strength') {
+ $('.strength').removeClass('hidden');
+ } else if(val === 'correlation') {
+ $('.correlation').removeClass('hidden');
+ }
+ });
+
+ // Drag & Drop audio files
+ var detectorElem = gui.detector.get(0);
+ detectorElem.ondragenter = function () {
+ this.classList.add("droptarget");
+ return false; };
+ detectorElem.ondragleave = function () { this.classList.remove("droptarget"); return false; };
+ detectorElem.ondrop = function (e) {
+ this.classList.remove("droptarget");
+ e.preventDefault();
+ theBuffer = null;
+
+ var reader = new FileReader();
+ reader.onload = function (event) {
+ audioContext.decodeAudioData( event.target.result, function(buffer) {
+ theBuffer = buffer;
+ }, function(){alert("error loading!");} );
+
+ };
+ reader.onerror = function (event) {
+ alert("Error: " + reader.error );
+ };
+ reader.readAsArrayBuffer(e.dataTransfer.files[0]);
+ return false;
+ };
+
+ // Get example audio file
+ var request = new XMLHttpRequest();
+ request.open("GET", "./whistling3.ogg", true);
+ request.responseType = "arraybuffer";
+ request.onload = function() {
+ audioContext.decodeAudioData( request.response, function(buffer) {
+ theBuffer = buffer;
+ console.log('loaded audio');
+ } );
+ };
+ request.send();
+
+ // Global Methods
+ window.stopNote = function stopNote(){
+ if(osc) {
+ osc.stop();
+ osc.disconnect();
+ osc = null;
+ }
+ };
+
+ window.playNote = function playNote(freq){
+ stopNote();
+ osc = audioContext.createOscillator();
+ osc.connect(audioContext.destination);
+ osc.frequency.value = freq;
+ osc.start(0);
+ };
+
+ window.stop = function stop(){
+ if(pitchDetector) pitchDetector.destroy();
+ pitchDetector = null;
+ };
+
+ window.start = function start(){
+ if(needsReset && pitchDetector) {
+ pitchDetector.destroy();
+ pitchDetector = null;
+ }
+
+ var input = inputs.input.val();
+ var sourceNode;
+ if(input === 'osc'){
+ sourceNode = audioContext.createOscillator();
+ sourceNode.frequency.value = 440;
+ sourceNode.start(0);
+ } else if(input === 'audio'){
+ sourceNode = audioContext.createBufferSource();
+ sourceNode.buffer = theBuffer;
+ sourceNode.loop = true;
+ sourceNode.start(0);
+ } else {
+ inputs.output.prop('checked', false);
+ }
+ options.input = sourceNode;
+
+ if(inputs.output.is(':checked')){
+ options.output = audioContext.destination;
+ }
+
+ options.length = inputs.length.val() * 1;
+ options.stopAfterDetection = inputs.stopAfterDetection.is(':checked');
+
+ for(var key in options){
+ if(/^(min|max)/.test(key)){
+ delete options[key];
+ }
+ }
+
+ options.minRms = 1.0 * inputs.minRms.val() || 0.01;
+ var normalize = inputs.normalize.val();
+ if(normalize !== 'none'){
+ options.normalize = normalize;
+ } else {
+ options.normalize = false;
+ }
+
+ var detection = inputs.detection.val();
+ options.minCorrelationIncrease = false;
+ options.minCorrelation = false;
+ if(detection === 'correlation'){
+ options.minCorrelation = inputs.minCorrelation.val() * 1.0;
+ } else if(detection === 'strength') {
+ options.minCorrelationIncrease = inputs.minCorrelationIncrease.val() * 1.0;
+ }
+
+ var range = inputs.range.val();// Frequency, Period, Note
+ if(range !== 'none'){
+ options['min'+range] = inputs.min.val() * 1.0;
+ options['max'+range] = inputs.max.val() * 1.0;
+ }
+
+ options.onDebug = false;
+ options.onDetect = false;
+ options[inputs.draw.val()] = draw;
+
+ options.context = audioContext;
+ if(needsReset || !pitchDetector){
+ console.log('created PitchDetector',options);
+ pitchDetector = new PitchDetector(options);
+ needsReset = false;
+ } else {
+ pitchDetector.setOptions(options,true);
+ }
+ delete options.context;
+ delete options.output;
+ delete options.input;
+ $('#settings').text(JSON.stringify(options,null,4));
+ window.pitchDetector = pitchDetector;
+
+ var data = {};
+ for(x in inputs){
+ var el = inputs[x];
+ data[x] = el.val();
+ }
+ localStorage.setItem('pitch-detector-settings',JSON.stringify(data));
+ };
+
+ function draw( stats, detector ) {
+ PitchDetectorCanvasDraw(canvas, stats, detector);
+
+ // Update Pitch Detection GUI
+ if (!stats.detected) {
+ gui.detector.attr('class','vague');
+ gui.pitch.text('--');
+ gui.note.text('-');
+ gui.detuneBox.attr('class','');
+ gui.detune.text('--');
+ } else {
+ gui.detector.attr('class','confident');
+ var note = detector.getNoteNumber();
+ var detune = detector.getDetune();
+ gui.pitch.text( Math.round( stats.frequency ) );
+ gui.note.text(detector.getNoteString());
+ if (detune === 0){
+ gui.detuneBox.attr('class','');
+ gui.detune.text('--');
+ } else {
+ if (detune < 0)
+ gui.detuneBox.attr('class','flat');
+ else
+ gui.detuneBox.attr('class','sharp');
+ gui.detune.text(Math.abs( detune ));
+ }
+ }
+ }
+
+});
\ No newline at end of file
diff --git a/example/index.html b/example/index.html
new file mode 100644
index 0000000..f5802b8
--- /dev/null
+++ b/example/index.html
@@ -0,0 +1,188 @@
+
+
+
+Pitch Detector
+
+
+
+
+
+
+
+
+
+
+
Pitch Detector
+
+ Calculated using the auto-correlation algorithm for every frequency.
+
+
+
+
+
+
+
+
--Hz
+
--
+
-- cents ♭ cents ♯
+
+
+
+
+
+Y-Axis: RMS (Red), Auto-Correlation Increase (Blue), Auto-Correlation Score (Black).
+
+X-Axis: Frequency range (2-512 samples = 22.05 kHz - 83 Hz = F10 - E2) and detection area (Green)
+
+
Pitch Detector Settings:
+
+
+
+
+
+
+
diff --git a/example/whistling3.ogg b/example/whistling3.ogg
new file mode 100644
index 0000000..492986c
Binary files /dev/null and b/example/whistling3.ogg differ
diff --git a/index.html b/index.html
deleted file mode 100644
index 907bf60..0000000
--- a/index.html
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-Pitch Detector
-
-
-
-
-
-
-
-
-
-
-
-
-
-
--Hz
-
--
-
-
--cents ♭cents ♯
-
-
-
-
-
-
-
-
diff --git a/js/pitchdetect.js b/js/pitchdetect.js
deleted file mode 100644
index fc607b0..0000000
--- a/js/pitchdetect.js
+++ /dev/null
@@ -1,373 +0,0 @@
-/*
-The MIT License (MIT)
-
-Copyright (c) 2014 Chris Wilson
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-*/
-
-window.AudioContext = window.AudioContext || window.webkitAudioContext;
-
-var audioContext = null;
-var isPlaying = false;
-var sourceNode = null;
-var analyser = null;
-var theBuffer = null;
-var DEBUGCANVAS = null;
-var mediaStreamSource = null;
-var detectorElem,
- canvasElem,
- waveCanvas,
- pitchElem,
- noteElem,
- detuneElem,
- detuneAmount;
-
-window.onload = function() {
- audioContext = new AudioContext();
- MAX_SIZE = Math.max(4,Math.floor(audioContext.sampleRate/5000)); // corresponds to a 5kHz signal
- var request = new XMLHttpRequest();
- request.open("GET", "../sounds/whistling3.ogg", true);
- request.responseType = "arraybuffer";
- request.onload = function() {
- audioContext.decodeAudioData( request.response, function(buffer) {
- theBuffer = buffer;
- } );
- }
- request.send();
-
- detectorElem = document.getElementById( "detector" );
- canvasElem = document.getElementById( "output" );
- DEBUGCANVAS = document.getElementById( "waveform" );
- if (DEBUGCANVAS) {
- waveCanvas = DEBUGCANVAS.getContext("2d");
- waveCanvas.strokeStyle = "black";
- waveCanvas.lineWidth = 1;
- }
- pitchElem = document.getElementById( "pitch" );
- noteElem = document.getElementById( "note" );
- detuneElem = document.getElementById( "detune" );
- detuneAmount = document.getElementById( "detune_amt" );
-
- detectorElem.ondragenter = function () {
- this.classList.add("droptarget");
- return false; };
- detectorElem.ondragleave = function () { this.classList.remove("droptarget"); return false; };
- detectorElem.ondrop = function (e) {
- this.classList.remove("droptarget");
- e.preventDefault();
- theBuffer = null;
-
- var reader = new FileReader();
- reader.onload = function (event) {
- audioContext.decodeAudioData( event.target.result, function(buffer) {
- theBuffer = buffer;
- }, function(){alert("error loading!");} );
-
- };
- reader.onerror = function (event) {
- alert("Error: " + reader.error );
- };
- reader.readAsArrayBuffer(e.dataTransfer.files[0]);
- return false;
- };
-
-
-
-}
-
-function error() {
- alert('Stream generation failed.');
-}
-
-function getUserMedia(dictionary, callback) {
- try {
- navigator.getUserMedia =
- navigator.getUserMedia ||
- navigator.webkitGetUserMedia ||
- navigator.mozGetUserMedia;
- navigator.getUserMedia(dictionary, callback, error);
- } catch (e) {
- alert('getUserMedia threw exception :' + e);
- }
-}
-
-function gotStream(stream) {
- // Create an AudioNode from the stream.
- mediaStreamSource = audioContext.createMediaStreamSource(stream);
-
- // Connect it to the destination.
- analyser = audioContext.createAnalyser();
- analyser.fftSize = 2048;
- mediaStreamSource.connect( analyser );
- updatePitch();
-}
-
-function toggleOscillator() {
- if (isPlaying) {
- //stop playing and return
- sourceNode.stop( 0 );
- sourceNode = null;
- analyser = null;
- isPlaying = false;
- if (!window.cancelAnimationFrame)
- window.cancelAnimationFrame = window.webkitCancelAnimationFrame;
- window.cancelAnimationFrame( rafID );
- return "play oscillator";
- }
- sourceNode = audioContext.createOscillator();
-
- analyser = audioContext.createAnalyser();
- analyser.fftSize = 2048;
- sourceNode.connect( analyser );
- analyser.connect( audioContext.destination );
- sourceNode.start(0);
- isPlaying = true;
- isLiveInput = false;
- updatePitch();
-
- return "stop";
-}
-
-function toggleLiveInput() {
- if (isPlaying) {
- //stop playing and return
- sourceNode.stop( 0 );
- sourceNode = null;
- analyser = null;
- isPlaying = false;
- if (!window.cancelAnimationFrame)
- window.cancelAnimationFrame = window.webkitCancelAnimationFrame;
- window.cancelAnimationFrame( rafID );
- }
- getUserMedia(
- {
- "audio": {
- "mandatory": {
- "googEchoCancellation": "false",
- "googAutoGainControl": "false",
- "googNoiseSuppression": "false",
- "googHighpassFilter": "false"
- },
- "optional": []
- },
- }, gotStream);
-}
-
-function togglePlayback() {
- if (isPlaying) {
- //stop playing and return
- sourceNode.stop( 0 );
- sourceNode = null;
- analyser = null;
- isPlaying = false;
- if (!window.cancelAnimationFrame)
- window.cancelAnimationFrame = window.webkitCancelAnimationFrame;
- window.cancelAnimationFrame( rafID );
- return "start";
- }
-
- sourceNode = audioContext.createBufferSource();
- sourceNode.buffer = theBuffer;
- sourceNode.loop = true;
-
- analyser = audioContext.createAnalyser();
- analyser.fftSize = 2048;
- sourceNode.connect( analyser );
- analyser.connect( audioContext.destination );
- sourceNode.start( 0 );
- isPlaying = true;
- isLiveInput = false;
- updatePitch();
-
- return "stop";
-}
-
-var rafID = null;
-var tracks = null;
-var buflen = 1024;
-var buf = new Float32Array( buflen );
-
-var noteStrings = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
-
-function noteFromPitch( frequency ) {
- var noteNum = 12 * (Math.log( frequency / 440 )/Math.log(2) );
- return Math.round( noteNum ) + 69;
-}
-
-function frequencyFromNoteNumber( note ) {
- return 440 * Math.pow(2,(note-69)/12);
-}
-
-function centsOffFromPitch( frequency, note ) {
- return Math.floor( 1200 * Math.log( frequency / frequencyFromNoteNumber( note ))/Math.log(2) );
-}
-
-// this is a float version of the algorithm below - but it's not currently used.
-/*
-function autoCorrelateFloat( buf, sampleRate ) {
- var MIN_SAMPLES = 4; // corresponds to an 11kHz signal
- var MAX_SAMPLES = 1000; // corresponds to a 44Hz signal
- var SIZE = 1000;
- var best_offset = -1;
- var best_correlation = 0;
- var rms = 0;
-
- if (buf.length < (SIZE + MAX_SAMPLES - MIN_SAMPLES))
- return -1; // Not enough data
-
- for (var i=0;i best_correlation) {
- best_correlation = correlation;
- best_offset = offset;
- }
- }
- if ((rms>0.1)&&(best_correlation > 0.1)) {
- console.log("f = " + sampleRate/best_offset + "Hz (rms: " + rms + " confidence: " + best_correlation + ")");
- }
-// var best_frequency = sampleRate/best_offset;
-}
-*/
-
-var MIN_SAMPLES = 0; // will be initialized when AudioContext is created.
-
-function autoCorrelate( buf, sampleRate ) {
- var SIZE = buf.length;
- var MAX_SAMPLES = Math.floor(SIZE/2);
- var best_offset = -1;
- var best_correlation = 0;
- var rms = 0;
- var foundGoodCorrelation = false;
- var correlations = new Array(MAX_SAMPLES);
-
- for (var i=0;i0.9) && (correlation > lastCorrelation)) {
- foundGoodCorrelation = true;
- if (correlation > best_correlation) {
- best_correlation = correlation;
- best_offset = offset;
- }
- } else if (foundGoodCorrelation) {
- // short-circuit - we found a good correlation, then a bad one, so we'd just be seeing copies from here.
- // Now we need to tweak the offset - by interpolating between the values to the left and right of the
- // best offset, and shifting it a bit. This is complex, and HACKY in this code (happy to take PRs!) -
- // we need to do a curve fit on correlations[] around best_offset in order to better determine precise
- // (anti-aliased) offset.
-
- // we know best_offset >=1,
- // since foundGoodCorrelation cannot go to true until the second pass (offset=1), and
- // we can't drop into this clause until the following pass (else if).
- var shift = (correlations[best_offset+1] - correlations[best_offset-1])/correlations[best_offset];
- return sampleRate/(best_offset+(8*shift));
- }
- lastCorrelation = correlation;
- }
- if (best_correlation > 0.01) {
- // console.log("f = " + sampleRate/best_offset + "Hz (rms: " + rms + " confidence: " + best_correlation + ")")
- return sampleRate/best_offset;
- }
- return -1;
-// var best_frequency = sampleRate/best_offset;
-}
-
-function updatePitch( time ) {
- var cycles = new Array;
- analyser.getFloatTimeDomainData( buf );
- var ac = autoCorrelate( buf, audioContext.sampleRate );
- // TODO: Paint confidence meter on canvasElem here.
-
- if (DEBUGCANVAS) { // This draws the current waveform, useful for debugging
- waveCanvas.clearRect(0,0,512,256);
- waveCanvas.strokeStyle = "red";
- waveCanvas.beginPath();
- waveCanvas.moveTo(0,0);
- waveCanvas.lineTo(0,256);
- waveCanvas.moveTo(128,0);
- waveCanvas.lineTo(128,256);
- waveCanvas.moveTo(256,0);
- waveCanvas.lineTo(256,256);
- waveCanvas.moveTo(384,0);
- waveCanvas.lineTo(384,256);
- waveCanvas.moveTo(512,0);
- waveCanvas.lineTo(512,256);
- waveCanvas.stroke();
- waveCanvas.strokeStyle = "black";
- waveCanvas.beginPath();
- waveCanvas.moveTo(0,buf[0]);
- for (var i=1;i<512;i++) {
- waveCanvas.lineTo(i,128+(buf[i]*128));
- }
- waveCanvas.stroke();
- }
-
- if (ac == -1) {
- detectorElem.className = "vague";
- pitchElem.innerText = "--";
- noteElem.innerText = "-";
- detuneElem.className = "";
- detuneAmount.innerText = "--";
- } else {
- detectorElem.className = "confident";
- pitch = ac;
- pitchElem.innerText = Math.round( pitch ) ;
- var note = noteFromPitch( pitch );
- noteElem.innerHTML = noteStrings[note%12];
- var detune = centsOffFromPitch( pitch, note );
- if (detune == 0 ) {
- detuneElem.className = "";
- detuneAmount.innerHTML = "--";
- } else {
- if (detune < 0)
- detuneElem.className = "flat";
- else
- detuneElem.className = "sharp";
- detuneAmount.innerHTML = Math.abs( detune );
- }
- }
-
- if (!window.requestAnimationFrame)
- window.requestAnimationFrame = window.webkitRequestAnimationFrame;
- rafID = window.requestAnimationFrame( updatePitch );
-}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..513e6b9
--- /dev/null
+++ b/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "pitch-detector",
+ "version": "0.2.0",
+ "description": "Detect pitch with the auto-correlation algorithm using the Web Audio API",
+ "main": "pitchdetector.js",
+ "directories": {
+ "example": "example"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/markmarijnissen/PitchDetect.git"
+ },
+ "keywords": [
+ "pitch",
+ "web",
+ "audio",
+ "auto-correlation"
+ ],
+ "authors": ["Chris Wilson","Mark Marijnissen"],
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/markmarijnissen/PitchDetect/issues"
+ },
+ "homepage": "https://github.com/markmarijnissen/PitchDetect"
+}
diff --git a/pitchdetector.js b/pitchdetector.js
new file mode 100644
index 0000000..eb6d517
--- /dev/null
+++ b/pitchdetector.js
@@ -0,0 +1,535 @@
+
+/*
+The MIT License (MIT)
+
+Copyright (c) 2014-2015 Chris Wilson, modified by Mark Marijnissen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+(function(){
+var noteStrings = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
+
+function frequencyToNote( frequency ) {
+ var noteNum = 12 * (Math.log( frequency / 440 )/Math.log(2) );
+ return Math.round( noteNum ) + 69;
+}
+
+function frequencyToString( frequency ){
+ var note = frequencyToNote(frequency);
+ return noteStrings[note % 12] + Math.floor((note-12) / 12);
+}
+
+function noteToFrequency( note ) {
+ return 440 * Math.pow(2,(note-69)/12);
+}
+
+function noteToPeriod (note, sampleRate) {
+ return sampleRate / noteToFrequency(note);
+}
+
+function centsOffFromPitch( frequency, note ) {
+ return Math.floor( 1200 * Math.log( frequency / noteToFrequency( note ))/Math.log(2) );
+}
+
+function getLiveInput(context,callback){
+ try {
+ navigator.getUserMedia(
+ {
+ "audio": {
+ "mandatory": {
+ "googEchoCancellation": "false",
+ "googAutoGainControl": "false",
+ "googNoiseSuppression": "false",
+ "googHighpassFilter": "false"
+ },
+ },
+ }, function(stream){
+ var liveInputNode = context.createMediaStreamSource(stream);
+ callback(null,liveInputNode);
+ }, function(error){
+ console.error('getUserMedia error',error);
+ callback(error,null);
+ });
+ } catch(e) {
+ console.error('getUserMedia exception',e);
+ callback(e,null);
+ }
+}
+
+// prefix fixes
+var requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame;
+navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
+
+function PitchDetector(options){
+
+ // Options:
+ this.options = {
+ minRms: 0.01,
+ interpolateFrequency: true,
+ stopAfterDetection: false,
+ normalize: false,
+ minCorrelation: false,
+ length: options.length,
+ minCorrelationIncrease: false
+ };
+
+ // Internal Variables
+ this.context = options.context; // AudioContext
+ this.sampleRate = this.context.sampleRate; // sampleRate
+ //this.buffer = new Float32Array( options.length || 1024 ); // buffer array
+ this.MAX_SAMPLES = Math.floor(options.length/2); // MAX_SAMPLES number
+ this.correlations = new Array(this.MAX_SAMPLES); // correlation array
+ this.update = this.update.bind(this); // update function (bound to this)
+ this.started = false; // state flag (to cancel requestAnimationFrame)
+ this.input = null; // Audio Input Node
+ this.output = null; // Audio Output Node
+
+ // Stats:
+ this.stats = {
+ detected: false,
+ frequency: -1,
+ best_period: 0,
+ worst_period: 0,
+ best_correlation: 0.0,
+ worst_correlation: 0.0,
+ time: 0.0,
+ rms: 0.0,
+ };
+
+ this.lastOnDetect = 0.0;
+
+ // Set input
+ if(!options.input){
+ var self = this;
+ getLiveInput(this.context,function(err,input){
+ if(err){
+ console.error('getUserMedia error:',err);
+ } else {
+ self.input = input;
+ self.start();
+ }
+ });
+ } else {
+ this.input = options.input;
+ }
+
+ // Set output
+ if(options.output){
+ this.output = options.output;
+ }
+
+ // Set options
+ options.input = undefined; // 'input' option only allowed in constructor
+ options.output = undefined; // 'output' option only allowed in constructor
+ options.context = undefined; // 'context' option only allowed in constructor
+ options.length = undefined; // 'length' option only allowed in constructor
+ this.setOptions(options);
+}
+
+PitchDetector.prototype.setOptions = function(options,ignoreConstructorOnlyProperties){
+ var self = this;
+
+ // Override options (if defined)
+ ['minCorrelation','minCorrelationIncrease','minRms',
+ 'normalize','stopAfterDetection','interpolateFrequency',
+ 'onDebug','onDetect','onDestroy'
+ ].forEach(function(option){
+ if(typeof options[option] !== 'undefined') {
+ self.options[option] = options[option];
+ }
+ });
+
+ if(ignoreConstructorOnlyProperties !== true){
+ // Warn if you're setting Constructor-only options!
+ ['input','output','length','context'].forEach(function(option){
+ if(typeof options[option] !== 'undefined'){
+ console.warn('PitchDetector: Cannot set option "'+option+'"" after construction!');
+ }
+ });
+ }
+
+ // Set frequency domain (i.e. min-max period to detect frequencies on)
+ var minPeriod = options.minPeriod || this.options.minPeriod || 2;
+ var maxPeriod = options.maxPeriod || this.options.maxPeriod || this.MAX_SAMPLES;
+ if(options.note){
+ var period = Math.round(noteToPeriod(options.note,this.sampleRate));
+ minPeriod = period;
+ maxPeriod = period;
+ }
+ if(options.minNote){
+ maxPeriod = Math.round(noteToPeriod(options.minNote,this.sampleRate));
+ }
+ if(options.maxNote){
+ minPeriod = Math.round(noteToPeriod(options.maxNote,this.sampleRate));
+ }
+ if(options.minFrequency) {
+ maxPeriod = Math.floor(this.sampleRate / options.minFrequency);
+ }
+ if(options.maxFrequency) {
+ minPeriod = Math.ceil(this.sampleRate / options.maxFrequency);
+ }
+ if(options.periods){
+ this.periods = options.periods;
+ } else {
+ this.periods = [];
+ if(maxPeriod < minPeriod) {
+ var tmp = maxPeriod;
+ maxPeriod = minPeriod;
+ minPeriod = tmp;
+ }
+ var range = [1,1];
+ if(this.options.minCorrelation){
+ range = [1,1];
+ } else if(this.options.minCorrelationIncrease){
+ range = [10,1];
+ }
+ if(maxPeriod - minPeriod < 1 + range[0] + range[1]){
+ minPeriod = Math.floor(minPeriod - range[0]);
+ maxPeriod = Math.ceil(maxPeriod + range[1]);
+ }
+ maxPeriod = Math.min(maxPeriod,this.MAX_SAMPLES);
+ minPeriod = Math.max(2,minPeriod);
+ this.options.minPeriod = minPeriod;
+ this.options.maxPeriod = maxPeriod;
+ for(var i = minPeriod; i <= maxPeriod; i++){
+ this.periods.push(i);
+ }
+ }
+
+ // keep track of stats for visualization
+ if(options.onDebug){
+ this.debug = {
+ detected: false,
+ frequency: -1,
+ best_period: 0,
+ worst_period: 0,
+ best_correlation: 0.0,
+ worst_correlation: 0.0,
+ time: 0.0,
+ rms: 0.0,
+ };
+ }
+
+ // Autostart
+ if(options.start){
+ this.start();
+ }
+};
+
+PitchDetector.prototype.start = function(){
+ // Wait until input is defined (when waiting for microphone)
+ if(!this.analyser && this.input){
+ //this.analyser = this.context.createAnalyser();
+ //this.analyser.fftSize = this.buffer.length * 2;
+
+ this.analyser = this.context.createScriptProcessor(this.options.length);
+ this.analyser.onaudioprocess = this.autoCorrelate.bind(this);
+ this.input.connect(this.analyser);
+ if(this.output){
+ this.analyser.connect(this.output);
+ } else {
+ // webkit but, it requires an output....
+ // var dummyOutput = this.context.createGain();
+ // dummyOutput.gain.value= 0;
+ // dummyOutput.connect(this.context.destination);
+ var dummyOutput = this.context.createAnalyser();
+ dummyOutput.fftSize = 32;
+ this.analyser.connect(dummyOutput);
+ }
+ }
+ if(!this.started){
+ this.started = true;
+ requestAnimationFrame(this.update);
+ }
+};
+PitchDetector.prototype.update = function(event){
+ if(this.lastOnDetect !== this.stats.time){
+ this.lastOnDetect = this.stats.time;
+ if(this.options.onDetect){
+ this.options.onDetect(this.stats,this);
+ }
+ }
+ if(this.options.onDebug){
+ this.options.onDebug(this.debug,this);
+ }
+ if(this.started === true){
+ requestAnimationFrame(this.update);
+ }
+};
+
+PitchDetector.prototype.stop = function(){
+ this.started = false;
+};
+
+// Free op resources
+//
+// Note: It's not tested if it actually frees up resources
+PitchDetector.prototype.destroy = function(){
+ this.stop();
+ if(this.options.onDestroy){
+ this.options.onDestroy();
+ }
+ if(this.input && this.input.stop){
+ try {
+ this.input.stop(0);
+ } catch(e){}
+ }
+ if(this.input) this.input.disconnect();
+ if(this.analyser) this.analyser.disconnect();
+ this.input = null;
+ this.analyser = null;
+ this.context = null;
+ this.buffer = null;
+};
+
+/**
+ * Sync methoc to retrieve latest pitch in various forms:
+ */
+
+PitchDetector.prototype.getFrequency = function(){
+ return this.stats.frequency;
+};
+
+PitchDetector.prototype.getNoteNumber = function(){
+ return frequencyToNote(this.stats.frequency);
+};
+
+PitchDetector.prototype.getNoteString = function(){
+ return frequencyToString(this.stats.frequency);
+};
+
+PitchDetector.prototype.getPeriod = function(){
+ return this.stats.best_period;
+};
+
+PitchDetector.prototype.getCorrelation = function(){
+ return this.stats.best_correlation;
+};
+
+PitchDetector.prototype.getCorrelationIncrease = function(){
+ return this.stats.best_correlation - this.stats.worst_correlation;
+};
+
+PitchDetector.prototype.getDetune = function(){
+ return centsOffFromPitch(this.stats.frequency,frequencyToNote(this.stats.frequency));
+};
+
+/**
+ * AutoCorrelate algorithm
+ */
+PitchDetector.prototype.autoCorrelate = function AutoCorrelate(event){
+ if(!this.started) return;
+
+ // Keep track of best period/correlation
+ var best_period = 0;
+ var best_correlation = 0;
+
+ // Keep track of local minima (i.e. nearby low correlation)
+ var worst_period = 0;
+ var worst_correlation = 1;
+
+ // Remember previous correlation to determine if
+ // we're ascending (i.e. getting near a frequency in the signal)
+ // or descending (i.e. moving away from a frequency in the signal)
+ var last_correlation = 1;
+
+ // iterators
+ var i = 0; // for the different periods we're checking
+ var j = 0; // for the different "windows" we're checking
+ var period = 0; // current period we're checking.
+
+ // calculated stuff
+ var rms = 0;
+ var correlation = 0;
+ var peak = 0;
+
+ // early stop algorithm
+ var found_pitch = !this.options.minCorrelationIncrease && !this.options.minCorrelation;
+ var find_local_maximum = this.options.minCorrelationIncrease;
+
+ // Constants
+ this.buffer = event.inputBuffer.getChannelData(0);
+ var NORMALIZE = 1;
+ var BUFFER_LENGTH = this.buffer.length;
+ var PERIOD_LENGTH = this.periods.length;
+ var MAX_SAMPLES = this.MAX_SAMPLES;
+
+
+ // Check if there is enough signal
+ for (i=0; i< BUFFER_LENGTH;i++) {
+ rms += this.buffer[i]*this.buffer[i];
+ // determine peak volume
+ if(this.buffer[i] > peak) peak = this.buffer[i];
+ }
+ rms = Math.sqrt(rms/ BUFFER_LENGTH);
+
+ // Abort if not enough signal
+ if (rms< this.options.minRms) {
+ return false;
+ }
+
+ // Normalize (if configured)
+ if(this.options.normalize === 'rms') {
+ NORMALIZE = 2*rms;
+ } else if(this.options.normalize === 'peak') {
+ NORMALIZE = peak;
+ }
+
+ /**
+ * Test different periods (i.e. frequencies)
+ *
+ * Buffer: |----------------------------------------| (1024)
+ * i: | 1 44.1 kHz
+ * || 2 22.05 kHz
+ * |-| 3 14.7 kHz
+ * |--| 4 11 kHz
+ * ...
+ * |-------------------| 512 86hz
+ *
+ *
+ * frequency = sampleRate / period
+ * period = sampleRate / frequency
+ *
+ *
+ */
+ for (i=0; i < PERIOD_LENGTH; i++) {
+ period = this.periods[i];
+ correlation = 0;
+
+ /**
+ *
+ * Sum all differences
+ *
+ * Version 1: Use absolute difference
+ * Version 2: Use squared difference.
+ *
+ * Version 2 exagerates differences, which is a good property.
+ * So we'll use version 2.
+ *
+ * Buffer: |-------------------|--------------------| (1024)
+ * j:
+ * |---| 0
+ * |---| 1
+ * |---| 2
+ * ...
+ * |---| 512
+ *
+ * sum-of-differences
+ */
+ for (j=0; j < MAX_SAMPLES; j++) {
+ // Version 1: Absolute values
+ correlation += Math.abs((this.buffer[j])-(this.buffer[j+period])) / NORMALIZE;
+
+ // Version 2: Squared values (exagarates difference, works better)
+ //correlation += Math.pow((this.buffer[j]-this.buffer[j+period]) / NORMALIZE,2);
+ }
+
+ // Version 1: Absolute values
+ correlation = 1 - (correlation/MAX_SAMPLES);
+
+ // Version 2: Squared values
+ //correlation = 1 - Math.sqrt(correlation/MAX_SAMPLES);
+
+ // Save Correlation
+ this.correlations[period] = correlation;
+
+ // We're descending (i.e. moving towards frequencies that are NOT in here)
+ if(last_correlation > correlation){
+
+ // We already found a good correlation, so early stop!
+ if(this.options.minCorrelation && best_correlation > this.options.minCorrelation) {
+ found_pitch = true;
+ break;
+ }
+
+ // We already found a good correlationIncrease, so early stop!
+ if(this.options.minCorrelationIncrease && best_correlation - worst_correlation > this.options.minCorrelationIncrease){
+ found_pitch = true;
+ break;
+ }
+
+ // Save the worst correlation of the latest descend (local minima)
+ worst_correlation = correlation;
+ worst_period = period;
+
+ // we're ascending, and found a new high!
+ } else if(find_local_maximum || correlation > best_correlation){
+ best_correlation = correlation;
+ best_period = period;
+ }
+
+ last_correlation = correlation;
+ }
+
+ if(this.options.onDebug){
+ this.debug.detected = false;
+ this.debug.rms = rms;
+ this.debug.time = this.context.currentTime;
+ this.debug.best_period = best_period;
+ this.debug.worst_period = worst_period;
+ this.debug.best_correlation = best_correlation;
+ this.debug.worst_correlation = worst_correlation;
+ this.debug.frequency = best_period > 0? this.sampleRate/best_period: 0;
+ }
+
+ if (best_correlation > 0.01 && found_pitch) {
+ this.stats.detected = true;
+ this.stats.best_period = best_period;
+ this.stats.worst_period = worst_period;
+ this.stats.best_correlation = best_correlation;
+ this.stats.worst_correlation = worst_correlation;
+ this.stats.time = this.context.currentTime;
+ this.stats.rms = rms;
+
+ var shift = 0;
+ if(this.options.interpolateFrequency && i >= 3 && period >= best_period + 1 && this.correlations[best_period+1] && this.correlations[best_period-1]){
+ // Now we need to tweak the period - by interpolating between the values to the left and right of the
+ // best period, and shifting it a bit. This is complex, and HACKY in this code (happy to take PRs!) -
+ // we need to do a curve fit on this.correlations[] around best_period in order to better determine precise
+ // (anti-aliased) period.
+
+ // we know best_period >=1,
+ // since found_pitch cannot go to true until the second pass (period=1), and
+ // we can't drop into this clause until the following pass (else if).
+ shift = (this.correlations[best_period+1] - this.correlations[best_period-1]) / best_correlation;
+ shift = shift * 8;
+ }
+ this.stats.frequency = this.sampleRate/(best_period + shift);
+
+ if(this.options.onDebug){
+ this.debug.detected = true;
+ this.debug.frequency = this.stats.frequency;
+ }
+ if(this.options.stopAfterDetection){
+ this.started = false;
+ }
+ return true;
+ } else {
+ return false;
+ }
+};
+
+// Export on Window or as CommonJS module
+if(typeof module !== 'undefined') {
+ module.exports = PitchDetector;
+} else {
+ window.PitchDetector = PitchDetector;
+}
+})();
diff --git a/pitchdetectorcanvas.js b/pitchdetectorcanvas.js
new file mode 100644
index 0000000..7f98054
--- /dev/null
+++ b/pitchdetectorcanvas.js
@@ -0,0 +1,99 @@
+(function(){
+ var PitchDetectorCanvasDraw = function PitchDetectorCanvasDraw(canvas,stats,pitchDetector){
+ if(!pitchDetector || !pitchDetector.buffer) return;
+ var buf = pitchDetector.buffer;
+ var i = 0, val = 0, len = 0, start = 20, end = 50;
+ if(pitchDetector.periods){
+ start = pitchDetector.periods[0] + 20;
+ end = pitchDetector.periods[pitchDetector.periods.length-1] + 20;
+ }
+ var width = end-start;
+
+ canvas.clearRect(0,0,512,256);
+ canvas.fillStyle = "#EEFFEE";
+ canvas.fillRect(start,0,width,256);
+
+ // AREA: Draw Pitch Detection Area
+ if(pitchDetector.options.minCorrelation){
+ canvas.fillStyle = "yellow";
+ canvas.fillRect(start,0,width,(1-pitchDetector.options.minCorrelation) * 256);
+ }
+
+ // AREA: Draw Correlations
+ if(pitchDetector.correlations){
+ canvas.beginPath();
+ canvas.strokeStyle = "black";
+ if(pitchDetector.options.minCorrelation || pitchDetector.options.minCorrelationIncrease){
+ len = Math.max(stats.worst_period,stats.best_period + 1);
+ } else {
+ len = pitchDetector.correlations.length;
+ }
+ for(i = 0; i