我发现了一些旧的代码片段,它们不能一起使用...我需要新的思路。
这里有一些问题需要解决,其中一些需要更多关于应用程序的信息。随着答案的进展,这个任务的规模会变得明显。
目前为止,这里有两个问题:
需要创建一种类似吉他调音器的东西...
1. 如何检测吉他音符的基本音高并将该信息反馈给用户在浏览器中?
和
那就是识别声音频率并确定我实际上正在演奏哪个和弦。
2. 如何检测吉他演奏的和弦?
第二个问题绝对不是一个简单的问题,但我们会逐步解决它。这不是一个编程问题,而是一个DSP问题。
如果您希望在浏览器中检测音符的音高,则应将其拆分为几个子问题。从直觉上讲,我们有以下JavaScript浏览器问题:
这不是一个详尽的列表,但它应该构成整个问题的大部分。
没有最小可重现示例,因此以上内容都不能被假定。
基本实现将使用A. v. Knesebeck和U. Zölzer论文[1]中概述的自相关方法来表示单个基本频率(f0)的数字表示。
还有其他方法,其中混合和匹配滤波器和音高检测算法,我认为远远超出了合理答案的范围。
注意:Web Audio API在所有浏览器上实现并不相同。您应该检查每个主要浏览器,并在程序中进行适当的调整。以下内容在Google Chrome中进行了测试,因此在其他浏览器中可能会有所不同。
我们的页面应包括
一个更全面的界面可能会将以下操作分开:
但为了简洁起见,它们将被包装成一个单独的元素。这给我们提供了一个基本的HTML页面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pitch Detection</title>
</head>
<body>
<h1>Frequency (Hz)</h1>
<h2 id="frequency">0.0</h2>
<div>
<button onclick="startPitchDetection()">
Start Pitch Detection
</button>
</div>
</body>
</html>
我们稍微有点着急使用<button onclick="startPitchDetection()">
。我们将把操作包装在一个名为startPitchDetection
的函数中。
对于自相关音高检测方法,我们的变量调色板需要包括:
这样我们就有了类似以下的内容
let audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let microphoneStream = null;
let analyserNode = audioCtx.createAnalyser()
let audioData = new Float32Array(analyserNode.fftSize);;
let corrolatedSignal = new Float32Array(analyserNode.fftSize);;
let localMaxima = new Array(10);
const frequencyDisplayElement = document.querySelector('#frequency');
一些值被留空,因为在麦克风流被激活之前它们是未知的。在 let localMaxima = new Array(10);
中的 10
有点任意。这个数组将存储相关信号连续极大值之间的样本距离。
我们的 <button>
元素具有 startPitchDetection
的 onclick
函数,因此需要它。我们还需要:
然而,我们首先要做的是请求使用麦克风的权限。为了实现这一点,我们使用 navigator.mediaDevices.getUserMedia
, 它将返回一个 Promise。根据 MDN 文档所述,这给我们提供了以下内容:
navigator.mediaDevices.getUserMedia({audio: true})
.then((stream) => {
/* use the stream */
})
.catch((err) => {
/* handle the error */
});
太好了!现在我们可以开始将主要功能添加到then
函数中。
我们的事件顺序应该是:
除此之外,还需要在catch
方法中添加错误日志记录。
然后,所有这些都可以包装到startPitchDetection
函数中,得到类似以下的内容:
function startPitchDetection()
{
navigator.mediaDevices.getUserMedia ({audio: true})
.then((stream) =>
{
microphoneStream = audioCtx.createMediaStreamSource(stream);
microphoneStream.connect(analyserNode);
audioData = new Float32Array(analyserNode.fftSize);
corrolatedSignal = new Float32Array(analyserNode.fftSize);
setInterval(() => {
analyserNode.getFloatTimeDomainData(audioData);
let pitch = getAutocorrolatedPitch();
frequencyDisplayElement.innerHTML = `${pitch}`;
}, 300);
})
.catch((err) =>
{
console.log(err);
});
}
setInterval
的更新间隔为 300
是任意的。一些试验将决定哪个间隔对你最好。你甚至可能希望让用户控制这个,但这超出了本问题的范围。
下一步���实际定义 getAutocorrolatedPitch()
做什么,所以让我们分解一下自相关是什么。
自相关是将信号与其自身进行卷积的过程。任何时候,当结果从正变为负的变化率时,被定义为局部最大值。从相关信号的开始到第一个最大值之间的样本数应该是f0
的样本周期。我们可以继续寻找后续的极大值并取平均值,这应该会稍微提高准确性。有些频率没有整个样本周期,例如在44100
Hz的采样率下,440
Hz的周期为100.227
。通过单个最大值,我们技术上永远无法准确检测到这个440
Hz的频率,结果总是441
Hz(44100/100
)或436
Hz(44100/101
)。
对于我们的自相关函数,我们需要:
function getAutocorrolatedPitch()
{
// First: autocorrolate the signal
let maximaCount = 0;
for (let l = 0; l < analyserNode.fftSize; l++) {
corrolatedSignal[l] = 0;
for (let i = 0; i < analyserNode.fftSize - l; i++) {
corrolatedSignal[l] += audioData[i] * audioData[i + l];
}
if (l > 1) {
if ((corrolatedSignal[l - 2] - corrolatedSignal[l - 1]) < 0
&& (corrolatedSignal[l - 1] - corrolatedSignal[l]) > 0) {
localMaxima[maximaCount] = (l - 1);
maximaCount++;
if ((maximaCount >= localMaxima.length))
break;
}
}
}
// Second: find the average distance in samples between maxima
let maximaMean = localMaxima[0];
for (let i = 1; i < maximaCount; i++)
maximaMean += localMaxima[i] - localMaxima[i - 1];
maximaMean /= maximaCount;
return audioCtx.sampleRate / maximaMean;
}
一旦您实施了这个方案,您可能会发现有几个问题。
不稳定的结果是因为自相关本身并不是一个完美的解决方案。您需要尝试首先过滤信号并聚合其他方法。您还可以尝试限制信号或仅在信号高于某个阈值时分析信号。您还可以增加执行检测的速率并平均结果。
其次,显示方法受到限制。音乐家不会欣赏简单的数字结果,而是更喜欢某种图形反馈。同样,这超出了问题的范围。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pitch Detection</title>
</head>
<body>
<h1>Frequency (Hz)</h1>
<h2 id="frequency">0.0</h2>
<div>
<button onclick="startPitchDetection()">
Start Pitch Detection
</button>
</div>
<script>
let audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let microphoneStream = null;
let analyserNode = audioCtx.createAnalyser()
let audioData = new Float32Array(analyserNode.fftSize);;
let corrolatedSignal = new Float32Array(analyserNode.fftSize);;
let localMaxima = new Array(10);
const frequencyDisplayElement = document.querySelector('#frequency');
function startPitchDetection()
{
navigator.mediaDevices.getUserMedia ({audio: true})
.then((stream) =>
{
microphoneStream = audioCtx.createMediaStreamSource(stream);
microphoneStream.connect(analyserNode);
audioData = new Float32Array(analyserNode.fftSize);
corrolatedSignal = new Float32Array(analyserNode.fftSize);
setInterval(() => {
analyserNode.getFloatTimeDomainData(audioData);
let pitch = getAutocorrolatedPitch();
frequencyDisplayElement.innerHTML = `${pitch}`;
}, 300);
})
.catch((err) =>
{
console.log(err);
});
}
function getAutocorrolatedPitch()
{
// First: autocorrolate the signal
let maximaCount = 0;
for (let l = 0; l < analyserNode.fftSize; l++) {
corrolatedSignal[l] = 0;
for (let i = 0; i < analyserNode.fftSize - l; i++) {
corrolatedSignal[l] += audioData[i] * audioData[i + l];
}
if (l > 1) {
if ((corrolatedSignal[l - 2] - corrolatedSignal[l - 1]) < 0
&& (corrolatedSignal[l - 1] - corrolatedSignal[l]) > 0) {
localMaxima[maximaCount] = (l - 1);
maximaCount++;
if ((maximaCount >= localMaxima.length))
break;
}
}
}
// Second: find the average distance in samples between maxima
let maximaMean = localMaxima[0];
for (let i = 1; i < maximaCount; i++)
maximaMean += localMaxima[i] - localMaxima[i - 1];
maximaMean /= maximaCount;
return audioCtx.sampleRate / maximaMean;
}
</script>
</body>
</html>
目前为止,我认为我们都可以同意这个答案有点失控了。到目前为止,我们只涵盖了单一的音高检测方法。请参见Ref [2, 3, 4],获取一些关于多个f0
检测算法的建议。
本质上,这个问题将归结为检测所有的f0
,并将结果音符与和弦字典进行匹配。为此,您至少应该做一些工作。任何关于DSP的问题可能都应该指向https://dsp.stackexchange.com。您将会在有关音高检测算法的问题方面有很多选择。