如何使用 JavaScript 从麦克风获取音频频率?

Mar*_*ros 8 javascript audio frontend frequency node.js

我需要创建一种类似吉他调音器的东西......它可以识别声音频率并确定我实际演奏的女巫和弦。它类似于我在网上找到的吉他调音器: https: //musicjungle.com.br/afinador-online 但由于 webpack 文件,我无法弄清楚它是如何工作的..我想让这个工具应用程序无后端..有人知道如何仅在前端执行此操作吗?

我创建了一些不能一起工作的旧代码..我需要新的想法

fdc*_*cpp 37

这里有很多问题需要解决,其中一些需要更多有关应用程序的信息。希望随着这个答案的进展,这项任务的巨大规模将变得显而易见。

\n

就目前情况而言,这里有两个问题:

\n
\n

需要创建一种类似吉他调音器的东西..

\n
\n

1. 如何检测吉他音符的基本音高并将该信息反馈给浏览器中的用户?

\n

\n
\n

这就是识别声音频率并确定我实际演奏的女巫和弦。

\n
\n

2. 如何检测吉他正在演奏哪个和弦?

\n

第二个问题绝对不是一个小问题,但我们将依次讨论它。这不是一个编程问题,而是一个DSP问题

\n

问题 1:浏览器中的音高检测

\n

分解

\n

如果您希望在浏览器中检测音符的音高,则需要分解几个子问题。从表面上看,我们有以下 JavaScript 浏览器问题:

\n\n

这不是一个详尽的列表,但它应该构成整个问题的大部分

\n

没有最小的、可重现的示例,因此不能假设上述情况。

\n

执行

\n

基本实现包括使用 A. v. Knesebeck 和 U. Z\xc3\xb6lzer 论文 [1] 中概述的自相关方法对单个基频 (f0) 进行数值表示。

\n

还有其他混合和匹配滤波和音高检测算法的方法,我认为这远远超出了合理答案的范围。

\n

注意: Web Audio API仍然没有在所有浏览器上得到同等的实现。您应该检查每个主要浏览器并在您的程序中进行调整。以下内容是在 Google Chrome 中进行测试的,因此您的情况可能(并且可能会)在其他浏览器中有所不同。

\n
超文本标记语言
\n

我们的页面应该包括

\n
    \n
  • 显示频率的元素
  • \n
  • 启动音高检测的元素
  • \n
\n

更全面的界面可能会拆分以下操作

\n
    \n
  • 请求麦克风许可
  • \n
  • 开始麦克风流
  • \n
  • 处理麦克风流
  • \n
\n

到单独的界面元素中,但为了简洁起见,它们将被包装到单个元素中。这给了我们一个基本的 HTML 页面

\n
<!DOCTYPE html>\n<html lang="en">\n<head>\n    <meta charset="UTF-8">\n    <title>Pitch Detection</title>\n</head>\n<body>\n<h1>Frequency (Hz)</h1>\n<h2 id="frequency">0.0</h2>\n<div>\n    <button onclick="startPitchDetection()">\n        Start Pitch Detection\n    </button>\n</div>\n</body>\n</html>\n
Run Code Online (Sandbox Code Playgroud)\n

我们有点操之过急了<button onclick="startPitchDetection()">。我们将把操作包装在一个名为的函数中startPitchDetection

\n
变量的调色板
\n

对于自相关音高检测方法,我们的变量列表需要包括:

\n
    \n
  • 音频上下文
  • \n
  • 麦克风流
  • \n
  • 分析器节点
  • \n
  • 音频数据数组
  • \n
  • 相关信号的数组
  • \n
  • 相关信号最大值的数组
  • \n
  • 对频率的 DOM 引用
  • \n
\n

给我们类似的东西

\n
let audioCtx = new (window.AudioContext || window.webkitAudioContext)();\nlet microphoneStream = null;\nlet analyserNode = audioCtx.createAnalyser()\nlet audioData = new Float32Array(analyserNode.fftSize);;\nlet corrolatedSignal = new Float32Array(analyserNode.fftSize);;\nlet localMaxima = new Array(10);\nconst frequencyDisplayElement = document.querySelector(\'#frequency\');\n
Run Code Online (Sandbox Code Playgroud)\n

留下一些值null,因为在激活麦克风流之前它们是不知道的。in有点随意10let localMaxima = new Array(10);该数组将存储关联信号的连续最大值之间的样本距离。

\n
主要脚本
\n

我们的<button>元素有一个onclick函数startPitchDetection,因此这是必需的。我们还需要

\n
    \n
  • 更新功能(用于更新显示)
  • \n
  • 返回音调的自相关函数
  • \n
\n

然而,我们要做的第一件事是请求使用麦克风的许可。为了实现这一点,我们使用navigator.mediaDevices.getUserMedia,它将返回一个Promise。对 MDN 文档中概述的内容进行修饰后,我们得到的内容大致如下

\n
navigator.mediaDevices.getUserMedia({audio: true})\n.then((stream) => {\n  /* use the stream */\n})\n.catch((err) => {\n  /* handle the error */\n});\n
Run Code Online (Sandbox Code Playgroud)\n

伟大的!现在我们可以开始将主要功能添加到该then函数中。

\n

我们的事件顺序应该是

\n
    \n
  • 启动麦克风流
  • \n
  • 将麦克风流连接到分析器节点
  • \n
  • 将定时回调设置为\n
      \n
    • 从分析器节点获取最新的时域音频数据
    • \n
    • 得到自相关推导的音调估计
    • \n
    • 使用值更新 html 元素
    • \n
    \n
  • \n
\n

最重要的是,添加该方法的错误日志catch

\n

然后可以将它们全部包装到startPitchDetection函数中,给出如下内容:

\n
function startPitchDetection()\n{\n    navigator.mediaDevices.getUserMedia ({audio: true})\n        .then((stream) =>\n        {\n            microphoneStream = audioCtx.createMediaStreamSource(stream);\n            microphoneStream.connect(analyserNode);\n\n            audioData = new Float32Array(analyserNode.fftSize);\n            corrolatedSignal = new Float32Array(analyserNode.fftSize);\n\n            setInterval(() => {\n                analyserNode.getFloatTimeDomainData(audioData);\n\n                let pitch = getAutocorrolatedPitch();\n\n                frequencyDisplayElement.innerHTML = `${pitch}`;\n            }, 300);\n        })\n        .catch((err) =>\n        {\n            console.log(err);\n        });\n}\n
Run Code Online (Sandbox Code Playgroud)\n

setIntervalof的更新间隔300是任意的。一些实验将决定哪种间隔最适合您。您甚至可能希望让用户对此进行控制,但这超出了本问题的范围。

\n

下一步是实际定义什么是getAutocorrolatedPitch()自相关,所以让我们实际分解什么是自相关。

\n

自相关是信号与其自身卷积的过程。任何时候结果从正变化率变为负变化率都被定义为局部最大值。相关信号开始到第一个最大值之间的样本数应为 的样本周期f0。我们可以继续寻找后续的最大值并取平均值,这应该会稍微提高准确性。某些频率没有完整样本的周期,例如440采样率为 Hz 的44100Hz 的周期为100.227。从技术上讲,我们永远无法440通过取单个最大值来准确检测 Hz 的频率,结果始终是441Hz ( 44100/100) 或436Hz ( 44100/101)。

\n

对于我们的自相关函数,我们需要

\n
    \n
  • 跟踪已检测到的最大值
  • \n
  • 最大值之间的平均距离
  • \n
\n

我们的函数应该首先执行自相关,找到局部最大值的样本位置,然后计算这些最大值之间的平均距离。这给出了一个如下所示的函数:

\n
function getAutocorrolatedPitch()\n{\n    // First: autocorrolate the signal\n\n    let maximaCount = 0;\n\n    for (let l = 0; l < analyserNode.fftSize; l++) {\n        corrolatedSignal[l] = 0;\n        for (let i = 0; i < analyserNode.fftSize - l; i++) {\n            corrolatedSignal[l] += audioData[i] * audioData[i + l];\n        }\n        if (l > 1) {\n            if ((corrolatedSignal[l - 2] - corrolatedSignal[l - 1]) < 0\n                && (corrolatedSignal[l - 1] - corrolatedSignal[l]) > 0) {\n                localMaxima[maximaCount] = (l - 1);\n                maximaCount++;\n                if ((maximaCount >= localMaxima.length))\n                    break;\n            }\n        }\n    }\n\n    // Second: find the average distance in samples between maxima\n\n    let maximaMean = localMaxima[0];\n\n    for (let i = 1; i < maximaCount; i++)\n        maximaMean += localMaxima[i] - localMaxima[i - 1];\n\n    maximaMean /= maximaCount;\n\n    return audioCtx.sampleRate / maximaMean;\n}\n
Run Code Online (Sandbox Code Playgroud)\n
问题
\n

一旦你实现了这一点,你可能会发现实际上存在一些问题。

\n
    \n
  • 频率结果有点不稳定
  • \n
  • 显示方法对于调整目的来说并不直观
  • \n
\n

不稳定的结果是由于自相关本身并不是一个完美的解决方案。您需要首先尝试过滤信号并聚合其他方法。您还可以尝试限制信号或仅在信号高于特定阈值时分析信号。您还可以提高执行检测的速率并对结果进行平均。

\n

其次,展示方式受到限制。音乐家不会欣赏简单的数字结果,相反,某种图形反馈会更直观。同样,这超出了问题的范围。

\n
完整页面和脚本
\n
<!DOCTYPE html>\n<html lang="en">\n<head>\n    <meta charset="UTF-8">\n    <title>Pitch Detection</title>\n</head>\n<body>\n<h1>Frequency (Hz)</h1>\n<h2 id="frequency">0.0</h2>\n<div>\n    <button onclick="startPitchDetection()">\n        Start Pitch Detection\n    </button>\n</div>\n<script>\n    let audioCtx = new (window.AudioContext || window.webkitAudioContext)();\n    let microphoneStream = null;\n    let analyserNode = audioCtx.createAnalyser()\n    let audioData = new Float32Array(analyserNode.fftSize);;\n    let corrolatedSignal = new Float32Array(analyserNode.fftSize);;\n    let localMaxima = new Array(10);\n    const frequencyDisplayElement = document.querySelector(\'#frequency\');\n\n    function startPitchDetection()\n    {\n        navigator.mediaDevices.getUserMedia ({audio: true})\n            .then((stream) =>\n            {\n                microphoneStream = audioCtx.createMediaStreamSource(stream);\n                microphoneStream.connect(analyserNode);\n\n                audioData = new Float32Array(analyserNode.fftSize);\n                corrolatedSignal = new Float32Array(analyserNode.fftSize);\n\n                setInterval(() => {\n                    analyserNode.getFloatTimeDomainData(audioData);\n\n                    let pitch = getAutocorrolatedPitch();\n\n                    frequencyDisplayElement.innerHTML = `${pitch}`;\n                }, 300);\n            })\n            .catch((err) =>\n            {\n                console.log(err);\n            });\n    }\n\n    function getAutocorrolatedPitch()\n    {\n        // First: autocorrolate the signal\n\n        let maximaCount = 0;\n\n        for (let l = 0; l < analyserNode.fftSize; l++) {\n            corrolatedSignal[l] = 0;\n            for (let i = 0; i < analyserNode.fftSize - l; i++) {\n                corrolatedSignal[l] += audioData[i] * audioData[i + l];\n            }\n            if (l > 1) {\n                if ((corrolatedSignal[l - 2] - corrolatedSignal[l - 1]) < 0\n                    && (corrolatedSignal[l - 1] - corrolatedSignal[l]) > 0) {\n                    localMaxima[maximaCount] = (l - 1);\n                    maximaCount++;\n                    if ((maximaCount >= localMaxima.length))\n                        break;\n                }\n            }\n        }\n\n        // Second: find the average distance in samples between maxima\n\n        let maximaMean = localMaxima[0];\n\n        for (let i = 1; i < maximaCount; i++)\n            maximaMean += localMaxima[i] - localMaxima[i - 1];\n\n        maximaMean /= maximaCount;\n\n        return audioCtx.sampleRate / maximaMean;\n    }\n</script>\n</body>\n</html>\n
Run Code Online (Sandbox Code Playgroud)\n

问题2:检测多个音符

\n

在这一点上,我想我们都同意这个答案已经有点失控了。到目前为止,我们刚刚介绍了一种音高检测方法。有关多重检测算法的一些建议,请参阅参考文献 [2,3,4] f0

\n

本质上,这个问题归结为检测所有f0s 并根据和弦字典查找生成的音符。为此,您至少应该做一些工作。有关 DSP 的任何问题都应该指向https://dsp.stackexchange.com关于音高检测算法的问题,您将被宠坏了

\n

参考

\n
    \n
  1. A. v. Knesebeck 和 U. Z\xc3\xb6lzer,“实时吉他效果音高跟踪器的比较”,第 13 届国际数字音频效果会议 (DAFx-10) 会议记录,奥地利格拉茨,9 月 6 日-10 , 2010 年。
  2. \n
  3. AP Klapuri,“一种感知驱动的多 F0 估计方法”,IEEE 音频和声学信号处理应用研讨会,2005 年,2005 年,第 291-294 页,doi:10.1109/ASPAA.2005.1540227。
  4. \n
  5. AP Klapuri,“基于谐波和频谱平滑度的多重基频估计”,载于IEEE 语音和音频处理汇刊,第 1 卷。11、没有。6,第 804-816 页,2003 年 11 月,doi:10.1109/TSA.2003.815516。
  6. \n
  7. AP Klapuri,“通过频谱平滑原理进行多音高估计和声音分离”,2001 年 IEEE 国际声学、语音和信号处理会议。会议记录(目录号 01CH37221),2001 年,第 3381-3384 页,第 5 卷,doi:10.1109/ICASSP.2001.940384。
  8. \n
  9. AM Stark 和 MD Plumbley,“现场表演的实时和弦识别”,2009 年国际计算机音乐会议 (ICMC 2009) 会议记录,加拿大蒙特利尔,2009 年 8 月 16-21 日。
  10. \n
  11. 阿兰·德·谢维恩\xc3\xa9、川原英树;YIN,语音和音乐的基频估计器。J.阿库斯特。苏克。是。2002 年 4 月 1 日;111(4):1917\xe2\x80\x931930。
  12. \n
\n

  • “在这一点上,我想我们都同意这个答案已经有点失控了。” 这是我最近读过的最有趣的书之一! (8认同)
  • 非常好的答案!我怀疑强大的和弦识别难度要大一个数量级,特别是与现实世界的调音混合时,这也是一个结合 DSP 和基于 AI/ML 的解决方案的积极研究领域。在 DSP 方面,这个看起来很有趣:https://silo.tips/download/automatic-chord-recognition-from-audio-using-enhanced-pitch-class-profile 并指向一系列来源。我怀疑一个真正强大的解决方案还需要理解上下文(例如,整个部分的关键)。对于这样的答案来说,所有这些都太过分了。 (3认同)