diff --git a/index.html b/index.html index 14bb85e..d0a4218 100644 --- a/index.html +++ b/index.html @@ -21,7 +21,6 @@ -

@@ -31,7 +30,7 @@

--Hz
-
--
+
--
--cents ♭cents ♯
diff --git a/js/pitchdetect.js b/js/pitchdetect.js index 213666a..281e2c3 100644 --- a/js/pitchdetect.js +++ b/js/pitchdetect.js @@ -31,172 +31,176 @@ var analyser = null; var theBuffer = null; var DEBUGCANVAS = null; var mediaStreamSource = null; -var detectorElem, - canvasElem, - waveCanvas, - pitchElem, - noteElem, - detuneElem, - detuneAmount; +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; - }; + 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/whitman.mp3", 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.'); + 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); - } + 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(); + // 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"; + audioContext.resume(); + 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); + audioContext.resume(); + 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"; + audioContext.resume(); + console.log(`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; @@ -207,168 +211,168 @@ 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; + 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); + return 440 * Math.pow(2,(note-69)/12); } function centsOffFromPitch( frequency, note ) { - return Math.floor( 1200 * Math.log( frequency / frequencyFromNoteNumber( note ))/Math.log(2) ); + 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 = 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. -var GOOD_ENOUGH_CORRELATION = 0.9; // this is the "bar" for how close a correlation needs to be - -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;iGOOD_ENOUGH_CORRELATION) && (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; -} + var MIN_SAMPLES = 0; // will be initialized when AudioContext is created. + var GOOD_ENOUGH_CORRELATION = 0.9; // this is the "bar" for how close a correlation needs to be + + 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;iGOOD_ENOUGH_CORRELATION) && (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 ); + 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/sounds/whitman.mp3 b/sounds/whitman.mp3 new file mode 100644 index 0000000..fe49d37 Binary files /dev/null and b/sounds/whitman.mp3 differ