127 lines
No EOL
3 KiB
JavaScript
127 lines
No EOL
3 KiB
JavaScript
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, ' '));
|
|
} |