前言
当你在给乐器调音时、又或者在小提琴、二胡这样的无品格乐器上演奏但无法确定音准时,你可以借助电脑或者移动设备的麦克风采集当前演奏乐器的声音,并通过算法实时分析声音的基音音高以及响度,这可以更好地帮助我们把握乐器的音准。
开始
在浏览器获取麦克风权限
在这里我们使用 Web Api 中的 getUserMedia 方法来获取设备麦克风。
async function getMicroPhoneDevice() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
console.log("浏览器不支持音频输入");
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const audioContext = new window.AudioContext();
const mediaStreamSource = audioContext.createMediaStreamSource(stream);
const processor = audioContext.createScriptProcessor(4096, 1, 1);
mediaStreamSource.connect(processor);
processor.connect(audioContext.destination);
processor.onaudioprocess = (e) => {
let inputBuffer = e.inputBuffer.getChannelData(0); // 读取第一个声道的数据
console.log(inputBuffer);
};
} catch (error) {
console.error("音频解析失败,请检查麦克风或重新尝试!", error);
}
}
这里的 inputBuffer
就是麦克风实时获取的音频数据,格式为 Buffer 数组。
处理获取的音频数据
现在我们得到了原始的音频数据,如果想要知道这段音频的频率,我们还需要使用自相关算法得出音频的基音以及周期。
1.首先使用 RMS 计算信号强度
let rms = 0;
let SIZE = inputBuffer.length;
for (let i = 0; i < SIZE; i++) {
let val = inputBuffer[i];
rms += val * val;
}
rms = Math.sqrt(rms / SIZE);
if (rms < 0.01) {
console.log("信号强度不足");
}
2.筛除噪声
let startIndex = 0,
endIndex = SIZE - 1,
threshold = 0.2;
for (let i = 0; i < SIZE / 2; i++) {
if (Math.abs(inputBuffer[i]) < threshold) {
startIndex = i;
break;
}
}
for (let i = 1; i < SIZE / 2; i++) {
if (Math.abs(inputBuffer[SIZE - i]) < threshold) {
endIndex = SIZE - i;
break;
}
}
inputBuffer = inputBuffer.slice(startIndex, endIndex);
SIZE = inputBuffer.length;
3.自相关计算(Autocorrelation Function, ACF)
let autocorrelation = new Array(SIZE).fill(0);
for (let i = 0; i < SIZE; i++) {
for (let j = 0; j < SIZE - i; j++) {
autocorrelation[i] += inputBuffer[j] * inputBuffer[j + i];
}
}
4.峰值检测
let peakIndex = 0;
while (autocorrelation[peakIndex] > autocorrelation[peakIndex + 1]) peakIndex++;
let maxCorrelationValue = -1,
maxCorrelationPosition = -1;
for (let i = peakIndex; i < SIZE; i++) {
if (autocorrelation[i] > maxCorrelationValue) {
maxCorrelationValue = autocorrelation[i];
maxCorrelationPosition = i;
}
}
let period = maxCorrelationPosition;
5.二次插值提高精度
let prevValue = autocorrelation[period - 1],
currentValue = autocorrelation[period],
nextValue = autocorrelation[period + 1];
let quadraticA = (prevValue + nextValue - 2 * currentValue) / 2;
let quadraticB = (nextValue - prevValue) / 2;
if (quadraticA) period = period - quadraticB / (2 * quadraticA);
6.频率计算
频率计算的公式为:f = 采样率 / 周期
let sampleRate = audioContext.sampleRate;
let frequency = sampleRate / period;
完整代码
async function getMicroPhoneDevice() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
console.log("浏览器不支持音频输入");
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const audioContext = new window.AudioContext();
const mediaStreamSource = audioContext.createMediaStreamSource(stream);
const processor = audioContext.createScriptProcessor(4096, 1, 1);
mediaStreamSource.connect(processor);
processor.connect(audioContext.destination);
processor.onaudioprocess = (e) => {
let inputBuffer = e.inputBuffer.getChannelData(0); // 读取第一个声道的数据
console.log(`音频频率为:${getFrequency(inputBuffer, audioContext.sampleRate)}`);
};
} catch (error) {
console.error("音频解析失败,请检查麦克风或重新尝试!", error);
}
}
function getFrequency(inputBuffer, sampleRate) {
let rms = 0;
let SIZE = inputBuffer.length;
for (let i = 0; i < SIZE; i++) {
let val = inputBuffer[i];
rms += val * val;
}
rms = Math.sqrt(rms / SIZE);
if (rms < 0.01) {
console.log("信号强度不足");
}
let startIndex = 0,
endIndex = SIZE - 1,
threshold = 0.2;
for (let i = 0; i < SIZE / 2; i++) {
if (Math.abs(inputBuffer[i]) < threshold) {
startIndex = i;
break;
}
}
for (let i = 1; i < SIZE / 2; i++) {
if (Math.abs(inputBuffer[SIZE - i]) < threshold) {
endIndex = SIZE - i;
break;
}
}
inputBuffer = inputBuffer.slice(startIndex, endIndex);
SIZE = inputBuffer.length;
let autocorrelation = new Array(SIZE).fill(0);
for (let i = 0; i < SIZE; i++) {
for (let j = 0; j < SIZE - i; j++) {
autocorrelation[i] += inputBuffer[j] * inputBuffer[j + i];
}
}
let peakIndex = 0;
while (autocorrelation[peakIndex] > autocorrelation[peakIndex + 1]) peakIndex++;
let maxCorrelationValue = -1,
maxCorrelationPosition = -1;
for (let i = peakIndex; i < SIZE; i++) {
if (autocorrelation[i] > maxCorrelationValue) {
maxCorrelationValue = autocorrelation[i];
maxCorrelationPosition = i;
}
}
let period = maxCorrelationPosition;
let prevValue = autocorrelation[period - 1],
currentValue = autocorrelation[period],
nextValue = autocorrelation[period + 1];
let quadraticA = (prevValue + nextValue - 2 * currentValue) / 2;
let quadraticB = (nextValue - prevValue) / 2;
if (quadraticA) period = period - quadraticB / (2 * quadraticA);
if (period === 0) return null;
let frequency = sampleRate / period;
return frequency;
}