如何生成 HTML5 视频音量级别图表?

How to generate HTML5 video volume level chart?

给定一段 30 多岁的纯网络视频:

<video src="my-video.mp4"></video>

我怎样才能生成它的音量级别图表?

volume|
 level|    ******
      |   *      *                           **
      |  *        *                         *  **
      |**          *      ***              *    
      |             ** * *   *            *
      +---------------*-*-----************------+--- time
      0                                        30s
          video is             and quiet 
          loud here            here

注:

根据用途,有多种方法可以做到这一点。

对于准确性,您可以使用常规体积和单位进行测量,例如 RMS, LUFS/LKFS (K-weighted, loudness), dBFS(满刻度 dB)等。

简单天真的方法是只绘制波形的峰值。您只会对正值感兴趣。为了获得峰值,您将检测两点之间的方向,并在方向从向上变为向下 (p0 > p1) 时记录第一个点。

对于所有方法,您最终都可以应用某种形式的平滑,例如加权移动平均值 () 或通用平滑算法来消除小峰值和变化,如果是 RMS、dB 等,您会使用 window 大小,它可以与 bin 平滑(每个段的平均值)结合使用。

要绘图,您将获得当前样本的值,假设它已被归一化并将其绘制为线或指向 canvas 按绘图区域高度缩放的点。

关于加载源数据的小型讨论

解决评论中的一些问题;这些只是我的头脑,可以提供一些指示-

由于 Web Audio API 无法自行进行流式传输,因此您必须将整个文件加载到内存中并将音轨解码到缓冲区中。

  • 优点:有效(分析部分),当数据最终准备好时快速分析,适用于较小的文件,如果缓存 URL 无需重新下载即可使用
  • 缺点:初始加载时间长 time/bad UX,可能的内存 hog/not 适用于大文件,音频与视频同步“分离”,强制重用 URL*,如果大型 and/or 缓存未到位,则必须下载文件 again/streamed,目前在某些 browsers/versions 中会导致问题(参见下面的示例) .

*: 始终可以选择将下载的视频作为 blob 存储在 IndexedDB 中(及其含义)并使用 Object-URL 用那个 blob 在视频元素中流式传输(可能需要 MSE 才能正常工作,我自己还没试过)。

流式传输时绘图:

  • 优点:memory/resources
  • 便宜
  • 缺点:在播放整个文件之前无法完整显示情节,用户可能 skip/jump 部分,可能无法完成

旁加载低质量的纯单声道音频文件:

  • 优点:音频可以独立于视频文件加载到内存中,为电平使用提供足够好的近似值
  • 缺点:可能会延迟视频的初始加载,可能无法在视频开始前及时准备好,需要提前进行额外处理

服务器端绘图:

  • 优点:上传时可以绘制,可以存储请求视频时作为元数据提供的原始绘图数据,低带宽,视频开始时数据就绪(假设数据代表时间段的平均值)。
  • 缺点:需要服务器上的基础设施来分离、分析和生成绘图数据,这取决于数据的存储方式可能需要修改数据库。

我可能遗漏或遗漏了一些要点,但它应该给出了总体思路...

例子

此示例测量每个样本给定 window 大小的常规 dB。 window 尺寸越大,结果越平滑,但也需要更多时间来计算。

请注意,为简单起见,在此示例中,像素位置决定了 dB window 范围。这可能会产生不均匀的 gaps/overlaps,具体取决于影响当前样本值的缓冲区大小,但应该适用于此处演示的目的。同样为了简单起见,我通过将 dB 读数除以 40 来缩放 dB 读数,这里是一个有点任意的数字(ABS 只是为了绘图和我大脑的工作方式(?)在 night/early 早上我做了这个: )).

我在顶部添加了红色的 bin/segment-smoothing 以更好地显示与自动调平等相关的长期音频变化。

我在这里使用的是音频源,但您可以插入视频源,只要它包含可以解码的音轨格式(aac、mp3、ogg 等)即可。

除此之外,这个例子就是一个例子。它不是生产代码,因此请物有所值。根据需要进行调整。

(出于某种原因,音频无法在 Firefox v58beta 中播放,但会播放。音频在 Chrome、FF58dev 中播放)。

var ctx = c.getContext("2d"), ref, audio;
var actx = new (AudioContext || webkitAudioContext)();
var url = "//dl.dropboxusercontent.com/s/a6s1qq4lnwj46uj/testaudiobyk3n_lo.mp3";
ctx.font = "20px sans-serif";
ctx.fillText("Loading and processing...", 10, 50);
ctx.fillStyle = "#001730";

// Load audio
fetch(url, {mode: "cors"})
.then(function(resp) {return resp.arrayBuffer()})
.then(actx.decodeAudioData.bind(actx))
.then(function(buffer) {

  // Get data from channel 0 (you will want to measure all/avg.)
  var channel = buffer.getChannelData(0);

  // dB per window + Plot
  var points = [0];
  ctx.clearRect(0, 0, c.width, c.height);
  ctx.moveTo(x, c.height);
  for(var x = 1, i, v; x < c.width; x++) {
    i = ((x / c.width) * channel.length)|0;   // get index in buffer based on x
    v = Math.abs(dB(channel, i, 8820)) / 40;  // 200ms window, normalize
    ctx.lineTo(x, c.height * v);
    points.push(v);
  }
  ctx.fill();

  // smooth using bins
  var bins = 40;  // segments
  var range = (c.width / bins)|0;
  var sum;
  ctx.beginPath();
  ctx.moveTo(0,c.height);
  for(x = 0, v; x < points.length; x++) {
    for(v = 0, i = 0; i < range; i++) {
      v += points[x++];
    }
    sum = v / range;
    ctx.lineTo(x - (range>>1), sum * c.height); //-r/2 to compensate visually
  }
  ctx.lineWidth = 2;
  ctx.strokeStyle = "#c00";
  ctx.stroke();
  
  // for audio / progressbar only
  c.style.backgroundImage = "url(" + c.toDataURL() + ")";
  c.width = c.width;
  ctx.fillStyle = "#c00";
  audio = document.querySelector("audio");
  audio.onplay = start;
  audio.onended = stop;
  audio.style.display = "block";
});

// calculates RMS per window and returns dB
function dB(buffer, pos, winSize) {
  for(var rms, sum = 0, v, i = pos - winSize; i <= pos; i++) {
    v = i < 0 ? 0 : buffer[i];
    sum += v * v;
  }
  rms = Math.sqrt(sum / winSize);  // corrected!
  return 20 * Math.log10(rms);
}

// for progress bar (audio)
function start() {if (!ref) ref = requestAnimationFrame(progress)}
function stop() {cancelAnimationFrame(ref);ref=null}
function progress() {
  var x = audio.currentTime / audio.duration * c.width;
  ctx.clearRect(0,0,c.width,c.height);
  ctx.fillRect(x-1,0,2,c.height);
  ref = requestAnimationFrame(progress)
}
body {background:#536375}
#c {border:1px solid;background:#7b8ca0}
<canvas id=c width=640 height=300></canvas><br>
<audio style="display:none" src="//dl.dropboxusercontent.com/s/a6s1qq4lnwj46uj/testaudiobyk3n_lo.mp3" controls></audio>