@audio/mir-chord 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/chord.js +123 -0
- package/package.json +31 -0
package/chord.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 24 binary chord templates: C, C#, … B (major) then C, C#, … B (minor).
|
|
5
|
+
* Each template is a length-12 vector with 1 on chord tones, 0 elsewhere.
|
|
6
|
+
*/
|
|
7
|
+
export const TEMPLATES = buildTemplates()
|
|
8
|
+
|
|
9
|
+
function buildTemplates() {
|
|
10
|
+
let T = []
|
|
11
|
+
// major: root, +4, +7
|
|
12
|
+
for (let r = 0; r < 12; r++) {
|
|
13
|
+
let t = new Float64Array(12)
|
|
14
|
+
t[r] = 1; t[(r + 4) % 12] = 1; t[(r + 7) % 12] = 1
|
|
15
|
+
T.push({ root: r, quality: 'maj', label: NOTE_NAMES[r], vec: t })
|
|
16
|
+
}
|
|
17
|
+
// minor: root, +3, +7
|
|
18
|
+
for (let r = 0; r < 12; r++) {
|
|
19
|
+
let t = new Float64Array(12)
|
|
20
|
+
t[r] = 1; t[(r + 3) % 12] = 1; t[(r + 7) % 12] = 1
|
|
21
|
+
T.push({ root: r, quality: 'min', label: NOTE_NAMES[r] + 'm', vec: t })
|
|
22
|
+
}
|
|
23
|
+
return T
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Classify a single chroma frame as one of 24 major/minor triads
|
|
28
|
+
* via cosine similarity with binary templates (Fujishima, 1999).
|
|
29
|
+
*
|
|
30
|
+
* @param {Float64Array | Float32Array | number[]} chromaVec - length-12 chroma vector
|
|
31
|
+
* @param {{minConfidence?: number}} [params]
|
|
32
|
+
* @returns {{root: number, quality: 'maj'|'min'|'N', label: string, confidence: number}}
|
|
33
|
+
*/
|
|
34
|
+
export default function chord(chromaVec, params) {
|
|
35
|
+
let minConf = params?.minConfidence ?? 0.3
|
|
36
|
+
let cNorm = l2norm(chromaVec)
|
|
37
|
+
if (cNorm === 0) return { root: -1, quality: 'N', label: 'N', confidence: 0 }
|
|
38
|
+
|
|
39
|
+
let best = null, bestSim = -Infinity
|
|
40
|
+
for (let t of TEMPLATES) {
|
|
41
|
+
let sim = cosine(chromaVec, t.vec, cNorm)
|
|
42
|
+
if (sim > bestSim) { bestSim = sim; best = t }
|
|
43
|
+
}
|
|
44
|
+
if (bestSim < minConf) return { root: -1, quality: 'N', label: 'N', confidence: bestSim }
|
|
45
|
+
return { root: best.root, quality: best.quality, label: best.label, confidence: bestSim }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Smooth a sequence of chroma frames into a chord sequence using Viterbi
|
|
50
|
+
* with a sticky self-transition prior. A simple stand-in for Mauch-style
|
|
51
|
+
* context models — works well in practice for short segments.
|
|
52
|
+
*
|
|
53
|
+
* @param {(Float64Array|Float32Array|number[])[]} frames - array of chroma vectors
|
|
54
|
+
* @param {{selfProb?: number}} [params]
|
|
55
|
+
* @returns {{root: number, quality: 'maj'|'min'|'N', label: string}[]}
|
|
56
|
+
*/
|
|
57
|
+
export function smooth(frames, params) {
|
|
58
|
+
let selfProb = params?.selfProb ?? 0.5
|
|
59
|
+
let n = frames.length
|
|
60
|
+
if (n === 0) return []
|
|
61
|
+
let S = TEMPLATES.length // 24 states
|
|
62
|
+
let logSelf = Math.log(selfProb)
|
|
63
|
+
let logSwitch = Math.log((1 - selfProb) / (S - 1))
|
|
64
|
+
|
|
65
|
+
// observation log-likelihoods: cosine similarity mapped to log-prob
|
|
66
|
+
let logObs = (frame) => {
|
|
67
|
+
let norm = l2norm(frame)
|
|
68
|
+
let out = new Float64Array(S)
|
|
69
|
+
if (norm === 0) { out.fill(-1e9); return out }
|
|
70
|
+
for (let i = 0; i < S; i++) {
|
|
71
|
+
let sim = cosine(frame, TEMPLATES[i].vec, norm)
|
|
72
|
+
// map [-1,1] → log-prob; temperature 8 gives reasonably sharp distributions
|
|
73
|
+
out[i] = 8 * sim
|
|
74
|
+
}
|
|
75
|
+
return out
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Viterbi
|
|
79
|
+
let delta = logObs(frames[0])
|
|
80
|
+
let psi = new Array(n)
|
|
81
|
+
for (let t = 1; t < n; t++) {
|
|
82
|
+
let lo = logObs(frames[t])
|
|
83
|
+
let next = new Float64Array(S)
|
|
84
|
+
let back = new Int32Array(S)
|
|
85
|
+
for (let j = 0; j < S; j++) {
|
|
86
|
+
let bestVal = -Infinity, bestI = 0
|
|
87
|
+
for (let i = 0; i < S; i++) {
|
|
88
|
+
let v = delta[i] + (i === j ? logSelf : logSwitch)
|
|
89
|
+
if (v > bestVal) { bestVal = v; bestI = i }
|
|
90
|
+
}
|
|
91
|
+
next[j] = bestVal + lo[j]
|
|
92
|
+
back[j] = bestI
|
|
93
|
+
}
|
|
94
|
+
psi[t] = back
|
|
95
|
+
delta = next
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// backtrace
|
|
99
|
+
let path = new Int32Array(n)
|
|
100
|
+
let last = 0, lastVal = -Infinity
|
|
101
|
+
for (let i = 0; i < S; i++) if (delta[i] > lastVal) { lastVal = delta[i]; last = i }
|
|
102
|
+
path[n - 1] = last
|
|
103
|
+
for (let t = n - 1; t > 0; t--) path[t - 1] = psi[t][path[t]]
|
|
104
|
+
|
|
105
|
+
return [...path].map(i => ({
|
|
106
|
+
root: TEMPLATES[i].root,
|
|
107
|
+
quality: TEMPLATES[i].quality,
|
|
108
|
+
label: TEMPLATES[i].label,
|
|
109
|
+
}))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function l2norm(v) {
|
|
113
|
+
let s = 0
|
|
114
|
+
for (let i = 0; i < 12; i++) s += v[i] * v[i]
|
|
115
|
+
return Math.sqrt(s)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function cosine(a, b, aNorm) {
|
|
119
|
+
let dot = 0, bNorm = 0
|
|
120
|
+
for (let i = 0; i < 12; i++) { dot += a[i] * b[i]; bNorm += b[i] * b[i] }
|
|
121
|
+
bNorm = Math.sqrt(bNorm)
|
|
122
|
+
return aNorm && bNorm ? dot / (aNorm * bNorm) : 0
|
|
123
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/mir-chord",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "24 binary chord templates: C, C#, … B (major) then C, C#, … B (minor)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "chord.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./chord.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"chord.js"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"audio",
|
|
17
|
+
"dsp",
|
|
18
|
+
"mir",
|
|
19
|
+
"chord",
|
|
20
|
+
"chord-detection",
|
|
21
|
+
"viterbi"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Dmitry Iv. <dfcreative@gmail.com>",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|