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