@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.
Files changed (2) hide show
  1. package/package.json +34 -0
  2. 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
+ }