@audio/mir-key 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/key.js +68 -0
  2. package/package.json +31 -0
package/key.js ADDED
@@ -0,0 +1,68 @@
1
+ const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
2
+
3
+ /**
4
+ * Krumhansl–Kessler major and minor key profiles.
5
+ * From Krumhansl & Kessler (1982), as used in the Krumhansl–Schmuckler
6
+ * key-finding algorithm. Values are perceived stability ratings per scale degree.
7
+ */
8
+ export const KK_MAJOR = [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88]
9
+ export const KK_MINOR = [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17]
10
+
11
+ /**
12
+ * Detect musical key from a chroma vector using the Krumhansl–Schmuckler
13
+ * algorithm: Pearson correlation between the input chroma and each of 24
14
+ * rotated major/minor key profiles. The key with the highest correlation wins.
15
+ *
16
+ * Accepts either a single 12-D chroma vector or an array of frames (in which
17
+ * case the mean across frames is used).
18
+ *
19
+ * @param {(Float64Array | Float32Array | number[]) | (Float64Array | Float32Array | number[])[]} input
20
+ * @param {{profile?: {major: number[], minor: number[]}}} [params]
21
+ * @returns {{tonic: number, mode: 'major'|'minor', label: string, confidence: number, scores: {label: string, score: number}[]}}
22
+ */
23
+ export default function key(input, params) {
24
+ let profile = params?.profile ?? { major: KK_MAJOR, minor: KK_MINOR }
25
+
26
+ // accept single vector or sequence of frames
27
+ let v = new Float64Array(12)
28
+ if (Array.isArray(input) && input.length && typeof input[0] !== 'number') {
29
+ for (let f of input) for (let i = 0; i < 12; i++) v[i] += f[i]
30
+ for (let i = 0; i < 12; i++) v[i] /= input.length
31
+ } else {
32
+ for (let i = 0; i < 12; i++) v[i] = input[i]
33
+ }
34
+
35
+ let scores = []
36
+ for (let r = 0; r < 12; r++) {
37
+ scores.push({ label: NOTE_NAMES[r], score: pearson(v, rotate(profile.major, r)) })
38
+ scores.push({ label: NOTE_NAMES[r] + 'm', score: pearson(v, rotate(profile.minor, r)) })
39
+ }
40
+ scores.sort((a, b) => b.score - a.score)
41
+
42
+ let best = scores[0]
43
+ let mode = best.label.endsWith('m') ? 'minor' : 'major'
44
+ let tonic = NOTE_NAMES.indexOf(mode === 'minor' ? best.label.slice(0, -1) : best.label)
45
+ return { tonic, mode, label: best.label, confidence: best.score, scores }
46
+ }
47
+
48
+ function rotate(arr, r) {
49
+ let out = new Array(12)
50
+ for (let i = 0; i < 12; i++) out[i] = arr[(i - r + 12) % 12]
51
+ return out
52
+ }
53
+
54
+ function pearson(a, b) {
55
+ let n = 12
56
+ let ma = 0, mb = 0
57
+ for (let i = 0; i < n; i++) { ma += a[i]; mb += b[i] }
58
+ ma /= n; mb /= n
59
+ let num = 0, da = 0, db = 0
60
+ for (let i = 0; i < n; i++) {
61
+ let ax = a[i] - ma, bx = b[i] - mb
62
+ num += ax * bx
63
+ da += ax * ax
64
+ db += bx * bx
65
+ }
66
+ let denom = Math.sqrt(da * db)
67
+ return denom === 0 ? 0 : num / denom
68
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@audio/mir-key",
3
+ "version": "1.0.0",
4
+ "description": "Krumhansl–Kessler major and minor key profiles",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "key.js",
8
+ "exports": {
9
+ ".": "./key.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "key.js"
14
+ ],
15
+ "keywords": [
16
+ "audio",
17
+ "dsp",
18
+ "mir",
19
+ "key",
20
+ "key-detection",
21
+ "krumhansl"
22
+ ],
23
+ "license": "MIT",
24
+ "author": "Dmitry Iv. <dfcreative@gmail.com>",
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }