note-mapper 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/index.js +57 -0
- package/package.json +19 -0
- package/src/degrees.js +147 -0
- package/src/intervals.js +138 -0
- package/src/mapper.js +254 -0
- package/src/scales.js +301 -0
package/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.js
|
|
3
|
+
* Main entry point for note-mapper.
|
|
4
|
+
* Re-exports everything from all modules so consumers
|
|
5
|
+
* only need one import.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { SCALES, createMapper, ACTIONS_5 } from "note-mapper"
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// scales
|
|
12
|
+
export {
|
|
13
|
+
SCALES,
|
|
14
|
+
KEYS,
|
|
15
|
+
MAJOR,
|
|
16
|
+
NATURAL_MINOR,
|
|
17
|
+
PENTATONIC_MINOR,
|
|
18
|
+
PENTATONIC_MAJOR,
|
|
19
|
+
BLUES,
|
|
20
|
+
DORIAN,
|
|
21
|
+
PHRYGIAN,
|
|
22
|
+
LYDIAN,
|
|
23
|
+
MIXOLYDIAN,
|
|
24
|
+
LOCRIAN,
|
|
25
|
+
HARMONIC_MINOR,
|
|
26
|
+
MELODIC_MINOR,
|
|
27
|
+
PHRYGIAN_DOMINANT,
|
|
28
|
+
WHOLE_TONE,
|
|
29
|
+
DIMINISHED,
|
|
30
|
+
} from "./scales.js"
|
|
31
|
+
|
|
32
|
+
// degrees
|
|
33
|
+
export {
|
|
34
|
+
getDegree,
|
|
35
|
+
getDegreeWithTolerance,
|
|
36
|
+
degreeToNote,
|
|
37
|
+
isInScale,
|
|
38
|
+
pitchClass,
|
|
39
|
+
} from "./degrees.js"
|
|
40
|
+
|
|
41
|
+
// intervals
|
|
42
|
+
export {
|
|
43
|
+
INTERVALS,
|
|
44
|
+
getInterval,
|
|
45
|
+
getIntervalInfo,
|
|
46
|
+
detectInterval,
|
|
47
|
+
isInterval,
|
|
48
|
+
} from "./intervals.js"
|
|
49
|
+
|
|
50
|
+
// mapper
|
|
51
|
+
export {
|
|
52
|
+
createMapper,
|
|
53
|
+
ACTIONS_5,
|
|
54
|
+
ACTIONS_6,
|
|
55
|
+
ACTIONS_7,
|
|
56
|
+
ACTIONS_INTERVAL,
|
|
57
|
+
} from "./mapper.js"
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "note-mapper",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Map guitar notes from note-listener to scale degrees, intervals and game actions",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["guitar", "music", "pitch", "scale", "interval", "game-controller"],
|
|
13
|
+
"author": "Donald Edwin",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"note-listener": "*"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/degrees.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* degrees.js
|
|
3
|
+
* Resolves a note from note-listener into a scale degree.
|
|
4
|
+
* A degree is just the position of the note in the scale — 0 is the root.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ─── Enharmonic equivalents ──────────────────────────────────────────────────
|
|
8
|
+
// Some notes have two names (C# = Db, F# = Gb etc).
|
|
9
|
+
// This map normalizes them so lookups always work regardless of which
|
|
10
|
+
// name the scale uses vs which name note-listener detected.
|
|
11
|
+
|
|
12
|
+
const ENHARMONIC = {
|
|
13
|
+
"Db": "C#",
|
|
14
|
+
"D#": "Eb",
|
|
15
|
+
"Gb": "F#",
|
|
16
|
+
"G#": "Ab",
|
|
17
|
+
"A#": "Bb",
|
|
18
|
+
// and the reverse
|
|
19
|
+
"C#": "Db",
|
|
20
|
+
"Eb": "D#",
|
|
21
|
+
"F#": "Gb",
|
|
22
|
+
"Ab": "G#",
|
|
23
|
+
"Bb": "A#",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Utilities ───────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Strips the octave number from a note string.
|
|
30
|
+
* "A2" → "A", "C#3" → "C#", "Bb4" → "Bb"
|
|
31
|
+
*/
|
|
32
|
+
export function pitchClass(note) {
|
|
33
|
+
return note.replace(/\d/g, "")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Looks up a pitch class in a scale, trying both its name
|
|
38
|
+
* and its enharmonic equivalent.
|
|
39
|
+
* Returns the index (degree) or -1 if not found.
|
|
40
|
+
*/
|
|
41
|
+
function findInScale(pc, scale) {
|
|
42
|
+
const direct = scale.indexOf(pc)
|
|
43
|
+
if (direct !== -1) return direct
|
|
44
|
+
|
|
45
|
+
const alt = ENHARMONIC[pc]
|
|
46
|
+
if (alt) return scale.indexOf(alt)
|
|
47
|
+
|
|
48
|
+
return -1
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Main exports ────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns the scale degree (0-based) of a note within a scale.
|
|
55
|
+
* Returns null if the note is not in the scale.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} note - note string from note-listener e.g. "A2"
|
|
58
|
+
* @param {string[]} scale - scale array from scales.js e.g. ["A","C","D","E","G"]
|
|
59
|
+
* @returns {number|null}
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* getDegree("A2", ["A","C","D","E","G"]) // → 0 (root)
|
|
63
|
+
* getDegree("E3", ["A","C","D","E","G"]) // → 3
|
|
64
|
+
* getDegree("F#2",["A","C","D","E","G"]) // → null (not in scale)
|
|
65
|
+
*/
|
|
66
|
+
export function getDegree(note, scale) {
|
|
67
|
+
const pc = pitchClass(note)
|
|
68
|
+
const degree = findInScale(pc, scale)
|
|
69
|
+
return degree === -1 ? null : degree
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Same as getDegree but with cents tolerance.
|
|
74
|
+
* Useful when the player is slightly out of tune —
|
|
75
|
+
* accepts the note if it's within centsTolerance cents of a scale degree.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} note - note string from note-listener e.g. "A2"
|
|
78
|
+
* @param {number} freq - frequency in Hz from note-listener
|
|
79
|
+
* @param {string[]} scale - scale array from scales.js
|
|
80
|
+
* @param {number} centsTolerance - how many cents off to still accept (default 40)
|
|
81
|
+
* @returns {number|null}
|
|
82
|
+
*/
|
|
83
|
+
export function getDegreeWithTolerance(note, freq, scale, centsTolerance = 40) {
|
|
84
|
+
// first try exact match
|
|
85
|
+
const exact = getDegree(note, scale)
|
|
86
|
+
if (exact !== null) return exact
|
|
87
|
+
|
|
88
|
+
// if no exact match, check if freq is close enough to any scale degree
|
|
89
|
+
for (let i = 0; i < scale.length; i++) {
|
|
90
|
+
const targetFreq = noteNameToFreq(scale[i])
|
|
91
|
+
if (!targetFreq) continue
|
|
92
|
+
|
|
93
|
+
// check across multiple octaves (2 through 6)
|
|
94
|
+
for (let octave = 2; octave <= 6; octave++) {
|
|
95
|
+
const octaveFreq = targetFreq * Math.pow(2, octave - 4)
|
|
96
|
+
const cents = Math.abs(1200 * Math.log2(freq / octaveFreq))
|
|
97
|
+
if (cents <= centsTolerance) return i
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Converts a pitch class name to its frequency in octave 4.
|
|
106
|
+
* Used internally by getDegreeWithTolerance.
|
|
107
|
+
*/
|
|
108
|
+
function noteNameToFreq(pc) {
|
|
109
|
+
const A4 = 440
|
|
110
|
+
const NOTE_NAMES = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"]
|
|
111
|
+
|
|
112
|
+
// normalize enharmonics to sharps for lookup
|
|
113
|
+
const normalized = ENHARMONIC[pc] && NOTE_NAMES.includes(ENHARMONIC[pc])
|
|
114
|
+
? ENHARMONIC[pc]
|
|
115
|
+
: pc
|
|
116
|
+
|
|
117
|
+
const semitone = NOTE_NAMES.indexOf(normalized)
|
|
118
|
+
if (semitone === -1) return null
|
|
119
|
+
|
|
120
|
+
// semitones from A4 (midi 69) → freq
|
|
121
|
+
const midi = semitone + 60 // C4 = midi 60
|
|
122
|
+
return A4 * Math.pow(2, (midi - 69) / 12)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns the note name for a given degree in a scale.
|
|
127
|
+
* The reverse of getDegree — useful for displaying what note to play next.
|
|
128
|
+
*
|
|
129
|
+
* @param {number} degree - 0-based degree index
|
|
130
|
+
* @param {string[]} scale - scale array from scales.js
|
|
131
|
+
* @returns {string|null}
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* degreeToNote(0, ["A","C","D","E","G"]) // → "A"
|
|
135
|
+
* degreeToNote(3, ["A","C","D","E","G"]) // → "E"
|
|
136
|
+
*/
|
|
137
|
+
export function degreeToNote(degree, scale) {
|
|
138
|
+
return scale[degree] ?? null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Returns true if a note is in the scale, false if not.
|
|
143
|
+
* Useful for filtering out notes that don't belong before mapping to actions.
|
|
144
|
+
*/
|
|
145
|
+
export function isInScale(note, scale) {
|
|
146
|
+
return getDegree(note, scale) !== null
|
|
147
|
+
}
|
package/src/intervals.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* intervals.js
|
|
3
|
+
* Detects and names the interval between two simultaneously detected notes.
|
|
4
|
+
* Works with the notes array from note-listener's onNotes callback.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ─── Interval definitions ────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export const INTERVALS = {
|
|
10
|
+
0: { name: "Unison", short: "P1", character: "same note" },
|
|
11
|
+
1: { name: "Minor 2nd", short: "m2", character: "tense, dissonant" },
|
|
12
|
+
2: { name: "Major 2nd", short: "M2", character: "stepwise, mild" },
|
|
13
|
+
3: { name: "Minor 3rd", short: "m3", character: "minor, sad" },
|
|
14
|
+
4: { name: "Major 3rd", short: "M3", character: "major, bright" },
|
|
15
|
+
5: { name: "Perfect 4th", short: "P4", character: "open, suspended" },
|
|
16
|
+
6: { name: "Tritone", short: "TT", character: "maximum tension" },
|
|
17
|
+
7: { name: "Perfect 5th", short: "P5", character: "power chord, strong" },
|
|
18
|
+
8: { name: "Minor 6th", short: "m6", character: "melancholic" },
|
|
19
|
+
9: { name: "Major 6th", short: "M6", character: "warm, resolved" },
|
|
20
|
+
10: { name: "Minor 7th", short: "m7", character: "bluesy, unresolved" },
|
|
21
|
+
11: { name: "Major 7th", short: "M7", character: "jazzy, tense" },
|
|
22
|
+
12: { name: "Octave", short: "P8", character: "same note reinforced" },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── Utilities ───────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Converts a frequency to a MIDI note number (not rounded — keeps decimals).
|
|
29
|
+
* Used for interval calculation so we work in semitone space.
|
|
30
|
+
*/
|
|
31
|
+
function freqToMidi(freq) {
|
|
32
|
+
return 12 * Math.log2(freq / 440) + 69
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Main exports ────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns the interval in semitones between two frequencies.
|
|
39
|
+
* Always returns a positive number between 0 and 12 (octave-reduced).
|
|
40
|
+
*
|
|
41
|
+
* @param {number} freqA
|
|
42
|
+
* @param {number} freqB
|
|
43
|
+
* @returns {number} semitones 0–12
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* getInterval(440, 660) // → 7 (perfect 5th, A4 to E5)
|
|
47
|
+
* getInterval(440, 880) // → 12 (octave)
|
|
48
|
+
*/
|
|
49
|
+
export function getInterval(freqA, freqB) {
|
|
50
|
+
const semitones = Math.abs(freqToMidi(freqA) - freqToMidi(freqB))
|
|
51
|
+
return Math.round(semitones) % 12 || 12
|
|
52
|
+
// the || 12 means: if mod 12 gives 0 (an octave), return 12 not 0
|
|
53
|
+
// so unison (same note) = 0, octave = 12, everything else 1–11
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Returns the full interval info object for two frequencies.
|
|
58
|
+
*
|
|
59
|
+
* @param {number} freqA
|
|
60
|
+
* @param {number} freqB
|
|
61
|
+
* @returns {{ semitones, name, short, character }}
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* getIntervalInfo(440, 660)
|
|
65
|
+
* // → { semitones: 7, name: "Perfect 5th", short: "P5", character: "power chord, strong" }
|
|
66
|
+
*/
|
|
67
|
+
export function getIntervalInfo(freqA, freqB) {
|
|
68
|
+
const semitones = getInterval(freqA, freqB)
|
|
69
|
+
return { semitones, ...INTERVALS[semitones] }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Detects an interval from the notes array given by note-listener's onNotes.
|
|
74
|
+
* Only looks at notes with isOnset: true so held notes don't re-trigger.
|
|
75
|
+
*
|
|
76
|
+
* Uses a timing window — notes don't have to arrive in the exact same frame,
|
|
77
|
+
* just within windowMs milliseconds of each other.
|
|
78
|
+
*
|
|
79
|
+
* @param {Array} notes - the notes array from onNotes
|
|
80
|
+
* @param {Array} history - a mutable array you maintain across calls (pass [] initially)
|
|
81
|
+
* @param {number} windowMs - how long to wait for a second note (default 150ms)
|
|
82
|
+
* @returns {{ semitones, name, short, character, notes } | null}
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* const history = []
|
|
86
|
+
*
|
|
87
|
+
* onNotes(notes) {
|
|
88
|
+
* const interval = detectInterval(notes, history)
|
|
89
|
+
* if (interval) console.log(interval.name) // "Perfect 5th"
|
|
90
|
+
* }
|
|
91
|
+
*/
|
|
92
|
+
export function detectInterval(notes, history, windowMs = 150) {
|
|
93
|
+
const now = Date.now()
|
|
94
|
+
|
|
95
|
+
// add any new onsets to the history buffer
|
|
96
|
+
for (const n of notes) {
|
|
97
|
+
if (n.isOnset) {
|
|
98
|
+
history.push({ freq: n.freq, note: n.note, time: now })
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// remove entries older than the window
|
|
103
|
+
while (history.length && now - history[0].time > windowMs) {
|
|
104
|
+
history.shift()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// if we have at least two notes in the window, we have an interval
|
|
108
|
+
if (history.length >= 2) {
|
|
109
|
+
const a = history[0]
|
|
110
|
+
const b = history[1]
|
|
111
|
+
const info = getIntervalInfo(a.freq, b.freq)
|
|
112
|
+
|
|
113
|
+
// clear history so we don't keep re-firing the same interval
|
|
114
|
+
history.length = 0
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
...info,
|
|
118
|
+
notes: [a.note, b.note],
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Checks if a detected interval matches a named interval.
|
|
127
|
+
* Useful for if/else game logic without dealing with semitone numbers.
|
|
128
|
+
*
|
|
129
|
+
* @param {number} semitones - from getInterval()
|
|
130
|
+
* @param {string} name - interval short name e.g. "P5", "m3", "TT"
|
|
131
|
+
* @returns {boolean}
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* if (isInterval(semitones, "P5")) triggerAction("power_attack")
|
|
135
|
+
*/
|
|
136
|
+
export function isInterval(semitones, name) {
|
|
137
|
+
return INTERVALS[semitones]?.short === name
|
|
138
|
+
}
|
package/src/mapper.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mapper.js
|
|
3
|
+
* Maps scale degrees and intervals to game actions.
|
|
4
|
+
* This is the final layer — it takes what degrees.js and intervals.js
|
|
5
|
+
* give you and turns them into something your game can act on.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─── Default action maps ─────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
// 5 note pentatonic — good for simple games
|
|
11
|
+
export const ACTIONS_5 = {
|
|
12
|
+
0: "jump",
|
|
13
|
+
1: "move_left",
|
|
14
|
+
2: "move_right",
|
|
15
|
+
3: "attack",
|
|
16
|
+
4: "shield",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 6 note blues — adds one extra action
|
|
20
|
+
export const ACTIONS_6 = {
|
|
21
|
+
0: "jump",
|
|
22
|
+
1: "move_left",
|
|
23
|
+
2: "move_right",
|
|
24
|
+
3: "attack",
|
|
25
|
+
4: "shield",
|
|
26
|
+
5: "special",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 7 note diatonic — full action set
|
|
30
|
+
export const ACTIONS_7 = {
|
|
31
|
+
0: "jump",
|
|
32
|
+
1: "move_left",
|
|
33
|
+
2: "move_right",
|
|
34
|
+
3: "attack",
|
|
35
|
+
4: "shield",
|
|
36
|
+
5: "special",
|
|
37
|
+
6: "ultimate",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// interval-based actions — two notes played together
|
|
41
|
+
export const ACTIONS_INTERVAL = {
|
|
42
|
+
7: "power_attack", // perfect 5th — power chord
|
|
43
|
+
3: "block", // minor 3rd
|
|
44
|
+
4: "dodge", // major 3rd
|
|
45
|
+
12: "ultimate", // octave
|
|
46
|
+
5: "special", // perfect 4th
|
|
47
|
+
6: "taunt", // tritone — maximum tension
|
|
48
|
+
10: "roll", // minor 7th
|
|
49
|
+
9: "heal", // major 6th
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Main exports ────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a mapper instance that connects note-listener output
|
|
56
|
+
* to game actions. This is the main thing you use in your app.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} opts
|
|
59
|
+
* @param {string[]} opts.scale - scale array from scales.js
|
|
60
|
+
* @param {object} [opts.degreeActions] - degree → action map (default ACTIONS_7)
|
|
61
|
+
* @param {object} [opts.intervalActions] - semitones → action map (default ACTIONS_INTERVAL)
|
|
62
|
+
* @param {number} [opts.centsTolerance] - how many cents off to still accept (default 40)
|
|
63
|
+
* @param {number} [opts.intervalWindow] - ms window for interval detection (default 150)
|
|
64
|
+
* @param {function} opts.onAction - fires when any action is triggered
|
|
65
|
+
*
|
|
66
|
+
* @returns {{ process, setScale, setDegreeActions, setIntervalActions }}
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* const mapper = createMapper({
|
|
70
|
+
* scale: SCALES["natural_minor"]["A"],
|
|
71
|
+
* onAction(action, meta) {
|
|
72
|
+
* console.log(action) // "jump", "power_attack" etc
|
|
73
|
+
* console.log(meta) // { note, freq, degree, interval, isOnset }
|
|
74
|
+
* }
|
|
75
|
+
* })
|
|
76
|
+
*
|
|
77
|
+
* // inside note-listener's onNotes:
|
|
78
|
+
* onNotes(notes) {
|
|
79
|
+
* mapper.process(notes)
|
|
80
|
+
* }
|
|
81
|
+
*/
|
|
82
|
+
export function createMapper({
|
|
83
|
+
scale,
|
|
84
|
+
degreeActions = ACTIONS_7,
|
|
85
|
+
intervalActions = ACTIONS_INTERVAL,
|
|
86
|
+
centsTolerance = 40,
|
|
87
|
+
intervalWindow = 150,
|
|
88
|
+
onAction,
|
|
89
|
+
} = {}) {
|
|
90
|
+
let _scale = scale
|
|
91
|
+
let _degreeActions = degreeActions
|
|
92
|
+
let _intervalActions = intervalActions
|
|
93
|
+
let _intervalHistory = []
|
|
94
|
+
|
|
95
|
+
// ── onset gate ────────────────────────────────────────────────────────────
|
|
96
|
+
// tracks which notes are currently held so we only fire on the attack,
|
|
97
|
+
// not continuously while the note rings
|
|
98
|
+
const _heldNotes = new Set()
|
|
99
|
+
|
|
100
|
+
function processOnsets(notes) {
|
|
101
|
+
const onsets = []
|
|
102
|
+
|
|
103
|
+
for (const n of notes) {
|
|
104
|
+
if (n.isOnset && !_heldNotes.has(n.note)) {
|
|
105
|
+
_heldNotes.add(n.note)
|
|
106
|
+
onsets.push(n)
|
|
107
|
+
}
|
|
108
|
+
// if a note is no longer in the detected list, release it
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// release notes that stopped sounding
|
|
112
|
+
const currentNotes = new Set(notes.map(n => n.note))
|
|
113
|
+
for (const held of _heldNotes) {
|
|
114
|
+
if (!currentNotes.has(held)) _heldNotes.delete(held)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return onsets
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── interval detection ────────────────────────────────────────────────────
|
|
121
|
+
function processInterval(notes) {
|
|
122
|
+
const now = Date.now()
|
|
123
|
+
|
|
124
|
+
for (const n of notes) {
|
|
125
|
+
if (n.isOnset) {
|
|
126
|
+
_intervalHistory.push({ freq: n.freq, note: n.note, time: now })
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// remove old entries outside the window
|
|
131
|
+
while (_intervalHistory.length && now - _intervalHistory[0].time > intervalWindow) {
|
|
132
|
+
_intervalHistory.shift()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (_intervalHistory.length >= 2) {
|
|
136
|
+
const a = _intervalHistory[0]
|
|
137
|
+
const b = _intervalHistory[1]
|
|
138
|
+
|
|
139
|
+
const semitones = Math.round(
|
|
140
|
+
Math.abs(12 * Math.log2(a.freq / b.freq))
|
|
141
|
+
) % 12 || 12
|
|
142
|
+
|
|
143
|
+
_intervalHistory.length = 0
|
|
144
|
+
return { semitones, notes: [a.note, b.note] }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── degree detection ──────────────────────────────────────────────────────
|
|
151
|
+
function processDegree(note, freq) {
|
|
152
|
+
const pc = note.replace(/\d/g, "")
|
|
153
|
+
|
|
154
|
+
// try direct match first
|
|
155
|
+
let degree = _scale.indexOf(pc)
|
|
156
|
+
|
|
157
|
+
// try enharmonic equivalent
|
|
158
|
+
if (degree === -1) {
|
|
159
|
+
const ENHARMONIC = {
|
|
160
|
+
"Db":"C#","D#":"Eb","Gb":"F#","G#":"Ab","A#":"Bb",
|
|
161
|
+
"C#":"Db","Eb":"D#","F#":"Gb","Ab":"G#","Bb":"A#",
|
|
162
|
+
}
|
|
163
|
+
const alt = ENHARMONIC[pc]
|
|
164
|
+
if (alt) degree = _scale.indexOf(alt)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// try cents tolerance if still not found
|
|
168
|
+
if (degree === -1 && freq) {
|
|
169
|
+
for (let i = 0; i < _scale.length; i++) {
|
|
170
|
+
for (let octave = 2; octave <= 6; octave++) {
|
|
171
|
+
const midi = (_scale.indexOf(_scale[i]) + 60)
|
|
172
|
+
const tFreq = 440 * Math.pow(2, (midi - 69) / 12) * Math.pow(2, octave - 4)
|
|
173
|
+
const cents = Math.abs(1200 * Math.log2(freq / tFreq))
|
|
174
|
+
if (cents <= centsTolerance) { degree = i; break }
|
|
175
|
+
}
|
|
176
|
+
if (degree !== -1) break
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return degree === -1 ? null : degree
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── main process function ─────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Call this inside note-listener's onNotes with the notes array.
|
|
187
|
+
* It will fire onAction for every detected action.
|
|
188
|
+
*
|
|
189
|
+
* @param {Array} notes - the notes array from note-listener's onNotes
|
|
190
|
+
*/
|
|
191
|
+
function process(notes) {
|
|
192
|
+
if (!notes?.length) return
|
|
193
|
+
|
|
194
|
+
// --- interval check (two notes together) ---
|
|
195
|
+
const interval = processInterval(notes)
|
|
196
|
+
if (interval) {
|
|
197
|
+
const action = _intervalActions[interval.semitones]
|
|
198
|
+
if (action && onAction) {
|
|
199
|
+
onAction(action, {
|
|
200
|
+
type: "interval",
|
|
201
|
+
interval: interval.semitones,
|
|
202
|
+
notes: interval.notes,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --- single note degree check ---
|
|
208
|
+
const onsets = processOnsets(notes)
|
|
209
|
+
for (const n of onsets) {
|
|
210
|
+
const degree = processDegree(n.note, n.freq)
|
|
211
|
+
if (degree === null) continue
|
|
212
|
+
|
|
213
|
+
const action = _degreeActions[degree]
|
|
214
|
+
if (action && onAction) {
|
|
215
|
+
onAction(action, {
|
|
216
|
+
type: "degree",
|
|
217
|
+
degree,
|
|
218
|
+
note: n.note,
|
|
219
|
+
freq: n.freq,
|
|
220
|
+
cents: n.cents,
|
|
221
|
+
energy: n.energy,
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── public API ────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
process,
|
|
231
|
+
|
|
232
|
+
/** swap the scale at runtime e.g. when player changes key */
|
|
233
|
+
setScale(newScale) {
|
|
234
|
+
_scale = newScale
|
|
235
|
+
_heldNotes.clear()
|
|
236
|
+
_intervalHistory.length = 0
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
/** swap the degree action map at runtime */
|
|
240
|
+
setDegreeActions(newActions) {
|
|
241
|
+
_degreeActions = newActions
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
/** swap the interval action map at runtime */
|
|
245
|
+
setIntervalActions(newActions) {
|
|
246
|
+
_intervalActions = newActions
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
/** returns which notes are currently being held */
|
|
250
|
+
getHeldNotes() {
|
|
251
|
+
return [..._heldNotes]
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
}
|
package/src/scales.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scales.js
|
|
3
|
+
* Complete scale definitions for all 12 keys.
|
|
4
|
+
* Each scale is an ordered array of pitch classes (no octave numbers).
|
|
5
|
+
* The order defines the degree — index 0 is always the root.
|
|
6
|
+
*
|
|
7
|
+
* Keys: C, C#, D, Eb, E, F, F#, G, Ab, A, Bb, B
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ─── Pentatonic Minor (5 notes) ──────────────────────────────────────────────
|
|
11
|
+
// Formula: 1 b3 4 5 b7
|
|
12
|
+
|
|
13
|
+
export const PENTATONIC_MINOR = {
|
|
14
|
+
"C": ["C", "Eb", "F", "G", "Bb"],
|
|
15
|
+
"C#": ["C#", "E", "F#", "G#", "B"],
|
|
16
|
+
"D": ["D", "F", "G", "A", "C"],
|
|
17
|
+
"Eb": ["Eb", "Gb", "Ab", "Bb", "Db"],
|
|
18
|
+
"E": ["E", "G", "A", "B", "D"],
|
|
19
|
+
"F": ["F", "Ab", "Bb", "C", "Eb"],
|
|
20
|
+
"F#": ["F#", "A", "B", "C#", "E"],
|
|
21
|
+
"G": ["G", "Bb", "C", "D", "F"],
|
|
22
|
+
"Ab": ["Ab", "B", "Db", "Eb", "Gb"],
|
|
23
|
+
"A": ["A", "C", "D", "E", "G"],
|
|
24
|
+
"Bb": ["Bb", "Db", "Eb", "F", "Ab"],
|
|
25
|
+
"B": ["B", "D", "E", "F#", "A"],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Pentatonic Major (5 notes) ──────────────────────────────────────────────
|
|
29
|
+
// Formula: 1 2 3 5 6
|
|
30
|
+
|
|
31
|
+
export const PENTATONIC_MAJOR = {
|
|
32
|
+
"C": ["C", "D", "E", "G", "A"],
|
|
33
|
+
"C#": ["C#", "D#", "F", "G#", "A#"],
|
|
34
|
+
"D": ["D", "E", "F#", "A", "B"],
|
|
35
|
+
"Eb": ["Eb", "F", "G", "Bb", "C"],
|
|
36
|
+
"E": ["E", "F#", "G#", "B", "C#"],
|
|
37
|
+
"F": ["F", "G", "A", "C", "D"],
|
|
38
|
+
"F#": ["F#", "G#", "A#", "C#", "D#"],
|
|
39
|
+
"G": ["G", "A", "B", "D", "E"],
|
|
40
|
+
"Ab": ["Ab", "Bb", "C", "Eb", "F"],
|
|
41
|
+
"A": ["A", "B", "C#", "E", "F#"],
|
|
42
|
+
"Bb": ["Bb", "C", "D", "F", "G"],
|
|
43
|
+
"B": ["B", "C#", "D#", "F#", "G#"],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Blues (6 notes) ─────────────────────────────────────────────────────────
|
|
47
|
+
// Formula: 1 b3 4 b5 5 b7
|
|
48
|
+
|
|
49
|
+
export const BLUES = {
|
|
50
|
+
"C": ["C", "Eb", "F", "Gb", "G", "Bb"],
|
|
51
|
+
"C#": ["C#", "E", "F#", "G", "G#", "B"],
|
|
52
|
+
"D": ["D", "F", "G", "Ab", "A", "C"],
|
|
53
|
+
"Eb": ["Eb", "Gb", "Ab", "A", "Bb", "Db"],
|
|
54
|
+
"E": ["E", "G", "A", "Bb", "B", "D"],
|
|
55
|
+
"F": ["F", "Ab", "Bb", "B", "C", "Eb"],
|
|
56
|
+
"F#": ["F#", "A", "B", "C", "C#", "E"],
|
|
57
|
+
"G": ["G", "Bb", "C", "Db", "D", "F"],
|
|
58
|
+
"Ab": ["Ab", "B", "Db", "D", "Eb", "Gb"],
|
|
59
|
+
"A": ["A", "C", "D", "Eb", "E", "G"],
|
|
60
|
+
"Bb": ["Bb", "Db", "Eb", "E", "F", "Ab"],
|
|
61
|
+
"B": ["B", "D", "E", "F", "F#", "A"],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Major / Ionian (7 notes) ────────────────────────────────────────────────
|
|
65
|
+
// Formula: 1 2 3 4 5 6 7
|
|
66
|
+
|
|
67
|
+
export const MAJOR = {
|
|
68
|
+
"C": ["C", "D", "E", "F", "G", "A", "B"],
|
|
69
|
+
"C#": ["C#", "D#", "F", "F#", "G#", "A#", "C"],
|
|
70
|
+
"D": ["D", "E", "F#", "G", "A", "B", "C#"],
|
|
71
|
+
"Eb": ["Eb", "F", "G", "Ab", "Bb", "C", "D"],
|
|
72
|
+
"E": ["E", "F#", "G#", "A", "B", "C#", "D#"],
|
|
73
|
+
"F": ["F", "G", "A", "Bb", "C", "D", "E"],
|
|
74
|
+
"F#": ["F#", "G#", "A#", "B", "C#", "D#", "F"],
|
|
75
|
+
"G": ["G", "A", "B", "C", "D", "E", "F#"],
|
|
76
|
+
"Ab": ["Ab", "Bb", "C", "Db", "Eb", "F", "G"],
|
|
77
|
+
"A": ["A", "B", "C#", "D", "E", "F#", "G#"],
|
|
78
|
+
"Bb": ["Bb", "C", "D", "Eb", "F", "G", "A"],
|
|
79
|
+
"B": ["B", "C#", "D#", "E", "F#", "G#", "A#"],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Natural Minor / Aeolian (7 notes) ───────────────────────────────────────
|
|
83
|
+
// Formula: 1 2 b3 4 5 b6 b7
|
|
84
|
+
|
|
85
|
+
export const NATURAL_MINOR = {
|
|
86
|
+
"C": ["C", "D", "Eb", "F", "G", "Ab", "Bb"],
|
|
87
|
+
"C#": ["C#", "D#", "E", "F#", "G#", "A", "B"],
|
|
88
|
+
"D": ["D", "E", "F", "G", "A", "Bb", "C"],
|
|
89
|
+
"Eb": ["Eb", "F", "Gb", "Ab", "Bb", "B", "Db"],
|
|
90
|
+
"E": ["E", "F#", "G", "A", "B", "C", "D"],
|
|
91
|
+
"F": ["F", "G", "Ab", "Bb", "C", "Db", "Eb"],
|
|
92
|
+
"F#": ["F#", "G#", "A", "B", "C#", "D", "E"],
|
|
93
|
+
"G": ["G", "A", "Bb", "C", "D", "Eb", "F"],
|
|
94
|
+
"Ab": ["Ab", "Bb", "B", "Db", "Eb", "E", "Gb"],
|
|
95
|
+
"A": ["A", "B", "C", "D", "E", "F", "G"],
|
|
96
|
+
"Bb": ["Bb", "C", "Db", "Eb", "F", "Gb", "Ab"],
|
|
97
|
+
"B": ["B", "C#", "D", "E", "F#", "G", "A"],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── Dorian (7 notes) ────────────────────────────────────────────────────────
|
|
101
|
+
// Formula: 1 2 b3 4 5 6 b7
|
|
102
|
+
|
|
103
|
+
export const DORIAN = {
|
|
104
|
+
"C": ["C", "D", "Eb", "F", "G", "A", "Bb"],
|
|
105
|
+
"C#": ["C#", "D#", "E", "F#", "G#", "A#", "B"],
|
|
106
|
+
"D": ["D", "E", "F", "G", "A", "B", "C"],
|
|
107
|
+
"Eb": ["Eb", "F", "Gb", "Ab", "Bb", "C", "Db"],
|
|
108
|
+
"E": ["E", "F#", "G", "A", "B", "C#", "D"],
|
|
109
|
+
"F": ["F", "G", "Ab", "Bb", "C", "D", "Eb"],
|
|
110
|
+
"F#": ["F#", "G#", "A", "B", "C#", "D#", "E"],
|
|
111
|
+
"G": ["G", "A", "Bb", "C", "D", "E", "F"],
|
|
112
|
+
"Ab": ["Ab", "Bb", "B", "Db", "Eb", "F", "Gb"],
|
|
113
|
+
"A": ["A", "B", "C", "D", "E", "F#", "G"],
|
|
114
|
+
"Bb": ["Bb", "C", "Db", "Eb", "F", "G", "Ab"],
|
|
115
|
+
"B": ["B", "C#", "D", "E", "F#", "G#", "A"],
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Phrygian (7 notes) ──────────────────────────────────────────────────────
|
|
119
|
+
// Formula: 1 b2 b3 4 5 b6 b7
|
|
120
|
+
|
|
121
|
+
export const PHRYGIAN = {
|
|
122
|
+
"C": ["C", "Db", "Eb", "F", "G", "Ab", "Bb"],
|
|
123
|
+
"C#": ["C#", "D", "E", "F#", "G#", "A", "B"],
|
|
124
|
+
"D": ["D", "Eb", "F", "G", "A", "Bb", "C"],
|
|
125
|
+
"Eb": ["Eb", "E", "Gb", "Ab", "Bb", "B", "Db"],
|
|
126
|
+
"E": ["E", "F", "G", "A", "B", "C", "D"],
|
|
127
|
+
"F": ["F", "Gb", "Ab", "Bb", "C", "Db", "Eb"],
|
|
128
|
+
"F#": ["F#", "G", "A", "B", "C#", "D", "E"],
|
|
129
|
+
"G": ["G", "Ab", "Bb", "C", "D", "Eb", "F"],
|
|
130
|
+
"Ab": ["Ab", "A", "B", "Db", "Eb", "E", "Gb"],
|
|
131
|
+
"A": ["A", "Bb", "C", "D", "E", "F", "G"],
|
|
132
|
+
"Bb": ["Bb", "B", "Db", "Eb", "F", "Gb", "Ab"],
|
|
133
|
+
"B": ["B", "C", "D", "E", "F#", "G", "A"],
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Lydian (7 notes) ────────────────────────────────────────────────────────
|
|
137
|
+
// Formula: 1 2 3 #4 5 6 7
|
|
138
|
+
|
|
139
|
+
export const LYDIAN = {
|
|
140
|
+
"C": ["C", "D", "E", "F#", "G", "A", "B"],
|
|
141
|
+
"C#": ["C#", "D#", "F", "G", "G#", "A#", "C"],
|
|
142
|
+
"D": ["D", "E", "F#", "G#", "A", "B", "C#"],
|
|
143
|
+
"Eb": ["Eb", "F", "G", "A", "Bb", "C", "D"],
|
|
144
|
+
"E": ["E", "F#", "G#", "A#", "B", "C#", "D#"],
|
|
145
|
+
"F": ["F", "G", "A", "B", "C", "D", "E"],
|
|
146
|
+
"F#": ["F#", "G#", "A#", "C", "C#", "D#", "F"],
|
|
147
|
+
"G": ["G", "A", "B", "C#", "D", "E", "F#"],
|
|
148
|
+
"Ab": ["Ab", "Bb", "C", "D", "Eb", "F", "G"],
|
|
149
|
+
"A": ["A", "B", "C#", "D#", "E", "F#", "G#"],
|
|
150
|
+
"Bb": ["Bb", "C", "D", "E", "F", "G", "A"],
|
|
151
|
+
"B": ["B", "C#", "D#", "F", "F#", "G#", "A#"],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Mixolydian (7 notes) ────────────────────────────────────────────────────
|
|
155
|
+
// Formula: 1 2 3 4 5 6 b7
|
|
156
|
+
|
|
157
|
+
export const MIXOLYDIAN = {
|
|
158
|
+
"C": ["C", "D", "E", "F", "G", "A", "Bb"],
|
|
159
|
+
"C#": ["C#", "D#", "F", "F#", "G#", "A#", "B"],
|
|
160
|
+
"D": ["D", "E", "F#", "G", "A", "B", "C"],
|
|
161
|
+
"Eb": ["Eb", "F", "G", "Ab", "Bb", "C", "Db"],
|
|
162
|
+
"E": ["E", "F#", "G#", "A", "B", "C#", "D"],
|
|
163
|
+
"F": ["F", "G", "A", "Bb", "C", "D", "Eb"],
|
|
164
|
+
"F#": ["F#", "G#", "A#", "B", "C#", "D#", "E"],
|
|
165
|
+
"G": ["G", "A", "B", "C", "D", "E", "F"],
|
|
166
|
+
"Ab": ["Ab", "Bb", "C", "Db", "Eb", "F", "Gb"],
|
|
167
|
+
"A": ["A", "B", "C#", "D", "E", "F#", "G"],
|
|
168
|
+
"Bb": ["Bb", "C", "D", "Eb", "F", "G", "Ab"],
|
|
169
|
+
"B": ["B", "C#", "D#", "E", "F#", "G#", "A"],
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ─── Locrian (7 notes) ───────────────────────────────────────────────────────
|
|
173
|
+
// Formula: 1 b2 b3 4 b5 b6 b7
|
|
174
|
+
|
|
175
|
+
export const LOCRIAN = {
|
|
176
|
+
"C": ["C", "Db", "Eb", "F", "Gb", "Ab", "Bb"],
|
|
177
|
+
"C#": ["C#", "D", "E", "F#", "G", "A", "B"],
|
|
178
|
+
"D": ["D", "Eb", "F", "G", "Ab", "Bb", "C"],
|
|
179
|
+
"Eb": ["Eb", "E", "Gb", "Ab", "A", "B", "Db"],
|
|
180
|
+
"E": ["E", "F", "G", "A", "Bb", "C", "D"],
|
|
181
|
+
"F": ["F", "Gb", "Ab", "Bb", "B", "Db", "Eb"],
|
|
182
|
+
"F#": ["F#", "G", "A", "B", "C", "D", "E"],
|
|
183
|
+
"G": ["G", "Ab", "Bb", "C", "Db", "Eb", "F"],
|
|
184
|
+
"Ab": ["Ab", "A", "B", "Db", "D", "E", "Gb"],
|
|
185
|
+
"A": ["A", "Bb", "C", "D", "Eb", "F", "G"],
|
|
186
|
+
"Bb": ["Bb", "B", "Db", "Eb", "E", "Gb", "Ab"],
|
|
187
|
+
"B": ["B", "C", "D", "E", "F", "G", "A"],
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Harmonic Minor (7 notes) ────────────────────────────────────────────────
|
|
191
|
+
// Formula: 1 2 b3 4 5 b6 7
|
|
192
|
+
|
|
193
|
+
export const HARMONIC_MINOR = {
|
|
194
|
+
"C": ["C", "D", "Eb", "F", "G", "Ab", "B"],
|
|
195
|
+
"C#": ["C#", "D#", "E", "F#", "G#", "A", "C"],
|
|
196
|
+
"D": ["D", "E", "F", "G", "A", "Bb", "C#"],
|
|
197
|
+
"Eb": ["Eb", "F", "Gb", "Ab", "Bb", "B", "D"],
|
|
198
|
+
"E": ["E", "F#", "G", "A", "B", "C", "D#"],
|
|
199
|
+
"F": ["F", "G", "Ab", "Bb", "C", "Db", "E"],
|
|
200
|
+
"F#": ["F#", "G#", "A", "B", "C#", "D", "F"],
|
|
201
|
+
"G": ["G", "A", "Bb", "C", "D", "Eb", "F#"],
|
|
202
|
+
"Ab": ["Ab", "Bb", "B", "Db", "Eb", "E", "G"],
|
|
203
|
+
"A": ["A", "B", "C", "D", "E", "F", "G#"],
|
|
204
|
+
"Bb": ["Bb", "C", "Db", "Eb", "F", "Gb", "A"],
|
|
205
|
+
"B": ["B", "C#", "D", "E", "F#", "G", "A#"],
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ─── Melodic Minor (7 notes) ─────────────────────────────────────────────────
|
|
209
|
+
// Formula: 1 2 b3 4 5 6 7
|
|
210
|
+
|
|
211
|
+
export const MELODIC_MINOR = {
|
|
212
|
+
"C": ["C", "D", "Eb", "F", "G", "A", "B"],
|
|
213
|
+
"C#": ["C#", "D#", "E", "F#", "G#", "A#", "C"],
|
|
214
|
+
"D": ["D", "E", "F", "G", "A", "B", "C#"],
|
|
215
|
+
"Eb": ["Eb", "F", "Gb", "Ab", "Bb", "C", "D"],
|
|
216
|
+
"E": ["E", "F#", "G", "A", "B", "C#", "D#"],
|
|
217
|
+
"F": ["F", "G", "Ab", "Bb", "C", "D", "E"],
|
|
218
|
+
"F#": ["F#", "G#", "A", "B", "C#", "D#", "F"],
|
|
219
|
+
"G": ["G", "A", "Bb", "C", "D", "E", "F#"],
|
|
220
|
+
"Ab": ["Ab", "Bb", "B", "Db", "Eb", "F", "G"],
|
|
221
|
+
"A": ["A", "B", "C", "D", "E", "F#", "G#"],
|
|
222
|
+
"Bb": ["Bb", "C", "Db", "Eb", "F", "G", "A"],
|
|
223
|
+
"B": ["B", "C#", "D", "E", "F#", "G#", "A#"],
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ─── Phrygian Dominant (7 notes) ─────────────────────────────────────────────
|
|
227
|
+
// Formula: 1 b2 3 4 5 b6 b7 (Spanish/metal feel)
|
|
228
|
+
|
|
229
|
+
export const PHRYGIAN_DOMINANT = {
|
|
230
|
+
"C": ["C", "Db", "E", "F", "G", "Ab", "Bb"],
|
|
231
|
+
"C#": ["C#", "D", "F", "F#", "G#", "A", "B"],
|
|
232
|
+
"D": ["D", "Eb", "F#", "G", "A", "Bb", "C"],
|
|
233
|
+
"Eb": ["Eb", "E", "G", "Ab", "Bb", "B", "Db"],
|
|
234
|
+
"E": ["E", "F", "G#", "A", "B", "C", "D"],
|
|
235
|
+
"F": ["F", "Gb", "A", "Bb", "C", "Db", "Eb"],
|
|
236
|
+
"F#": ["F#", "G", "A#", "B", "C#", "D", "E"],
|
|
237
|
+
"G": ["G", "Ab", "B", "C", "D", "Eb", "F"],
|
|
238
|
+
"Ab": ["Ab", "A", "C", "Db", "Eb", "E", "Gb"],
|
|
239
|
+
"A": ["A", "Bb", "C#", "D", "E", "F", "G"],
|
|
240
|
+
"Bb": ["Bb", "B", "D", "Eb", "F", "Gb", "Ab"],
|
|
241
|
+
"B": ["B", "C", "D#", "E", "F#", "G", "A"],
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ─── Whole Tone (6 notes) ────────────────────────────────────────────────────
|
|
245
|
+
// Formula: all whole steps — only 2 unique versions exist, rest are modes of same scale
|
|
246
|
+
|
|
247
|
+
export const WHOLE_TONE = {
|
|
248
|
+
"C": ["C", "D", "E", "F#", "G#", "A#"],
|
|
249
|
+
"C#": ["C#", "D#", "F", "G", "A", "B"],
|
|
250
|
+
"D": ["D", "E", "F#", "G#", "A#", "C"],
|
|
251
|
+
"Eb": ["Eb", "F", "G", "A", "B", "C#"],
|
|
252
|
+
"E": ["E", "F#", "G#", "A#", "C", "D"],
|
|
253
|
+
"F": ["F", "G", "A", "B", "C#", "D#"],
|
|
254
|
+
"F#": ["F#", "G#", "A#", "C", "D", "E"],
|
|
255
|
+
"G": ["G", "A", "B", "C#", "D#", "F"],
|
|
256
|
+
"Ab": ["Ab", "Bb", "C", "D", "E", "F#"],
|
|
257
|
+
"A": ["A", "B", "C#", "D#", "F", "G"],
|
|
258
|
+
"Bb": ["Bb", "C", "D", "E", "F#", "G#"],
|
|
259
|
+
"B": ["B", "C#", "D#", "F", "G", "A"],
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ─── Diminished (8 notes) ────────────────────────────────────────────────────
|
|
263
|
+
// Formula: alternating whole and half steps
|
|
264
|
+
|
|
265
|
+
export const DIMINISHED = {
|
|
266
|
+
"C": ["C", "D", "Eb", "F", "Gb", "Ab", "A", "B"],
|
|
267
|
+
"C#": ["C#", "D#", "E", "F#", "G", "A", "Bb", "C"],
|
|
268
|
+
"D": ["D", "E", "F", "G", "Ab", "Bb", "B", "C#"],
|
|
269
|
+
"Eb": ["Eb", "F", "Gb", "Ab", "A", "B", "C", "D"],
|
|
270
|
+
"E": ["E", "F#", "G", "A", "Bb", "C", "Db", "D#"],
|
|
271
|
+
"F": ["F", "G", "Ab", "Bb", "B", "Db", "D", "E"],
|
|
272
|
+
"F#": ["F#", "G#", "A", "B", "C", "D", "Eb", "F"],
|
|
273
|
+
"G": ["G", "A", "Bb", "C", "Db", "Eb", "E", "F#"],
|
|
274
|
+
"Ab": ["Ab", "Bb", "B", "Db", "D", "E", "F", "G"],
|
|
275
|
+
"A": ["A", "B", "C", "D", "Eb", "F", "F#", "G#"],
|
|
276
|
+
"Bb": ["Bb", "C", "Db", "Eb", "E", "F#", "G", "A"],
|
|
277
|
+
"B": ["B", "C#", "D", "E", "F", "G", "Ab", "A#"],
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ─── Convenience lookup ───────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
export const SCALES = {
|
|
283
|
+
"major": MAJOR,
|
|
284
|
+
"natural_minor": NATURAL_MINOR,
|
|
285
|
+
"pentatonic_minor": PENTATONIC_MINOR,
|
|
286
|
+
"pentatonic_major": PENTATONIC_MAJOR,
|
|
287
|
+
"blues": BLUES,
|
|
288
|
+
"dorian": DORIAN,
|
|
289
|
+
"phrygian": PHRYGIAN,
|
|
290
|
+
"lydian": LYDIAN,
|
|
291
|
+
"mixolydian": MIXOLYDIAN,
|
|
292
|
+
"locrian": LOCRIAN,
|
|
293
|
+
"harmonic_minor": HARMONIC_MINOR,
|
|
294
|
+
"melodic_minor": MELODIC_MINOR,
|
|
295
|
+
"phrygian_dominant": PHRYGIAN_DOMINANT,
|
|
296
|
+
"whole_tone": WHOLE_TONE,
|
|
297
|
+
"diminished": DIMINISHED,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// list of all valid root keys
|
|
301
|
+
export const KEYS = ["C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"]
|