如何生成 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
注:
- 普通JavaScript,请。没有图书馆。
根据用途,有多种方法可以做到这一点。
对于准确性,您可以使用常规体积和单位进行测量,例如 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>
给定一段 30 多岁的纯网络视频:
<video src="my-video.mp4"></video>
我怎样才能生成它的音量级别图表?
volume|
level| ******
| * * **
| * * * **
|** * *** *
| ** * * * *
+---------------*-*-----************------+--- time
0 30s
video is and quiet
loud here here
注:
- 普通JavaScript,请。没有图书馆。
根据用途,有多种方法可以做到这一点。
对于准确性,您可以使用常规体积和单位进行测量,例如 RMS, LUFS/LKFS (K-weighted, loudness), dBFS(满刻度 dB)等。
简单天真的方法是只绘制波形的峰值。您只会对正值感兴趣。为了获得峰值,您将检测两点之间的方向,并在方向从向上变为向下 (p0 > p1) 时记录第一个点。
对于所有方法,您最终都可以应用某种形式的平滑,例如加权移动平均值 (
要绘图,您将获得当前样本的值,假设它已被归一化并将其绘制为线或指向 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>