Skip to content

Instantly share code, notes, and snippets.

@amiika
Last active January 5, 2017 01:06
Show Gist options
  • Save amiika/5271149 to your computer and use it in GitHub Desktop.
Save amiika/5271149 to your computer and use it in GitHub Desktop.
Pitch detection
<!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 &#9837;</span><span id="sharp">cents &#9839;</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>
@amiika
Copy link
Author

amiika commented Mar 29, 2013

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment