Initial voice analyzer app

This commit is contained in:
Hannah 2024-10-30 16:12:48 +00:00
commit 30df8ab584
2 changed files with 156 additions and 0 deletions

29
index.html Normal file
View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Voice Pitch Analyzer</title>
<script src="https://nocdnbs.private.coffee/ajax/libs/p5.js/1.4.0/p5.js"></script>
<script src="https://nocdnbs.private.coffee/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script>
<style>
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f4f4f9;
font-family: Arial, sans-serif;
}
</style>
</head>
<body>
<h2>Voice Pitch Analyzer</h2>
<script src="./script.js"></script>
</body>
</html>

127
script.js Normal file
View file

@ -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, ' '));
}