@audio/mir-fingerprint 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fingerprint.js +78 -0
- package/package.json +34 -0
package/fingerprint.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Landmark audio fingerprinting (Wang 2003, Shazam class): spectrogram peak
|
|
2
|
+
// constellation → anchor→target hashes (f1, f2, Δt) → matching = offset-histogram vote.
|
|
3
|
+
// Robust to noise and level; exact-match identification, not similarity.
|
|
4
|
+
|
|
5
|
+
import { fft } from 'fourier-transform'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {Float32Array} data — mono PCM
|
|
9
|
+
* @param {object} opts — { fs=44100, frameSize=1024, hop=512, peaksPerFrame=5, fanout=5,
|
|
10
|
+
* window=32 (max Δt frames for pairing) }
|
|
11
|
+
* @returns {Array<{hash:number, t:number}>} landmarks (t in frames; frame = hop/fs seconds)
|
|
12
|
+
*/
|
|
13
|
+
export default function fingerprint (data, { frameSize = 1024, hop = 512, peaksPerFrame = 5, fanout = 5, window = 32 } = {}) {
|
|
14
|
+
let half = frameSize / 2
|
|
15
|
+
let nFrames = Math.max(0, Math.floor((data.length - frameSize) / hop) + 1)
|
|
16
|
+
let win = new Float64Array(frameSize)
|
|
17
|
+
for (let i = 0; i < frameSize; i++) win[i] = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / frameSize)
|
|
18
|
+
let buf = new Float64Array(frameSize)
|
|
19
|
+
|
|
20
|
+
// constellation: per-frame prominent spectral peaks
|
|
21
|
+
let peaks = [] // [{t, f}]
|
|
22
|
+
for (let t = 0; t < nFrames; t++) {
|
|
23
|
+
for (let i = 0; i < frameSize; i++) buf[i] = data[t * hop + i] * win[i]
|
|
24
|
+
let [re, im] = fft(buf)
|
|
25
|
+
let mag = new Float64Array(half)
|
|
26
|
+
let mean = 0
|
|
27
|
+
for (let k = 1; k < half; k++) { mag[k] = re[k] * re[k] + im[k] * im[k]; mean += mag[k] }
|
|
28
|
+
mean /= half
|
|
29
|
+
let frame = []
|
|
30
|
+
for (let k = 3; k < half - 3; k++) {
|
|
31
|
+
if (mag[k] > 4 * mean && mag[k] > mag[k - 1] && mag[k] >= mag[k + 1] && mag[k] > mag[k - 2] && mag[k] >= mag[k + 2]) {
|
|
32
|
+
frame.push({ k, m: mag[k] })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
frame.sort((a, b) => b.m - a.m)
|
|
36
|
+
for (let p of frame.slice(0, peaksPerFrame)) peaks.push({ t, f: p.k })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// pair anchors with forward targets
|
|
40
|
+
let landmarks = []
|
|
41
|
+
for (let i = 0; i < peaks.length; i++) {
|
|
42
|
+
let a = peaks[i], fan = 0
|
|
43
|
+
for (let j = i + 1; j < peaks.length && fan < fanout; j++) {
|
|
44
|
+
let b = peaks[j]
|
|
45
|
+
let dt = b.t - a.t
|
|
46
|
+
if (dt < 1) continue
|
|
47
|
+
if (dt > window) break
|
|
48
|
+
landmarks.push({ hash: (a.f * 512 + b.f) * 64 + dt, t: a.t })
|
|
49
|
+
fan++
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return landmarks
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Match two fingerprints: histogram of time offsets over shared hashes.
|
|
57
|
+
* @returns {{ score: number (votes for the best offset), offset: number (frames, b within a) }}
|
|
58
|
+
*/
|
|
59
|
+
export function match (fpA, fpB) {
|
|
60
|
+
let index = new Map()
|
|
61
|
+
for (let l of fpA) {
|
|
62
|
+
let arr = index.get(l.hash)
|
|
63
|
+
if (arr) arr.push(l.t)
|
|
64
|
+
else index.set(l.hash, [l.t])
|
|
65
|
+
}
|
|
66
|
+
let hist = new Map()
|
|
67
|
+
for (let l of fpB) {
|
|
68
|
+
let arr = index.get(l.hash)
|
|
69
|
+
if (!arr) continue
|
|
70
|
+
for (let ta of arr) {
|
|
71
|
+
let off = ta - l.t
|
|
72
|
+
hist.set(off, (hist.get(off) || 0) + 1)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let score = 0, offset = 0
|
|
76
|
+
for (let [off, count] of hist) if (count > score) { score = count; offset = off }
|
|
77
|
+
return { score, offset }
|
|
78
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/mir-fingerprint",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Landmark fingerprinting — spectral peak constellation hashes + offset-vote matching (Wang 2003 class)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "fingerprint.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./fingerprint.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"fingerprint.js"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"fourier-transform": "^2.2.0"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"audio",
|
|
20
|
+
"dsp",
|
|
21
|
+
"mir",
|
|
22
|
+
"fingerprint",
|
|
23
|
+
"shazam",
|
|
24
|
+
"landmark"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "Dmitry Iv. <dfcreative@gmail.com>",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|