Initial voice analyzer app
This commit is contained in:
commit
30df8ab584
2 changed files with 156 additions and 0 deletions
29
index.html
Normal file
29
index.html
Normal 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
127
script.js
Normal 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, ' '));
|
||||
}
|
Loading…
Reference in a new issue