缘起

我个人偶然发现的网站,用多了觉得这个网站测出来的BPM准确率还挺高的,几乎就没错过,就算有也只是差那么一点点(比我自己用librosa还有wave库要高,这两个准的时候很准,不准的时候差十万八千里),于是就想看看抄抄算法。

image-20240110221734848

过程

查看源代码,发现有两个核心的js:AppBpmFinder以及key-finder

先看AppBpmFinder,搜索bpm,找到和bpm处理有关的代码。

image-20240110202158376

我这里首先下的断点是最下面那个,运行,发现到这里的时候BPM已经测出来了,于是往上再设断点。

在这里设置断点后,运行,此时BPM还没测出来,单步执行下一个函数调用。

p.postMessage({
sampleRate: e.sampleRate,
arrayPCM: e.numberOfChannels > 1 ? [e.getChannelData(0), e.getChannelData(1)] : [e.getChannelData(0)]
})

然后发现这里是post了采样率以及PCM数组的message到了key-finder中。(所以为什么是在key-finder里面测BPM)

image-20240110202242870

感觉快找到真相了~~(并不)~~,贴一个上图用到的一些函数。

const ZB = 16e3
, lB = new Essentia(h)
, wg = A=>lB.arrayToVector(A)
, Yg = A=>lB.KeyExtractor(A, !0, 4096, 4096, 12, 3500, 60, 25, .2, "bgate", ZB, 1e-4, 440, "cosine", "hann")
, yg = A=>lB.PercivalBpmEstimator(A, 1024, 2048, 128, 128, 210, 50, ZB)
, Ug = A=>{
if (A.length === 2) {
const I = A[0]
, E = A[1];
return I.map((C,g)=>.5 * (C + E[g]))
} else
return A[0]
}
;
function Sg(A, I, E) {
if (E === I)
return A;
let C = I / E
, g = Math.round(A.length / C)
, B = new Float32Array(g)
, Q = 0
, G = 0;
for (; Q < B.length; ) {
let i = Math.round((Q + 1) * C)
, o = 0
, a = 0;
for (let F = G; F < i && F < A.length; F++)
o += A[F],
a++;
B[Q] = o / a,
Q++,
G = i
}
return B
}

首先看看

I = Ug(I)

Ug = A=>{
if (A.length === 2) {
const I = A[0]
, E = A[1];
return I.map((C,g)=>.5 * (C + E[g]))
} else
return A[0]
}

Ug 这个函数首先判断 A 的 length 是否为 2 (对应音频是否为双声道),从一个二维数组 A 中取出第一行和第二行,分别赋值给 I 和 E,然后返回一个新的数组,它的每个元素是 I 和 E 对应位置元素的平均值。例如,如果 A 是:

[[1, 2, 3],
[4, 5, 6]]

那么 I 就是 [1, 2, 3],E 就是 [4, 5, 6],返回的数组就是 [2.5, 3.5, 4.5]

这段代码使用了 JavaScript 的 map 方法,它可以对一个数组的每个元素执行一个函数,并返回一个新的数组。这里的函数是 (C,g)=>.5 * (C + E[g]),它的参数是 C 和 g,其中 C 是 I 的每个元素,g 是它的索引。函数的返回值是 C 和 E 在相同索引位置的元素的平均值,即 .5 * (C + E[g])

如果 A 的 length 为 1 ,则直接返回 A[0]。

所以这个函数的作用是将双声道音频的数据压缩(取平均)。

然后再看

I = Sg(I,E,C)

function Sg(A, I, E) {
if (E === I)
return A;
let C = I / E
, g = Math.round(A.length / C)
, B = new Float32Array(g)
, Q = 0
, G = 0;
for (; Q < B.length; ) {
let i = Math.round((Q + 1) * C)
, o = 0
, a = 0;
for (let F = G; F < i && F < A.length; F++)
o += A[F],
a++;
B[Q] = o / a,
Q++,
G = i
}
return B
}

这个函数的意思是,它可以将一个音频信号的采样率从 I 转换为 E(16000),返回一个新的 Float32Array,它的长度是原来的 I / E 倍。这个函数使用了一种简单的重采样算法,它将原始信号分成若干个等长的区间,每个区间的长度是 I / E,然后计算每个区间内的信号的平均值,作为新信号的一个采样点。这种算法可以有效地减少信号的长度,但是也会损失一些信号的细节和质量。

接下来就是关键了

const g = wg(I), B = yg(g);

wg = A=>lB.arrayToVector(A)
yg = A=>lB.PercivalBpmEstimator(A, 1024, 2048, 128, 128, 210, 50, ZB)

这里首先 wg 是一个箭头函数,它接受一个参数 A,然后返回 lB 的 arrayToVector 方法的结果,这个方法可以将一个数组转换成一个向量2,然后调用 yg 计算出BPM。

那么这个lb是何方神圣呢?

lB = new Essentia(h)

lB 是一个 Essentia 类的实例,它是一个音频分析和处理的库1

噔 噔 咚

结果

原来是Essentia库。

yg = A=>lB.PercivalBpmEstimator(A, 1024, 2048, 128, 128, 210, 50, ZB)

所以这个网站是调用Essentia库中PercivalBpmEstimator的方法得到的BPM。

但是问题来了,我一开始调研的时候是有考虑过Essentia库的,但是这个库在Windows上不能直接pip install,所以我就没用这个库。

贴一个作者在不同issue下的回复。

You can run pip install essentia inside a Windows Subsystem for Linux (WSL). We do not provide pip wheels for Windows yet.

Wheels aren’t yet supported for Windows (see #1157).

唉,把这个库安装,然后用上,实在是太难了。要么换一个操作系统再开发、打包等等进行一系列操作;要么花大把时间看着也不知到能不能成的英文教程(贴一下链接,真的看着就头大)去install,感觉都是非常吃力不讨好的。遂放弃。(反正我的项目中有究极无敌准确的人工测算法(不是)

世上无难事,只要肯放弃。