├── img └── forkme.png ├── README.md ├── LICENSE.txt ├── index.html └── js └── pitchdetect.js /img/forkme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwilso/PitchDetect/HEAD/img/forkme.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple pitch detection 2 | 3 | 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. 4 | 5 | Live instance hosted on Github at https://cwilso.github.io/PitchDetect/. 6 | 7 | Check it out, feel free to fork, submit pull requests, etc. MIT-Licensed - party on. 8 | 9 | -Chris 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Chris Wilson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |27 | 28 | 29 | 30 | 31 |
32 | 33 |
44 |
45 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/js/pitchdetect.js:
--------------------------------------------------------------------------------
1 | /*
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2014 Chris Wilson
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | */
24 |
25 | window.AudioContext = window.AudioContext || window.webkitAudioContext;
26 |
27 | var audioContext = null;
28 | var isPlaying = false;
29 | var sourceNode = null;
30 | var analyser = null;
31 | var theBuffer = null;
32 | var DEBUGCANVAS = null;
33 | var mediaStreamSource = null;
34 | var detectorElem,
35 | canvasElem,
36 | waveCanvas,
37 | pitchElem,
38 | noteElem,
39 | detuneElem,
40 | detuneAmount;
41 |
42 | window.onload = function() {
43 | audioContext = new AudioContext();
44 | MAX_SIZE = Math.max(4,Math.floor(audioContext.sampleRate/5000)); // corresponds to a 5kHz signal
45 |
46 | detectorElem = document.getElementById( "detector" );
47 | canvasElem = document.getElementById( "output" );
48 | DEBUGCANVAS = document.getElementById( "waveform" );
49 | if (DEBUGCANVAS) {
50 | waveCanvas = DEBUGCANVAS.getContext("2d");
51 | waveCanvas.strokeStyle = "black";
52 | waveCanvas.lineWidth = 1;
53 | }
54 | pitchElem = document.getElementById( "pitch" );
55 | noteElem = document.getElementById( "note" );
56 | detuneElem = document.getElementById( "detune" );
57 | detuneAmount = document.getElementById( "detune_amt" );
58 |
59 | detectorElem.ondragenter = function () {
60 | this.classList.add("droptarget");
61 | return false; };
62 | detectorElem.ondragleave = function () { this.classList.remove("droptarget"); return false; };
63 | detectorElem.ondrop = function (e) {
64 | this.classList.remove("droptarget");
65 | e.preventDefault();
66 | theBuffer = null;
67 |
68 | var reader = new FileReader();
69 | reader.onload = function (event) {
70 | audioContext.decodeAudioData( event.target.result, function(buffer) {
71 | theBuffer = buffer;
72 | }, function(){alert("error loading!");} );
73 |
74 | };
75 | reader.onerror = function (event) {
76 | alert("Error: " + reader.error );
77 | };
78 | reader.readAsArrayBuffer(e.dataTransfer.files[0]);
79 | return false;
80 | };
81 |
82 | fetch('whistling3.ogg')
83 | .then((response) => {
84 | if (!response.ok) {
85 | throw new Error(`HTTP error, status = ${response.status}`);
86 | }
87 | return response.arrayBuffer();
88 | }).then((buffer) => audioContext.decodeAudioData(buffer)).then((decodedData) => {
89 | theBuffer = decodedData;
90 | });
91 |
92 | }
93 |
94 | function startPitchDetect() {
95 | // grab an audio context
96 | audioContext = new AudioContext();
97 |
98 | // Attempt to get audio input
99 | navigator.mediaDevices.getUserMedia(
100 | {
101 | "audio": {
102 | "mandatory": {
103 | "googEchoCancellation": "false",
104 | "googAutoGainControl": "false",
105 | "googNoiseSuppression": "false",
106 | "googHighpassFilter": "false"
107 | },
108 | "optional": []
109 | },
110 | }).then((stream) => {
111 | // Create an AudioNode from the stream.
112 | mediaStreamSource = audioContext.createMediaStreamSource(stream);
113 |
114 | // Connect it to the destination.
115 | analyser = audioContext.createAnalyser();
116 | analyser.fftSize = 2048;
117 | mediaStreamSource.connect( analyser );
118 | updatePitch();
119 | }).catch((err) => {
120 | // always check for errors at the end.
121 | console.error(`${err.name}: ${err.message}`);
122 | alert('Stream generation failed.');
123 | });
124 | }
125 |
126 | function toggleOscillator() {
127 | if (isPlaying) {
128 | //stop playing and return
129 | sourceNode.stop( 0 );
130 | sourceNode = null;
131 | analyser = null;
132 | isPlaying = false;
133 | if (!window.cancelAnimationFrame)
134 | window.cancelAnimationFrame = window.webkitCancelAnimationFrame;
135 | window.cancelAnimationFrame( rafID );
136 | return "play oscillator";
137 | }
138 | sourceNode = audioContext.createOscillator();
139 |
140 | analyser = audioContext.createAnalyser();
141 | analyser.fftSize = 2048;
142 | sourceNode.connect( analyser );
143 | analyser.connect( audioContext.destination );
144 | sourceNode.start(0);
145 | isPlaying = true;
146 | isLiveInput = false;
147 | updatePitch();
148 |
149 | return "stop";
150 | }
151 |
152 | function toggleLiveInput() {
153 | if (isPlaying) {
154 | //stop playing and return
155 | sourceNode.stop( 0 );
156 | sourceNode = null;
157 | analyser = null;
158 | isPlaying = false;
159 | if (!window.cancelAnimationFrame)
160 | window.cancelAnimationFrame = window.webkitCancelAnimationFrame;
161 | window.cancelAnimationFrame( rafID );
162 | }
163 | getUserMedia(
164 | {
165 | "audio": {
166 | "mandatory": {
167 | "googEchoCancellation": "false",
168 | "googAutoGainControl": "false",
169 | "googNoiseSuppression": "false",
170 | "googHighpassFilter": "false"
171 | },
172 | "optional": []
173 | },
174 | }, gotStream);
175 | }
176 |
177 | function togglePlayback() {
178 | if (isPlaying) {
179 | //stop playing and return
180 | sourceNode.stop( 0 );
181 | sourceNode = null;
182 | analyser = null;
183 | isPlaying = false;
184 | if (!window.cancelAnimationFrame)
185 | window.cancelAnimationFrame = window.webkitCancelAnimationFrame;
186 | window.cancelAnimationFrame( rafID );
187 | return "start";
188 | }
189 |
190 | sourceNode = audioContext.createBufferSource();
191 | sourceNode.buffer = theBuffer;
192 | sourceNode.loop = true;
193 |
194 | analyser = audioContext.createAnalyser();
195 | analyser.fftSize = 2048;
196 | sourceNode.connect( analyser );
197 | analyser.connect( audioContext.destination );
198 | sourceNode.start( 0 );
199 | isPlaying = true;
200 | isLiveInput = false;
201 | updatePitch();
202 |
203 | return "stop";
204 | }
205 |
206 | var rafID = null;
207 | var tracks = null;
208 | var buflen = 2048;
209 | var buf = new Float32Array( buflen );
210 |
211 | var noteStrings = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
212 |
213 | function noteFromPitch( frequency ) {
214 | var noteNum = 12 * (Math.log( frequency / 440 )/Math.log(2) );
215 | return Math.round( noteNum ) + 69;
216 | }
217 |
218 | function frequencyFromNoteNumber( note ) {
219 | return 440 * Math.pow(2,(note-69)/12);
220 | }
221 |
222 | function centsOffFromPitch( frequency, note ) {
223 | return Math.floor( 1200 * Math.log( frequency / frequencyFromNoteNumber( note ))/Math.log(2) );
224 | }
225 |
226 | // this is the previously used pitch detection algorithm.
227 | /*
228 | var MIN_SAMPLES = 0; // will be initialized when AudioContext is created.
229 | var GOOD_ENOUGH_CORRELATION = 0.9; // this is the "bar" for how close a correlation needs to be
230 |
231 | function autoCorrelate( buf, sampleRate ) {
232 | var SIZE = buf.length;
233 | var MAX_SAMPLES = Math.floor(SIZE/2);
234 | var best_offset = -1;
235 | var best_correlation = 0;
236 | var rms = 0;
237 | var foundGoodCorrelation = false;
238 | var correlations = new Array(MAX_SAMPLES);
239 |
240 | for (var i=0;i