From 30df8ab584182fd14d88cb7984a02b8157f7e686 Mon Sep 17 00:00:00 2001 From: Hannah Date: Wed, 30 Oct 2024 16:12:48 +0000 Subject: [PATCH] Initial voice analyzer app --- index.html | 29 ++++++++++++ script.js | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 index.html create mode 100644 script.js diff --git a/index.html b/index.html new file mode 100644 index 0000000..28b6cbf --- /dev/null +++ b/index.html @@ -0,0 +1,29 @@ + + + + + + Voice Pitch Analyzer + + + + + + +

Voice Pitch Analyzer

+ + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..ebbb0eb --- /dev/null +++ b/script.js @@ -0,0 +1,127 @@ +let mic, fft; +let pitchHistory = []; +const minPitch = 50; +const maxPitch = parseFloat(getUrlParameter('cutoff')) || 300; + +function setup() { + createCanvas(800, 400); + mic = new p5.AudioIn(); + mic.start(); + fft = new p5.FFT(0, 2048); + fft.setInput(mic); + textSize(16); +} + +function draw() { + background(240); + + drawPitchRanges(); + + let timeDomain = fft.waveform(2048, 'float32'); + let frequency = autoCorrelate(timeDomain); + + if (frequency > minPitch && frequency < maxPitch) { + pitchHistory.push(frequency); + + if (pitchHistory.length > width) { + pitchHistory.splice(0, 1); + } + } + + drawPitchPlot(); +} + +function drawPitchRanges() { + noStroke(); + fill(135, 206, 250, 100); + rect(0, frequencyToY(180), width, frequencyToY(85) - frequencyToY(180)); // "Male" range + fill(255, 182, 193, 100); + rect(0, frequencyToY(255), width, frequencyToY(165) - frequencyToY(255)); // "Female" range + + stroke(0); + fill(0); + textAlign(LEFT); + text('Typical "Male" Range (85-180 Hz)', 10, frequencyToY(85) - 5); + text('Typical "Female" Range (165-255 Hz)', 10, frequencyToY(165) - 5); +} + +function frequencyToY(freq) { + return map(freq, minPitch, maxPitch, height, 0); +} + +function drawPitchPlot() { + noFill(); + stroke(50); + beginShape(); + for (let i = 0; i < pitchHistory.length; i++) { + vertex(i, frequencyToY(pitchHistory[i])); + } + endShape(); +} + +function autoCorrelate(buf) { + let size = buf.length; + let rms = 0; + + for (let i = 0; i < size; i++) { + let val = buf[i]; + rms += val * val; + } + rms = Math.sqrt(rms / size); + + if (rms < 0.01) // Likely silence/noise + return -1; + + let r1 = 0, r2 = size - 1; + for (let i = 0; i < size / 2; i++) { + if (Math.abs(buf[i]) < 0.2) { + r1 = i; + break; + } + } + for (let i = 1; i < size / 2; i++) { + if (Math.abs(buf[size - i]) < 0.2) { + r2 = size - i; + break; + } + } + + buf = buf.slice(r1, r2); + size = buf.length; + + let c = new Array(size).fill(0); + for (let i = 0; i < size; i++) { + for (let j = 0; j < size - i; j++) { + c[i] = c[i] + buf[j] * buf[j + i]; + } + } + + let d = 0; + while (c[d] > c[d + 1]) d++; + let maxval = -1, maxpos = -1; + for (let i = d; i < size; i++) { + if (c[i] > maxval) { + maxval = c[i]; + maxpos = i; + } + } + + let T0 = maxpos; + let x1 = c[T0 - 1], x2 = c[T0], x3 = c[T0 + 1]; + let a = (x1 + x3 - 2 * x2) / 2; + let b = (x3 - x1) / 2; + + if (a) T0 = T0 - b / (2 * a); + + let sampleRate = getAudioContext().sampleRate; + return sampleRate / T0; +} + +function getUrlParameter(name) { + name = name.replace(/[\[\]]/g, '\\$&'); + let regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); + let results = regex.exec(window.location.href); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); +} \ No newline at end of file