Last active
January 5, 2017 01:06
-
-
Save amiika/5271149 to your computer and use it in GitHub Desktop.
Pitch detection
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<!-- Forked from https://github.com/cwilso/PitchDetect --> | |
<html> | |
<head> | |
<title>Pitch Detector</title> | |
<link href='http://fonts.googleapis.com/css?family=Alike' rel='stylesheet' type='text/css'> | |
<style> | |
body { font: 14pt 'Alike', sans-serif;} | |
#note { font-size: 164px; } | |
.droptarget { background-color: #348781} | |
div.confident { color: black; } | |
div.vague { color: lightgrey; } | |
#note { display: inline-block; width: 1em; text-align: left;} | |
#detector { width: 300px; height: 300px; border: 4px solid gray; border-radius: 8px; text-align: center;} | |
#output { position: absolute; width: 300px; height: 300px; } | |
#flat { display: none; } | |
#sharp { display: none; } | |
.flat #flat { display: inline; } | |
.sharp #sharp { display: inline; } | |
</style> | |
</head> | |
<body> | |
<button onclick="toggleLiveInput()">use live input</button> | |
<!--<button onclick="updatePitch(0);">sample</button>--> | |
<div id="detector" class="vague"> | |
<canvas id="output"></canvas> | |
<div class="pitch"><span id="pitch">--</span>Hz</div> | |
<div class="note"><span id="note">--</span></div> | |
<div id="detune"><span id="detune_amt">--</span><span id="flat">cents ♭</span><span id="sharp">cents ♯</span></div> | |
</div> | |
<script type="text/javascript"> | |
var audioContext = new webkitAudioContext(); | |
var isPlaying = false; | |
var sourceNode = null; | |
var analyser = null; | |
var theBuffer = null; | |
var detectorElem, | |
canvasElem, | |
pitchElem, | |
noteElem, | |
detuneElem, | |
detuneAmount; | |
window.onload = function() { | |
//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" ); | |
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 convertToMono( input ) { | |
var splitter = audioContext.createChannelSplitter(2); | |
var merger = audioContext.createChannelMerger(2); | |
input.connect( splitter ); | |
splitter.connect( merger, 0, 0 ); | |
splitter.connect( merger, 0, 1 ); | |
return merger; | |
} | |
function error() { | |
alert('Stream generation failed.'); | |
} | |
function getUserMedia(dictionary, callback) { | |
try { | |
navigator.webkitGetUserMedia(dictionary, callback, error); | |
} catch (e) { | |
alert('webkitGetUserMedia threw exception :' + e); | |
} | |
} | |
function gotStream(stream) { | |
// Create an AudioNode from the stream. | |
var mediaStreamSource = audioContext.createMediaStreamSource(stream); | |
// Connect it to the destination. | |
analyser = audioContext.createAnalyser(); | |
analyser.fftSize = 2048; | |
convertToMono( mediaStreamSource ).connect( analyser ); | |
updatePitch(); | |
} | |
function toggleLiveInput() { | |
getUserMedia({audio:true}, gotStream); | |
} | |
function togglePlayback() { | |
var now = audioContext.currentTime; | |
if (isPlaying) { | |
//stop playing and return | |
sourceNode.noteOff( now ); | |
sourceNode = null; | |
analyser = null; | |
isPlaying = false; | |
webkitCancelAnimationFrame( 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.noteOn( now ); | |
isPlaying = true; | |
isLiveInput = false; | |
updatePitch(); | |
return "stop"; | |
} | |
var rafID = null; | |
var tracks = null; | |
var buflen = 1024; | |
var buf = new Uint8Array( buflen ); | |
var MINVAL = 134; // 128 == zero. MINVAL is the "minimum detected signal" level. | |
function findNextPositiveZeroCrossing( start ) { | |
var i = Math.ceil( start ); | |
var last_zero = -1; | |
// advance until we're zero or negative | |
while (i<buflen && (buf[i] > 128 ) ) | |
i++; | |
if (i>=buflen) | |
return -1; | |
// advance until we're above MINVAL, keeping track of last zero. | |
while (i<buflen && ((t=buf[i]) < MINVAL )) { | |
if (t >= 128) { | |
if (last_zero == -1) | |
last_zero = i; | |
} else | |
last_zero = -1; | |
i++; | |
} | |
// we may have jumped over MINVAL in one sample. | |
if (last_zero == -1) | |
last_zero = i; | |
if (i==buflen) // We didn't find any more positive zero crossings | |
return -1; | |
// The first sample might be a zero. If so, return it. | |
if (last_zero == 0) | |
return 0; | |
// Otherwise, the zero might be between two values, so we need to scale it. | |
var t = ( 128 - buf[last_zero-1] ) / (buf[last_zero] - buf[last_zero-1]); | |
return last_zero+t; | |
} | |
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) ); | |
} | |
function updatePitch( time ) { | |
var cycles = new Array; | |
analyser.getByteTimeDomainData( buf ); | |
var i=0; | |
// find the first point | |
var last_zero = findNextPositiveZeroCrossing( 0 ); | |
var n=0; | |
// keep finding points, adding cycle lengths to array | |
while ( last_zero != -1) { | |
var next_zero = findNextPositiveZeroCrossing( last_zero + 1 ); | |
if (next_zero > -1) | |
cycles.push( next_zero - last_zero ); | |
last_zero = next_zero; | |
n++; | |
if (n>1000) | |
break; | |
} | |
// 1?: average the array | |
var num_cycles = cycles.length; | |
var sum = 0; | |
var pitch = 0; | |
for (var i=0; i<num_cycles; i++) { | |
sum += cycles[i]; | |
} | |
if (num_cycles) { | |
sum /= num_cycles; | |
pitch = audioContext.sampleRate/sum; | |
} | |
// confidence = num_cycles / num_possible_cycles = num_cycles / (audioContext.sampleRate/) | |
var confidence = (num_cycles ? ((num_cycles/(pitch * buflen / audioContext.sampleRate)) * 100) : 0); | |
/* | |
console.log( | |
"Cycles: " + num_cycles + | |
" - average length: " + sum + | |
" - pitch: " + pitch + "Hz " + | |
" - note: " + noteFromPitch( pitch ) + | |
" - confidence: " + confidence + "% " | |
); | |
*/ | |
// possible other approach to confidence: sort the array, take the median; go through the array and compute the average deviation | |
detectorElem.className = (confidence>50)?"confident":"vague"; | |
// TODO: Paint confidence meter on canvasElem here. | |
if (num_cycles == 0) { | |
pitchElem.innerText = "--"; | |
noteElem.innerText = "-"; | |
detuneElem.className = ""; | |
detuneAmount.innerText = "--"; | |
} else { | |
pitchElem.innerText = Math.floor( pitch ); | |
var note = noteFromPitch( pitch ); | |
noteElem.innerText = noteStrings[note%12]; | |
var detune = centsOffFromPitch( pitch, note ); | |
if (detune == 0 ) { | |
detuneElem.className = ""; | |
detuneAmount.innerText = "--"; | |
} else { | |
if (detune < 0) | |
detuneElem.className = "flat"; | |
else | |
detuneElem.className = "sharp"; | |
detuneAmount.innerText = Math.abs( detune ); | |
} | |
} | |
rafID = window.webkitRequestAnimationFrame( updatePitch ); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Forked from https://github.com/cwilso/PitchDetect