You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

118 lines
3.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

function startPitchDetection(callback) {
// Check for browser support
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
console.error("getUserMedia is not supported in this browser.");
return;
}
// Request access to the microphone
//navigator.mediaDevices.getUserMedia({ audio: true })
navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false
}
})
.then(stream => {
// Create an audio context and an analyser node
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const analyser = audioContext.createAnalyser();
analyser.fftSize = 2048; // Set FFT size; higher values offer more precision but more latency
// Connect the microphone stream to the analyser node
const source = audioContext.createMediaStreamSource(stream);
source.connect(analyser);
// Create a buffer to hold the time domain data
const bufferLength = analyser.fftSize;
const buffer = new Float32Array(bufferLength);
/**
* Autocorrelation algorithm to estimate pitch from the audio buffer.
* Returns the estimated pitch in Hz, or -1 if no pitch is detected.
*/
function autoCorrelate(buf, sampleRate) {
const SIZE = buf.length;
let rms = 0;
// Compute Root Mean Square (RMS) to check if there's enough signal
for (let i = 0; i < SIZE; i++) {
const val = buf[i];
rms += val * val;
}
rms = Math.sqrt(rms / SIZE);
if (rms < 0.01) // Signal too weak likely silence
return -1;
// Trim the buffer to remove noise at the beginning and end
let r1 = 0, r2 = SIZE - 1;
for (let i = 0; i < SIZE; i++) {
if (Math.abs(buf[i]) < 0.2) { r1 = i; break; }
}
for (let i = 1; i < SIZE; i++) {
if (Math.abs(buf[SIZE - i]) < 0.2) { r2 = SIZE - i; break; }
}
const trimmedBuffer = buf.slice(r1, r2);
const trimmedSize = trimmedBuffer.length;
// Calculate the autocorrelation of the trimmed buffer
const correlations = new Array(trimmedSize).fill(0);
for (let lag = 0; lag < trimmedSize; lag++) {
for (let i = 0; i < trimmedSize - lag; i++) {
correlations[lag] += trimmedBuffer[i] * trimmedBuffer[i + lag];
}
}
// Find the first dip in the autocorrelation skip lags before this point
let d = 0;
while (d < correlations.length - 1 && correlations[d] > correlations[d + 1]) {
d++;
}
// Search for the peak correlation after the dip
let maxVal = -1, maxPos = -1;
for (let i = d; i < correlations.length; i++) {
if (correlations[i] > maxVal) {
maxVal = correlations[i];
maxPos = i;
}
}
// Parabolic interpolation for a more accurate peak estimate
let T0 = maxPos;
if (T0 > 0 && T0 < correlations.length - 1) {
const x1 = correlations[T0 - 1];
const x2 = correlations[T0];
const x3 = correlations[T0 + 1];
const a = (x1 + x3 - 2 * x2) / 2;
const b = (x3 - x1) / 2;
if (a !== 0) {
T0 = T0 - b / (2 * a);
}
}
// Convert lag to frequency
const pitch = sampleRate / T0;
return pitch;
}
// Continuously update and detect pitch
function updatePitch() {
// Get the latest time-domain data
analyser.getFloatTimeDomainData(buffer);
// Estimate pitch using our autocorrelation function
const pitch = autoCorrelate(buffer, audioContext.sampleRate);
// Pass the detected pitch (in Hz) to the provided callback
callback(pitch);
// Continue the update loop
requestAnimationFrame(updatePitch);
}
updatePitch();
})
.catch(err => {
console.error("Error accessing the microphone: ", err);
});
}