跳到主要内容

在浏览器中实时获取声音频率

4 分钟阅读

前言

当你在给乐器调音时、又或者在小提琴、二胡这样的无品格乐器上演奏但无法确定音准时,你可以借助电脑或者移动设备的麦克风采集当前演奏乐器的声音,并通过算法实时分析声音的基音音高以及响度,这可以更好地帮助我们把握乐器的音准。

开始

在浏览器获取麦克风权限

在这里我们使用 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;
}
评论
0条评论

添加新评论

昵称
邮箱
网址