{"size": 15574, "ext": "js", "max_stars_repo_path": "js/pitchdetect.js", "max_stars_repo_name": "VictorStanoev/LearnKaval", "max_stars_repo_stars_event_min_datetime": "2021-03-19T08:38:42.000Z", "max_stars_repo_stars_event_max_datetime": "2021-03-19T08:38:42.000Z", "max_issues_repo_path": "js/pitchdetect.js", "max_issues_repo_name": "VictorStanoev/LearnKaval", "max_issues_repo_issues_event_min_datetime": null, "max_issues_repo_issues_event_max_datetime": null, "max_forks_repo_path": "js/pitchdetect.js", "max_forks_repo_name": "VictorStanoev/LearnKaval", "max_forks_repo_forks_event_min_datetime": null, "max_forks_repo_forks_event_max_datetime": null}
{"max_stars_count": 1, "max_issues_count": null, "max_forks_count": null, "avg_line_length": 27.5159010601, "max_line_length": 115, "alphanum_fraction": 0.6921150636}
/*
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 count = 0;//How often to visualize signal
var count_to = 3;//How long to wait before next signal visualized
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;





//https://raw.githubusercontent.com/ytung/YaleHack/master/docs/whistling3.ogg
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", "https://gymrealmmanager.blob.core.windows.net/tests/LearnKaval/music/TheEmigrant.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() {
	const btns = document.getElementsByTagName('button');
	const oscilBtn = btns[2];

	if (isPlaying) {
		//stop playing and return
		sourceNode.stop(0);
		//audioContext.suspend();
		sourceNode = null;
		analyser = null;
		isPlaying = false;
		if (!window.cancelAnimationFrame)
			window.cancelAnimationFrame = window.webkitCancelAnimationFrame;
		window.cancelAnimationFrame(rafID);

		//Enable all buttons
		Array.from(btns).filter(x => x.className = 'button');
		Array.from(btns).filter(x => x.disabled = false);

		oscilBtn.innerHTML = "start oscillator";
		return;
	}

	audioContext.resume().then(() => {
		console.log('Playback resumed successfully');
	});

	sourceNode = audioContext.createOscillator();

	analyser = audioContext.createAnalyser();
	analyser.fftSize = 2048;
	sourceNode.connect(analyser);
	analyser.connect(audioContext.destination);
	sourceNode.start(0);
	isPlaying = true;
	isLiveInput = false;
	updatePitch();

	//Disable all other buttons
	btns[0].disabled = true
	btns[0].className = 'button blocked'
	btns[1].disabled = true
	btns[1].className = 'button blocked'

	oscilBtn.innerHTML = "stop oscillator";

}

function toggleLiveInput() {
	const btns = document.getElementsByTagName('button');
	const liveInpBtn = btns[1];

	if (isPlaying) {
		//stop playing and return
		//sourceNode.stop(0);
		audioContext.suspend()
		sourceNode = null;
		analyser = null;
		isPlaying = false;
		if (!window.cancelAnimationFrame)
			window.cancelAnimationFrame = window.webkitCancelAnimationFrame;
		window.cancelAnimationFrame(rafID);

		//Enable all buttons
		Array.from(btns).filter(x => x.disabled = false)
		Array.from(btns).filter(x => x.className = 'button')
		return liveInpBtn.innerHTML = 'start live input';
	}

	audioContext.resume().then(() => {
		console.log('Playback resumed successfully');
	});

	isPlaying = true;

	getUserMedia(
		{
			"audio": {
				"mandatory": {
					"googEchoCancellation": "false",
					"googAutoGainControl": "false",
					"googNoiseSuppression": "false",
					"googHighpassFilter": "false"
				},
				"optional": []
			},
		}, gotStream);

	//Disable all other buttons
	btns[0].disabled = true
	btns[0].className = 'button blocked'
	btns[2].disabled = true
	btns[2].className = 'button blocked'

	liveInpBtn.innerHTML = 'stop live input';
}

function togglePlayback() {
	const btns = document.getElementsByTagName('button');
	const demoBtn = btns[0];

	if (isPlaying) {
		//stop playing and return
		sourceNode.stop(0);
		audioContext.suspend();
		sourceNode = null;
		analyser = null;
		isPlaying = false;
		if (!window.cancelAnimationFrame)
			window.cancelAnimationFrame = window.webkitCancelAnimationFrame;
		window.cancelAnimationFrame(rafID);

		demoBtn.innerHTML = 'start demo audio';

		//Enable all buttons
		Array.from(btns).filter(x => x.disabled = false)
		Array.from(btns).filter(x => x.className = 'button')
		return
	}

	//Disable other buttons
	btns[1].disabled = true
	btns[1].className = 'button blocked'
	btns[2].disabled = true
	btns[2].className = 'button blocked'

	audioContext.resume().then(() => {
		console.log('Playback resumed successfully');
	});

	sourceNode = audioContext.createBufferSource();
	sourceNode.buffer = theBuffer;
	sourceNode.loop = false;

	analyser = audioContext.createAnalyser();
	analyser.fftSize = 2048;
	sourceNode.connect(analyser);
	analyser.connect(audioContext.destination);
	sourceNode.start(0);
	isPlaying = true;
	isLiveInput = false;
	updatePitch();

	return demoBtn.innerHTML = "stop demo audio";
}

var rafID = null;
var tracks = null;
var buflen = 2048;//1024;
var buf = new Float32Array(buflen);

let compareNote;

var noteStrings = [
	"C - &#1044&#1054",
	"C# - &#1044&#1054#",
	"D - &#1056&#1045",
	"D# - &#1056&#1045#",
	"E - &#1052&#1048",
	"F - &#1060&#1040",
	"F# - &#1060&#1040#",
	"G - &#1057&#1054&#1051",
	"G# - &#1057&#1054&#1051#",
	"A - &#1051&#1040",
	"A# - &#1051&#1040#",
	"B - &#1057&#1048"
];

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<SIZE;i++)
		rms += buf[i]*buf[i];
	rms = Math.sqrt(rms/SIZE);

	for (var offset = MIN_SAMPLES; offset <= MAX_SAMPLES; offset++) {
		var correlation = 0;

		for (var i=0; i<SIZE; i++) {
			correlation += Math.abs(buf[i]-buf[i+offset]);
		}
		correlation = 1 - (correlation/SIZE);
		if (correlation > 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; i < SIZE; i++) {
		var val = buf[i];
		rms += val * val;
	}
	rms = Math.sqrt(rms / SIZE);
	if (rms < 0.01) // not enough signal
		return -1;

	var lastCorrelation = 1;
	for (var offset = MIN_SAMPLES; offset < MAX_SAMPLES; offset++) {
		var correlation = 0;

		for (var i = 0; i < MAX_SAMPLES; i++) {
			correlation += Math.abs((buf[i]) - (buf[i + offset]));
		}
		correlation = 1 - (correlation / MAX_SAMPLES);
		correlations[offset] = correlation; // store it, for the tweaking we need to do below.
		if ((correlation > GOOD_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 startTime, endTime;
var startEmptyTime, endEmptyTime;

function start() {
	startTime = performance.now();
};

function end() {
	endTime = performance.now();
	let timeDiff = endTime - startTime; //in ms 

	if (timeDiff != undefined) {
		return timeDiff;
	}
}

function startEmpty() {
	startEmptyTime = performance.now();
};

function endEmpty() {
	endEmptyTime = performance.now();
	let timeDiff = endEmptyTime - startEmptyTime; //in ms 

	if (timeDiff != undefined) {
		console.log('0 --> ' + timeDiff.toFixed());
	}
}



function updatePitch(time) {

	var cycles = new Array;
	analyser.getFloatTimeDomainData(buf);
	var ac = autoCorrelate(buf, audioContext.sampleRate);
	// TODO: Paint confidence meter on canvasElem here.

	if (count > count_to) {
		count = 0;
		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();
		}
	}
	count++;

	//image element
	const boardDivElem = document.getElementById('noteBoard');
	const noteFiles = Array.from(boardDivElem.children);

	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);
		var register = get_register(note);// note;//Math.floor((note-20)/10);//.toFixed();
		const noteValue = note % 12
		const selectedNote = noteStrings[noteValue];
		noteElem.innerHTML = selectedNote + " -> " + register.toString();

		//Take only note simvol from array
		const noteSimvol = selectedNote.substring(0, 2).trim();
		//console.log(noteSimvol, '-->', register)



		if (noteSimvol != compareNote) {

			if (end() > 50) {
				let timePlayed = end();
				console.log(compareNote, '-->', register, '-->', timePlayed.toFixed(0));
			}
			compareNote = noteSimvol
			start();
		}


		//Make visible the note file as per note and register
		noteFiles.filter(x => x.dataset.reg == register && x.dataset.note == noteSimvol
			? x.style.visibility = 'visible' : x.style.visibility = 'hidden');


		showEmptyFlute();

		//show empy flute picture if no note picture is visible
		function showEmptyFlute() {
			if (noteFiles.every(x => x.style.visibility == 'hidden')) {
				noteFiles.filter(x => x.dataset.note == 'note')[0].style.visibility = 'visible'
			}

		}

		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);
}

function get_register(note) {
	var register = 0;
	if (note < 24)
		register = 0;
	if (note >= 24 & note < 36)
		register = 1;
	if (note >= 36 & note < 48)
		register = 2;
	if (note >= 48 & note < 60)
		register = 3;
	if (note >= 60 & note < 72)
		register = 4;
	if (note >= 72 & note < 84)
		register = 5;
	if (note >= 84 & note < 96)
		register = 6;
	if (note >= 108 & note < 120)
		register = 7;
	if (note >= 120 & note < 132)
		register = 8;
	//register = note;
	return register;
}
