Skip to content

LUFS 只是预警之一:浏览器端音频发布前体检 ​

译注:本文编译自 dev.to 文章《Creator Audio Pre-Publish Health Check: Why LUFS Is Only One Warning》,作者介绍了一款基于 Web Audio API 的浏览器端音频预检工具。原文链接见文末。

发布视频或播客之前,创作者真正关心的往往不是波形看起来"够不够大",而是导出的文件在另一个平台上会不会出问题。响度、峰值余量、削波和直流偏移回答的是不同的问题。Creator Audio Pre-Publish Health Check 把这些测量放在一起,让创作者在上传前就能发现明显的问题。

这款工具的目标读者是 YouTuber、播客剪辑师或音乐人,他们需要的是快速的浏览器端预检,而不是替代母带电平表。真正有价值的点在于:这些测量值是如何推导出来的,以及为什么"绿灯"结果仍然需要结合上下文判断。它是一个可运行的 Web Audio 分析示例,使用明确的阈值,而不是一个神秘的单一评分。

在本地解码文件,然后检查采样点 ​

上传入口接受音频和视频 MIME 类型,以及 FLAC、OGG、OPUS 扩展名。runAnalysis 将选中的文件读取为 ArrayBuffer,在 48 kHz 下创建 AudioContext,并调用 decodeAudioData:

javascript
const arrayBuffer = await selectedFile.value.arrayBuffer();
const audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 48000 });

let audioBuffer;
try {
  audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
} catch {
  await audioCtx.close();
  throw new Error("decode");
}
await audioCtx.close();

解码完成后,代码通过 getChannelData 收集每个声道。直流偏移只从第零声道计算,而削波则跨所有声道检查:

javascript
let dcSum = 0;
const len = channels[0].length;
for (let i = 0; i < len; i++) dcSum += channels[0][i];
const dcOffset = dcSum / len;

let clipCount = 0;
let totalSamples = 0;
for (const ch of channels) {
  for (let i = 0; i < ch.length; i++) {
    if (Math.abs(ch[i]) >= 0.99999) clipCount++;
    totalSamples++;
  }
}
const clippingRate = clipCount / totalSamples;

这个阈值接近归一化的 0 dBFS 上限。它是一个采样级削波指标,适合发现顶部被削平的数字化音频,但并不能证明模拟环节或编解码器永远不会失真。

显示的峰值是最大的解码采样值 ​

分析器会在所有声道中搜索绝对值最大的采样点,并将该振幅转换为分贝:

javascript
let maxSample = 0;
for (const ch of channels) {
  for (let i = 0; i < ch.length; i++) {
    const abs = Math.abs(ch[i]);
    if (abs > maxSample) maxSample = abs;
  }
}
const truePeak = maxSample > 0 ? 20 * Math.log10(maxSample) : -Infinity;

界面把这个值标为"True Peak",并建议保持在 -1.0 dBTP 或以下。对于有损编码来说,这是一个实用的安全目标,但该实现测量的是最大解码采样值;它没有过采样,也没有重建采样间峰值。在与专业真峰值表对比时,这一区别很重要。一个文件可能通过这项浏览器检查,但在转码后仍暴露出采样间峰值。

LUFS 使用一次离线滤波 ​

在响度方面,calcLufs 会构建一个 OfflineAudioContext,最多复制两个声道到缓冲区,并通过两个 Biquad 滤波器处理:

javascript
const numCh = Math.min(channels.length, 2);
const offlineCtx = new OfflineAudioContext(numCh, totalSamples, sampleRate);

const preFilter = offlineCtx.createBiquadFilter();
preFilter.type = "highshelf";
preFilter.frequency.value = 1681.974;
preFilter.gain.value = 3.999843853;

const rlbFilter = offlineCtx.createBiquadFilter();
rlbFilter.type = "highpass";
rlbFilter.frequency.value = 38.13547;
rlbFilter.Q.value = 0.5003270373;

它渲染滤波后的缓冲区,对采样平方求平均,并返回 -0.691 + 10 * Math.log10(meanSquare)。结果是对 ITU-R BS.1770 风格 K 加权积分响度的近似。平台对比则使用配置好的目标值:YouTube 和 Spotify 为 -14 LUFS,Apple Music 为 -16,播客为 -16 且容差更宽,Instagram/TikTok/Facebook 为 -14,EBU R128 为 -23。

状态逻辑刻意保持简单。-16 到 -12 LUFS 之间标记为良好,低于 -24 给出警告,中间区域则视为超限。这让界面在预检场景下可操作,同时平台卡片展示了不同的目标值和容差选择。

结果对象还会在核心指标之外保留时长、声道数和采样率。这些字段并非装饰:异常的声道数可以解释响度不匹配,采样率则告诉你浏览器实际解码成了什么。进度值是界面检查点,而不是剩余 CPU 时间的度量——读取前为 5,缓冲区加载后为 20,解码后为 50,分析完成时为 100——因此很长的文件在离线渲染仍在进行时,可能看起来像卡住了。

发布前的诚实边界 ​

所有处理都在浏览器中运行,但解码一个很长的多声道文件仍然需要为完整的 AudioBuffer 和一次离线渲染分配内存。即使扩展名看起来受支持,浏览器也可能拒绝某个编解码器,而 catch 块只会报告一个通用的解码失败。该工具不会归一化或修复文件,它只测量解码后的数据。

直流偏移从第一个声道计算,当其绝对平均值达到 0.01 时标记。削波是达到或超过阈值的采样百分比,而不是按时长加权的感知度量。最重要的是,由于浏览器精度、采样率行为、声道处理方式以及缺少真峰值过采样,LUFS 滤波和采样峰值计算可能与 DAW 不同。作者使用这份报告来决定下一步检查什么,然后在专用电平表中交叉核对广播或发行母带。

原文链接 ​

https://dev.to/begoodtool/creator-audio-pre-publish-health-check-why-lufs-is-only-one-warning-2ojk