@audio/mir-structure 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/package.json +34 -0
- package/structure.js +64 -0
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/mir-structure",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Structural segmentation \u2014 Foote checkerboard novelty over MFCC self-similarity (Foote 2000)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "structure.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./structure.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"structure.js"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@audio/spectral-mfcc": "^1.0.0"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"audio",
|
|
20
|
+
"dsp",
|
|
21
|
+
"mir",
|
|
22
|
+
"structure",
|
|
23
|
+
"segmentation",
|
|
24
|
+
"novelty"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "Dmitry Iv. <dfcreative@gmail.com>",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/structure.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Structural segmentation — Foote novelty (Foote 2000): MFCC timbre frames → cosine
|
|
2
|
+
// self-similarity → checkerboard-kernel correlation along the diagonal → novelty peaks
|
|
3
|
+
// = section boundaries (verse/chorus/texture changes).
|
|
4
|
+
|
|
5
|
+
import mfcc from '@audio/spectral-mfcc'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {Float32Array} data — mono PCM
|
|
9
|
+
* @param {object} opts — { fs=44100, frameSize=2048, hop=1024, kernel=16 (frames per
|
|
10
|
+
* checkerboard quadrant), sensitivity=1 (higher → fewer boundaries) }
|
|
11
|
+
* @returns {{ boundaries: number[] (seconds), novelty: Float32Array, times: Float32Array }}
|
|
12
|
+
*/
|
|
13
|
+
export default function structure (data, { fs = 44100, frameSize = 2048, hop = 1024, kernel = 16, sensitivity = 1 } = {}) {
|
|
14
|
+
let nFrames = Math.max(0, Math.floor((data.length - frameSize) / hop) + 1)
|
|
15
|
+
// timbre vectors: c1..c12 (drop c0 energy), L2-normalized
|
|
16
|
+
let feats = []
|
|
17
|
+
for (let i = 0; i < nFrames; i++) {
|
|
18
|
+
let c = mfcc(data.subarray(i * hop, i * hop + frameSize), { fs })
|
|
19
|
+
let v = Float64Array.from(c.subarray(1))
|
|
20
|
+
let norm = Math.hypot(...v) || 1
|
|
21
|
+
for (let k = 0; k < v.length; k++) v[k] /= norm
|
|
22
|
+
feats.push(v)
|
|
23
|
+
}
|
|
24
|
+
let sim = (a, b) => {
|
|
25
|
+
let s = 0
|
|
26
|
+
for (let k = 0; k < a.length; k++) s += a[k] * b[k]
|
|
27
|
+
return s
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let K = kernel
|
|
31
|
+
let novelty = new Float32Array(nFrames)
|
|
32
|
+
for (let i = K; i < nFrames - K; i++) {
|
|
33
|
+
let n = 0
|
|
34
|
+
for (let u = 0; u < K; u++) {
|
|
35
|
+
for (let v = 0; v < K; v++) {
|
|
36
|
+
let within = sim(feats[i - 1 - u], feats[i - 1 - v]) + sim(feats[i + u], feats[i + v])
|
|
37
|
+
let across = 2 * sim(feats[i - 1 - u], feats[i + v])
|
|
38
|
+
n += within - across
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
novelty[i] = n / (K * K)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// peak pick: local maxima above mean + sensitivity·std, min distance K
|
|
45
|
+
let mean = 0
|
|
46
|
+
for (let v of novelty) mean += v
|
|
47
|
+
mean /= nFrames
|
|
48
|
+
let sd = 0
|
|
49
|
+
for (let v of novelty) sd += (v - mean) ** 2
|
|
50
|
+
sd = Math.sqrt(sd / nFrames)
|
|
51
|
+
let thresh = mean + sensitivity * sd
|
|
52
|
+
|
|
53
|
+
let boundaries = []
|
|
54
|
+
let last = -Infinity
|
|
55
|
+
for (let i = K; i < nFrames - K; i++) {
|
|
56
|
+
if (novelty[i] > thresh && novelty[i] >= novelty[i - 1] && novelty[i] >= novelty[i + 1] && i - last >= K) {
|
|
57
|
+
boundaries.push((i * hop + frameSize / 2) / fs)
|
|
58
|
+
last = i
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
let times = new Float32Array(nFrames)
|
|
62
|
+
for (let i = 0; i < nFrames; i++) times[i] = (i * hop + frameSize / 2) / fs
|
|
63
|
+
return { boundaries, novelty, times }
|
|
64
|
+
}
|