Skip to content

Instantly share code, notes, and snippets.

@llimllib
Last active September 30, 2024 15:29
Show Gist options
  • Save llimllib/7c461b508b1f10f6181fdc4a89e645ba to your computer and use it in GitHub Desktop.
Save llimllib/7c461b508b1f10f6181fdc4a89e645ba to your computer and use it in GitHub Desktop.
attempt at a basic guitar tuner
<!doctype html>
<html>
<head>
<title>Frequency Domain Visualization</title>
<style>
canvas {
border: 1px solid black;
}
</style>
</head>
<body>
<canvas id="spectro" width="800" height="400"></canvas>
<canvas id="harmonic" width="800" height="400"></canvas>
<script src="tuner.js"></script>
</body>
</html>
/** Calculate the dominant pitch
* @param{Uint8Array} buffer
* @param{number} sampleRate
*/
function calculateDominantPitch(data, sampleRate) {
const maxHarmonics = 10;
const downsampledLength = Math.ceil(data.length / maxHarmonics);
const maxFrequency = sampleRate / 2;
const pitchProducts = new Array(downsampledLength).fill(1);
for (let i = 1; i <= maxHarmonics; i++) {
// const harmonicWeight = 1 / (i + 1);
for (let j = 0; j < downsampledLength; j++) {
const binIndex = Math.round(j * maxHarmonics + i);
if (binIndex < data.length) {
//pitchProducts[j] *= Math.pow(data[binIndex] / 255, harmonicWeight);
pitchProducts[j] *= (hanningData[binIndex] / 255) * i;
}
}
}
let maxProduct = 0;
let maxIndex = 0;
for (let i = 0; i < downsampledLength; i++) {
if (pitchProducts[i] > maxProduct) {
maxProduct = pitchProducts[i];
maxIndex = i;
}
}
const canvas = document.getElementById("harmonic");
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
const barWidth = canvas.width / pitchProducts.length;
let x = 0;
for (let i = 0; i < pitchProducts.length; i++) {
const barHeight = (pitchProducts[i] / 255) * canvas.height;
ctx.fillStyle = `rgb(${pitchProducts[i]} 50 50)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
x += barWidth;
}
return (maxIndex * maxFrequency) / downsampledLength;
}
/** draw a spectrogram
* @param{CanvasRenderingContext2D} ctx
* @param{number} w
* @param{number} h
* @param{number} sampleRate
* @param{AnalyserNode} analyser
* @param{Uint8Array} buffer
*/
function draw(ctx, w, h, sampleRate, analyser, buffer) {
ctx.clearRect(0, 0, w, h);
analyser.getByteFrequencyData(buffer);
const dominantPitch = calculateDominantPitch(buffer, sampleRate);
ctx.fillStyle = "black";
ctx.font = "20px Arial";
ctx.fillText(`Dominant Pitch: ${dominantPitch.toFixed(2)} Hz`, 10, 30);
const barWidth = w / buffer.length;
let x = 0;
for (let i = 0; i < buffer.length; i++) {
const barHeight = (buffer[i] / 255) * h;
ctx.fillStyle = `rgb(${buffer[i]} ${buffer[i]} 50)`;
ctx.fillRect(x, h - barHeight, barWidth, barHeight);
x += barWidth;
}
requestAnimationFrame(() => draw(ctx, w, h, sampleRate, analyser, buffer));
}
async function main() {
const canvas = document.getElementById("spectro");
const ctx = canvas.getContext("2d");
const audioCtx = new AudioContext();
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const source = audioCtx.createMediaStreamSource(stream);
// https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/createAnalyser
const analyser = audioCtx.createAnalyser();
// https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/fftSize
analyser.fftSize = 32768;
source.connect(analyser);
const buffer = new Uint8Array(analyser.frequencyBinCount);
draw(ctx, canvas.width, canvas.height, audioCtx.sampleRate, analyser, buffer);
}
main()
.then()
.catch((e) => console.error(e));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment