使用方法
- 点「开始检测麦克风」。浏览器会弹出授权提示,选择「允许」;如果选了「阻止」,页面会告诉你被拒绝以及怎么改回来,而不是一直转圈。
- 授权通过后,音量条立刻随声音起伏,频谱图显示各频段的能量分布,黑色竖线是峰值保持标记——它记录你这一段时间喊到的最大音量,按设定速度缓慢回落。
- 对着麦克风用正常音量说话,让读数稳定在 −24 到 −12 dBFS 之间比较合适;一直贴着 0 dBFS 说明过载,录出来会破音;低于 −60 dBFS 基本等于没声音。
- 换设备时,先授权,「输入设备」下拉框才会列出真正的设备名(未授权前浏览器只给一个空白项)。选中另一个设备后检测会自动重启。
- 「FFT 点数」决定频率分辨率:点数越大,频谱越细,但时间窗口也越长、对瞬态反应越迟钝。默认 2048 是常规折中。
- 「峰值保持衰减」控制黑线回落速度,单位是 dB/帧;填 0 则峰值停在最大值不再回落,适合做最大音量标定。
- 测完点「停止并释放麦克风」。这一步会真正调用
track.stop()关掉采集,而不是仅仅停止画图。
这个麦克风测试能解决什么问题
一是确认麦克风到底有没有被系统认到。值不值得怀疑硬件之前,先用这里看一眼:有波形说明设备正常,授权成功却始终是一条直线,问题多半在系统输入音量被拉到 0,或者是笔记本的物理静音键被按下了。
二是找出「对方听不到我」的位置。开会时听不到自己,可能是选错了输入设备。授权之后设备名会真正列出来(这一点很关键,下面会解释为什么),换成另一个再说话,音量条有没有反应一目了然。
三是把麦克风音量调到合适的挡位。看着 dBFS 读数说话,把系统输入增益调到正常说话时落在 −24 到 −12 dBFS,就不用靠「听起来差不多」来猜了。
四是快速判断有没有持续的底噪或电流声。安静环境下如果频谱在某一两个频点长期亮着一根细柱,通常是电源噪声或设备自激;宽带铺满一层则更像环境底噪。麦克风测试只做呈现和判断,不替你做降噪。
本页不做这些事:不录音、不生成音频文件、不分析音高、不做回声消除、不上传任何音频数据。它只回答一个问题——这台设备现在收到了多大的声音、分布在哪些频段。
检测原理:getUserMedia、AnalyserNode 与 dBFS
链路只有四步:getUserMedia 拿到音频流 → AudioContext 建立音频图 → AnalyserNode 每帧取样 → 换算成音量与频谱。每一步都有必须先满足的前提。
第一步,权限必须先手动触发,而且页面必须在安全上下文里。navigator.mediaDevices.getUserMedia({audio:true}) 只在 HTTPS 或 localhost 下可用;用 file:// 直接打开本地 HTML,或者用 http:// 访问一个真实域名,navigator.mediaDevices 根本就是 undefined,连请求都发不出去。所以本页把调用放在按钮的点击处理里:既是浏览器「必须由用户手势触发」的要求,也能避免一打开页面就弹权限框。授权成功后拿到的 MediaStream 还必须在测完主动释放——stream.getTracks().forEach(t => t.stop()),否则浏览器标签上的录音图标会一直亮着。
第二步,enumerateDevices() 在授权前拿不到设备名。这是最容易误解的一点。未授权时调用它,返回的数组里 label 是空字符串、deviceId 也是空的,你只会看到几项没有名字的占位。只有授权之后再次枚举,才能读到「内置麦克风阵列」这样的真实名字。所以本页的设备下拉框在授权回调里才去填充,而不是在页面加载时。
第三步,AudioContext 要在用户手势后 resume()。现代浏览器为防止自动播放噪音,新建的 AudioContext 处于 suspended 状态,必须调用 resume() 才会走时钟。本页在按钮点击的同一个手势里创建并恢复上下文,不会出现「接了线但图不动」的情况。
第四步,音量怎么算成 dBFS。每次绘制前调用 analyser.getByteTimeDomainData(buf),拿到的是一串 0~255 的字节,静音的中值是 128。先归一化:v = (byte − 128) / 128,范围落在 −1 到 1。然后求均方根 rms = sqrt(Σv² / n),最后换算成分贝:
dBFS = 20 × log10(rms),满量程记作 0 dBFS。这里有个反直觉的点:满幅的单频正弦波只有 −3.01 dBFS,不是 0,因为它的 rms 是峰值的 1/√2 = 0.7071,而 20 × log10(0.7071) = −3.01。所以看到读数稳定在 −3 dBFS 左右,说明已经贴着满幅了。另外振幅每减半就是 −6.02 dB:rms 从 1 降到 0.5 是 −6.02,降到 0.25 是 −12.04。真正的数字静音让 log10(0) 变成负无穷,代码里必须钳位到 −60,否则读数是 -Infinity。
频谱那一块靠 getByteFrequencyData。它给出 fftSize / 2 个频点(本页默认 1024 个),第 i 个频点对应的中心频率是 i × sampleRate / fftSize。这就是频率分辨率的来源:分辨率 = 采样率 ÷ FFT 点数。48 kHz 采样配 fftSize 2048,分辨率 = 48000 / 2048 = 23.4 Hz,意思是两个频率相差不到 23.4 Hz 就没法在这张图上分开;换成 4096 点,分辨率细到 11.7 Hz,代价是时域窗口从 42.67 毫秒拉长到 85.33 毫秒,响应变钝。这也解释了为什么不能用 setInterval 去轮询——采样应当跟着屏幕刷新走,用 requestAnimationFrame 才既不掉帧又不空转。
完整示例(数值仅作演示)
页面打开时的默认条件:FFT 点数 2048、峰值衰减 0.5 dB/帧,演示用的一组读数按 48 kHz 采样率折算,与页面上的演示面板一致:
- 频率分辨率
48000 ÷ 2048 = 23.4 Hz,有效频点2048 ÷ 2 = 1024个,最高可分析频率是奈奎斯特频率24000 Hz; - 时域窗口
2048 ÷ 48000 = 42.67 ms; - 演示电平 −15.0 dBFS,它由时域字节峰值 160 反推而来:
(160 − 128) / 128 = 0.25,按正弦 rms0.25 ÷ √2 = 0.1768,再算20 × log10(0.1768) = −15.05 dBFS; - 若读数换成字节峰值 136,归一化 0.0625、rms 0.0442,结果是 −27.09 dBFS,这已经属于「能听见但偏轻」的一档。
把这几组数对上页面:音量条按 −60 到 0 dBFS 线性铺开,−15.0 dBFS 对应 (−15.0 + 60) ÷ 60 = 75% 的宽度,正是演示音量条停在的位置;峰值黑线同宽,因为演示取的就是峰值本身。
采样率、FFT 点数与频率分辨率对照表
下表每一行都能直接套用公式 分辨率 = 采样率 ÷ FFT 点数、频点数 = 点数 ÷ 2、窗口 = 点数 ÷ 采样率 复核。同一台设备换不同采样率,能分辨的间隔会跟着变。
| 采样率 | FFT 点数 | 频率分辨率 | 频点数 | 时域窗口 |
|---|---|---|---|---|
| 44,100 Hz | 1024 | 43.07 Hz | 512 | 23.22 ms |
| 44,100 Hz | 2048 | 21.53 Hz | 1,024 | 46.44 ms |
| 44,100 Hz | 4096 | 10.77 Hz | 2,048 | 92.88 ms |
| 48,000 Hz | 1024 | 46.88 Hz | 512 | 21.33 ms |
| 48,000 Hz(默认) | 2048(默认) | 23.44 Hz | 1,024 | 42.67 ms |
| 48,000 Hz | 4096 | 11.72 Hz | 2,048 | 85.33 ms |
| 16,000 Hz | 1024 | 15.63 Hz | 512 | 64.00 ms |
| 16,000 Hz | 2048 | 7.81 Hz | 1,024 | 128.00 ms |
| 8,000 Hz | 512 | 15.63 Hz | 256 | 64.00 ms |
| 8,000 Hz | 1024 | 7.81 Hz | 512 | 128.00 ms |
| 96,000 Hz | 2048 | 46.88 Hz | 1,024 | 21.33 ms |
| 96,000 Hz | 4096 | 23.44 Hz | 2,048 | 42.67 ms |
| 96,000 Hz | 8192 | 11.72 Hz | 4,096 | 85.33 ms |
| 48,000 Hz | 8192 | 5.86 Hz | 4,096 | 170.67 ms |
| 44,100 Hz | 8192 | 5.38 Hz | 4,096 | 185.76 ms |
| 192,000 Hz | 4096 | 46.88 Hz | 2,048 | 21.33 ms |
注意 96,000 Hz 那一档:采样率翻倍、点数不变时分辨率反而变粗(2048 点从 23.4 Hz 变成 46.9 Hz),因为 Nyquist 上限被抬到了 48 kHz,同样的频点数量要摊到更宽的频率范围上。想在高采样率下保持细分辨率,只能把点数同步加倍。
边界与权限说明
- 必须 HTTPS 或 localhost。这是浏览器的硬性规定,不是本工具的限制。用
file://打开的页面拿不到navigator.mediaDevices,用http://访问真实域名同样拿不到;页面会明确告诉你属于哪一种。 - 权限只在点击后申请。打开页面不会弹任何授权框,也不会占用麦克风。拒绝授权后浏览器会记住这个站点的选择,再次点击不会重新弹框,需要在地址栏的站点设置里改回「允许」。
- 设备名要授权后才可见。授权前
enumerateDevices()返回的label是空串,所以下拉框在授权前只有一项占位提示。 - FFT 点数只接受 32 到 32768 之间的 2 的整数次幂。本页提供 1024 / 2048 / 4096 三挡,都是 2 的幂。填别的值会在赋值时被浏览器直接报错。
- dBFS 是相对满量程的比例,不是绝对声压。它衡量的是「数字信号离满幅还有多远」,跟房间里实际的声压级(dB SPL)不是一回事——不同麦克风的灵敏度差很多,同样的读数在专业话筒和笔记本内置麦上对应的实际响度完全不同。
- 读数低于 −60 dBFS 一律按静音处理。这不是精度限制,而是
log10(0)会得到负无穷,必须给一个下界;同时 −60 dBFS 已经低于绝大多数设备的本底噪声。 - 频谱是实时快照,不是平均值。它跟着屏幕刷新走(通常 60 次/秒),所以瞬态声音会一闪而过;长时间平均、精确的频域分析请用专业软件的 FFT 功能。
- 页面不保留任何音频。所有取样都发生在内存缓冲区里,刷新页面即清空,也不会写入浏览器存储。
常见问题
点了「开始检测麦克风」没反应,或者提示拿不到接口?
先看地址栏:必须是 https:// 或 localhost。用 file:// 双击打开的本地 HTML、或者用 http:// 访问真实域名,浏览器都不会提供 navigator.mediaDevices,这与浏览器版本无关,是安全上下文的规定。
授权了,但设备下拉框还是空的?
这正是 enumerateDevices() 的特性:授权前拿不到 label,只能拿到空的占位项。授权成功后本页会重新枚举一次并填上下拉框。如果授权成功仍是空的,检查操作系统是否把它列成了「未插入设备」。
提示「设备被占用」或「无法读取」怎么办?
说明麦克风已经被别的程序独占了,常见的是另一个浏览器标签、会议软件或录音软件。把占用它的程序关掉再点一次即可。部分声卡驱动也允许共享,但独占模式下会返回 NotReadableError。
音量条一直在动但听不到自己的声音,是坏了吗?
不是。这个麦克风测试只采集、不回放,因为把麦克风直接接到扬声器会形成啸叫。想确认扬声器,请用别的工具;这里只负责告诉你「麦克风有没有在收声、收到多大」。
为什么读数最高只到 −3 dBFS 左右?
因为满幅正弦波的均方根是峰值的 1/√2 ≈ 0.7071,代入 20 × log10(0.7071) 得 −3.01 dBFS。也就是说,单频信号贴着满幅时读数就是 −3 上下,只有方波这类 rms 等于峰值的波形才可能读到 0 dBFS。看到 −3 就说明已经到顶了,别再加增益。
频率分辨率 23.4 Hz 是什么意思,够用吗?
意思是两个频率相差小于 23.4 Hz 时,在这张图上会被合并成同一个频点,分不开。它以 采样率 ÷ FFT 点数 计算(48 kHz ÷ 2048 = 23.4 Hz)。人声基频大约 85~255 Hz,在这个分辨率下只对应第 4 到第 11 根柱子,做「有没有声音、大概在哪个频段」够用;做精细的频谱分析要把点数加到 4096 甚至更高。
测完直接关掉标签页可以吗?
可以,但更稳妥的是先点「停止并释放麦克风」。关闭标签页时浏览器也会释放采集,但如果你只是切到别的页面继续用,采集中会一直占用设备并显示录音标记。点这个按钮会执行 track.stop()、断开节点并关闭 AudioContext。
我的录音会被上传或保存吗?
不会。音频流只连接到一个 AnalyserNode,数据在内存里的两个字节数组之间流动,既不写入磁盘也不发请求。想自己验证,打开开发者工具的 Network 面板,点「开始检测麦克风」,可以看到没有任何请求发出。