Files
JamBuddy/src/lib/education.js
T
2026-03-18 01:26:51 +00:00

437 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NOTES, CHORD_TYPES } from './theory'
// ─── Parse helper (self-contained, mirrors voicings.js) ───────────────────────
export function parseChord(name) {
if (!name) return null
const FLAT = ['C','Db','D','Eb','E','F','Gb','G','Ab','A','Bb','B']
let rest = name
let root = rest.length > 1 && (rest[1] === '#' || rest[1] === 'b')
? (rest = rest.slice(2), name.slice(0, 2))
: (rest = rest.slice(1), name.slice(0, 1))
let rootPc = NOTES.indexOf(root)
if (rootPc === -1) rootPc = FLAT.indexOf(root)
if (rootPc === -1) return null
const suffixMap = {
'': 'maj', 'm': 'min', 'min': 'min', 'maj': 'maj',
'7': 'dom7', 'maj7': 'maj7', 'm7': 'min7', 'min7': 'min7',
'dim': 'dim', 'dim7': 'dim7', 'm7b5': 'half_dim', 'ø': 'half_dim',
'aug': 'aug', '+': 'aug',
'sus4': 'sus4', 'sus2': 'sus2',
'6': 'maj6', 'm6': 'min6', 'add9': 'add9',
}
return { rootPc, type: suffixMap[rest] ?? 'maj' }
}
function chordName(rootPc, type) {
return NOTES[rootPc] + (CHORD_TYPES[type]?.suffix ?? '')
}
// ─── Famous progressions ──────────────────────────────────────────────────────
// degrees[] — semitone offsets from tonic
// qualities[] — chord type keys (CHORD_TYPES) for each degree
// mode — the mode this progression is naturally written in
// styleVariations — how genre players re-harmonise these chords
export const FAMOUS_PROGRESSIONS = [
{
id: 'axis',
name: 'Axis Progression',
pattern: 'I V vi IV',
degrees: [0, 7, 9, 5],
qualities: ['maj', 'maj', 'min', 'maj'],
mode: 'major',
genre: ['Pop', 'Rock'],
songs: ['Let It Be — Beatles', 'With or Without You — U2', 'Someone Like You — Adele', "Don't Stop Believin' — Journey", 'Wonderwall — Oasis', 'Demons — Imagine Dragons'],
description: 'The defining progression of modern pop. Works in every genre and tempo.',
tip: 'Try starting on the vi instead — suddenly it feels darker and more yearning.',
styleVariations: [
{ label: 'Jazz', pattern: 'Imaj7 V7 vim7 IVmaj7', qualities: ['maj7','dom7','min7','maj7'] },
{ label: 'Soul', pattern: 'Imaj9 V9 vim9 IVmaj9', qualities: ['maj7','dom7','min7','maj7'] },
{ label: 'Blues', pattern: 'I7 V7 vi7 IV7', qualities: ['dom7','dom7','dom7','dom7'] },
{ label: 'Ambient', pattern: 'Isus2 Vsus2 vim7 IVadd9', qualities: ['sus2','sus2','min7','add9'] },
],
},
{
id: 'fifties',
name: '50s / Doo-Wop',
pattern: 'I vi IV V',
degrees: [0, 9, 5, 7],
qualities: ['maj', 'min', 'maj', 'maj'],
mode: 'major',
genre: ['Pop', 'Doo-Wop', 'Rock'],
songs: ['Stand By Me — Ben E. King', 'Earth Angel', 'Blue Moon', 'Unchained Melody', 'Every Breath You Take — Police'],
description: 'The doo-wop backbone. Sweet, nostalgic, universally singable and timeless.',
tip: "The vi chord is the emotional pivot — it's the same root as in the Axis, just a different order.",
styleVariations: [
{ label: 'Jazz', pattern: 'Imaj7 vim7 IVmaj7 V7', qualities: ['maj7','min7','maj7','dom7'] },
{ label: 'Soul', pattern: 'I6 vim7 IV V7sus4', qualities: ['maj6','min7','maj','sus4'] },
{ label: 'Funk', pattern: 'Imaj9 vim9 IV9 V9', qualities: ['maj7','min7','dom7','dom7'] },
],
},
{
id: 'canon',
name: 'Canon / Pachelbel',
pattern: 'I V vi iii IV I IV V',
degrees: [0, 7, 9, 4, 5, 0, 5, 7],
qualities: ['maj','maj','min','min','maj','maj','maj','maj'],
mode: 'major',
genre: ['Classical', 'Pop', 'Rock'],
songs: ['Canon in D — Pachelbel', 'Basket Case — Green Day', 'Go West — Pet Shop Boys', 'Graduation — Vitamin C'],
description: 'Baroque timelessness. The descending bass line creates inevitable forward motion.',
tip: 'The iii chord is the secret ingredient — it bridges vi and IV with aristocratic weight.',
styleVariations: [
{ label: 'Rock', pattern: 'Power chords all the way down', qualities: [] },
{ label: 'Neo-soul',pattern: 'Imaj9 V9 vim9 iiim7 IVmaj9', qualities: ['maj7','dom7','min7','min7','maj7'] },
],
},
{
id: 'two_five_one',
name: 'ii V I (Jazz)',
pattern: 'iim7 V7 Imaj7',
degrees: [2, 7, 0],
qualities: ['min7', 'dom7', 'maj7'],
mode: 'major',
genre: ['Jazz', 'Bossa Nova'],
songs: ['Autumn Leaves', 'All The Things You Are', 'Fly Me To The Moon', 'The Girl From Ipanema', 'Misty'],
description: 'The bedrock of jazz harmony. The tritone in V7 resolves to I with beautiful tension.',
tip: 'The ii chord pre-resolves the V — together they create an inevitable pull to the I.',
styleVariations: [
{ label: 'Bossa', pattern: 'iim7 V7(9) Imaj9', qualities: ['min7','dom7','maj7'] },
{ label: 'Bebop', pattern: 'iim7 V7(♭9) Imaj7(#11)', qualities: ['min7','dom7','maj7'] },
{ label: 'Modal', pattern: 'im7 IV7 (Dorian vamp)', qualities: ['min7','dom7'] },
],
},
{
id: 'blues_12',
name: '12-Bar Blues',
pattern: 'I I I I IV IV I I V IV I V',
degrees: [0, 0, 0, 0, 5, 5, 0, 0, 7, 5, 0, 7],
qualities: ['dom7','dom7','dom7','dom7','dom7','dom7','dom7','dom7','dom7','dom7','dom7','dom7'],
mode: 'major',
genre: ['Blues', 'Rock', 'Jazz'],
songs: ['Johnny B. Goode — Chuck Berry', 'Pride & Joy — SRV', 'Crossroads — Robert Johnson', 'Hound Dog', 'Folsom Prison Blues — Cash'],
description: 'The foundation of rock and blues. Master this and you can jam with anyone on Earth.',
tip: 'All three chords are dominant 7ths — that dissonance is what makes blues feel so restless.',
styleVariations: [
{ label: 'Shuffle', pattern: 'I7 IV7 V7 with shuffle rhythm', qualities: ['dom7','dom7','dom7'] },
{ label: 'Jazz', pattern: 'Imaj7 IV7 iim7 V7 quick changes', qualities: ['maj7','dom7','min7','dom7'] },
{ label: 'Minor', pattern: 'im7 ivm7 vm7 (minor blues)', qualities: ['min7','min7','min7'] },
],
},
{
id: 'andalusian',
name: 'Andalusian Cadence',
pattern: 'i ♭VII ♭VI V',
degrees: [0, 10, 8, 7],
qualities: ['min', 'maj', 'maj', 'maj'],
mode: 'minor',
genre: ['Flamenco', 'Rock', 'Classical'],
songs: ['Stairway to Heaven intro — Led Zeppelin', 'Hit the Road Jack', 'Sultans of Swing — Dire Straits', 'White Christmas'],
description: 'Descending bass line creates unstoppable forward motion. Timeless, dramatic, inevitable.',
tip: 'The final V chord (major) is the harmonic surprise in a minor context — forces resolution.',
styleVariations: [
{ label: 'Flamenco', pattern: 'im ♭VII ♭VI V7', qualities: ['min','maj','maj','dom7'] },
{ label: 'Rock', pattern: 'im5 ♭VII5 ♭VI5 V5 (power)', qualities: ['min','maj','maj','maj'] },
{ label: 'Jazz', pattern: 'im(maj7) ♭VIImaj7 ♭VImaj7 V7(#9)', qualities: ['min7','maj7','maj7','dom7'] },
],
},
{
id: 'minor_anthem',
name: 'Minor Anthem',
pattern: 'i ♭VI ♭III ♭VII',
degrees: [0, 8, 3, 10],
qualities: ['min', 'maj', 'maj', 'maj'],
mode: 'minor',
genre: ['Rock', 'Pop', 'Metal'],
songs: ['Numb — Linkin Park', 'In The End — Linkin Park', 'Boulevard of Broken Dreams — Green Day', 'Creep — Radiohead', 'Smells Like Teen Spirit — Nirvana'],
description: 'The anthem of angst. Powerful, relentless, emotionally direct.',
tip: 'Every chord is major except the tonic — the contrast makes the i feel even more desperate.',
styleVariations: [
{ label: 'Stripped', pattern: 'im7 ♭VImaj7 ♭IIImaj7 ♭VIImaj7', qualities: ['min7','maj7','maj7','maj7'] },
{ label: 'Epic', pattern: 'im ♭VI ♭III ♭VII with sus2 variants', qualities: ['min','maj','maj','maj'] },
],
},
{
id: 'minor_oscillate',
name: 'Minor Oscillation',
pattern: 'i ♭VII ♭VI ♭VII',
degrees: [0, 10, 8, 10],
qualities: ['min', 'maj', 'maj', 'maj'],
mode: 'minor',
genre: ['Rock', 'Folk', 'Pop'],
songs: ['All Along the Watchtower — Dylan/Hendrix', "Knockin' on Heaven's Door — Dylan", 'Pumped Up Kicks — Foster the People', 'Africa — Toto'],
description: 'The ♭VII oscillates back and forth — creates hypnotic looping energy.',
tip: 'Works as a 2-bar vamp (i ♭VII) or a full 4-bar loop. The ♭VI adds breathing room.',
styleVariations: [
{ label: 'Folk', pattern: 'im ♭VII ♭VI ♭VII fingerpicked', qualities: ['min','maj','maj','maj'] },
{ label: 'Rock', pattern: 'im ♭VII ♭VI power chords', qualities: ['min','maj','maj','maj'] },
],
},
{
id: 'mixolydian',
name: 'Mixolydian Rock',
pattern: 'I ♭VII IV',
degrees: [0, 10, 5],
qualities: ['maj', 'maj', 'maj'],
mode: 'major',
genre: ['Rock', 'Folk', 'Celtic'],
songs: ['Sweet Home Alabama — Lynyrd Skynyrd', 'La Grange — ZZ Top', 'Werewolves of London — Warren Zevon', 'Norwegian Wood — Beatles'],
description: 'The ♭VII chord defines Mixolydian mode. Swagger and swagger only.',
tip: 'In standard major, the VII is diminished. Flattening it to ♭VII gives you a major chord — that shift is everything.',
styleVariations: [
{ label: 'Celtic', pattern: 'I ♭VII IV I alternating', qualities: ['maj','maj','maj','maj'] },
{ label: 'Funk', pattern: 'I9 ♭VII9 IV9 dominant 9ths', qualities: ['dom7','dom7','dom7'] },
],
},
{
id: 'dorian_vamp',
name: 'Dorian Vamp',
pattern: 'i IV',
degrees: [0, 5],
qualities: ['min', 'maj'],
mode: 'minor',
genre: ['Rock', 'Jazz', 'Funk'],
songs: ['Oye Como Va — Santana', 'So What — Miles Davis', 'Scarborough Fair', 'Eleanor Rigby verse — Beatles', 'Mad World — Tears for Fears'],
description: 'The major IV over a minor i chord signals Dorian mode. Smooth, open, sophisticated.',
tip: 'Natural minor would have a minor iv. The major IV is Dorian\'s signature — it feels both sad and groovy.',
styleVariations: [
{ label: 'Jazz', pattern: 'im7 IV7 (comp beneath a soloist)', qualities: ['min7','dom7'] },
{ label: 'Funk', pattern: 'im9 IV13 (layered synth and guitar)', qualities: ['min7','dom7'] },
{ label: 'Rock', pattern: 'im IV im IV (guitar riff)', qualities: ['min','maj'] },
],
},
{
id: 'jazz_turnaround',
name: 'Jazz Turnaround',
pattern: 'I vi ii V',
degrees: [0, 9, 2, 7],
qualities: ['maj7', 'min7', 'min7', 'dom7'],
mode: 'major',
genre: ['Jazz', 'Swing'],
songs: ['I Got Rhythm — Gershwin', 'Rhythm Changes', 'How High the Moon', 'countless jazz standards'],
description: 'The jazz turnaround — loops back to the top of the form with elegant inevitability.',
tip: 'Each chord resolves down a fifth to the next. That chain of fifths is what makes jazz sound "right".',
styleVariations: [
{ label: 'Bebop', pattern: 'Imaj7 vim7 iim7 V7(♭9)', qualities: ['maj7','min7','min7','dom7'] },
{ label: 'Tritone', pattern: 'Imaj7 ♭III7 iim7 ♭II7 (tritone subs)', qualities: ['maj7','dom7','min7','dom7'] },
],
},
{
id: 'phrygian',
name: 'Phrygian Tension',
pattern: 'i ♭II',
degrees: [0, 1],
qualities: ['min', 'maj'],
mode: 'minor',
genre: ['Flamenco', 'Metal', 'Film'],
songs: ['Game of Thrones theme', 'Spanish flamenco standards', 'heavy metal riffs', 'El Tango de Roxanne'],
description: 'The ♭II (Neapolitan) chord creates extreme tension. Spanish fire and metal darkness.',
tip: 'Just two chords — the half-step relationship between roots is what makes it feel so tense.',
styleVariations: [
{ label: 'Flamenco', pattern: 'im ♭II im with fast strumming', qualities: ['min','maj','min'] },
{ label: 'Metal', pattern: 'im5 ♭II5 power chord riff', qualities: ['min','maj'] },
],
},
{
id: 'minor_pop',
name: 'Sad Pop Minor',
pattern: 'vi IV I V',
degrees: [9, 5, 0, 7],
qualities: ['min', 'maj', 'maj', 'maj'],
mode: 'major',
genre: ['Pop', 'Rock', 'Indie'],
songs: ['Zombie — Cranberries', 'Torn — Natalie Imbruglia', 'Apologize — Timbaland', 'Fix You — Coldplay'],
description: 'Same chords as the Axis — just starting on the vi. Instantly feels darker and more yearning.',
tip: 'Which chord you start on changes everything emotionally. This is the Axis heard through minor eyes.',
styleVariations: [
{ label: 'Soul', pattern: 'vim7 IVmaj7 Imaj7 V9', qualities: ['min7','maj7','maj7','dom7'] },
{ label: 'Indie', pattern: 'vim7 IVadd9 Iadd9 Vsus4', qualities: ['min7','add9','add9','sus4'] },
],
},
{
id: 'lydian_float',
name: 'Lydian Float',
pattern: 'I II',
degrees: [0, 2],
qualities: ['maj', 'maj'],
mode: 'major',
genre: ['Film', 'Jazz', 'Pop'],
songs: ['The Simpsons theme', 'Joe Satriani — Flying in a Blue Dream', 'many John Williams cues', 'Man or Muppet'],
description: 'The raised ♯4 in Lydian makes the II chord major instead of minor. Dreamy, floating, magical.',
tip: 'In normal major, the II chord is minor. Making it major (Lydian) lifts the whole progression off the ground.',
styleVariations: [
{ label: 'Film', pattern: 'Imaj7 IImaj7 slowly held', qualities: ['maj7','maj7'] },
{ label: 'Jazz', pattern: 'Imaj7(#11) — the Lydian 7th chord', qualities: ['maj7','maj7'] },
],
},
{
id: 'sensitive',
name: 'Sensitive Oscillation',
pattern: 'vi V IV V',
degrees: [9, 7, 5, 7],
qualities: ['min', 'maj', 'maj', 'maj'],
mode: 'major',
genre: ['Pop', 'Indie'],
songs: ['Mad World — Tears for Fears', 'Every Breath You Take — Police', 'Losing My Religion — REM'],
description: 'Oscillates between vi and IV through V. Aching, introspective, perpetually unresolved.',
tip: 'The V never quite resolves to I — it keeps bouncing back to the IV or vi. That suspension is the emotion.',
styleVariations: [
{ label: 'Indie', pattern: 'vim7 Vsus4 IVadd9 V', qualities: ['min7','sus4','add9','maj'] },
],
},
]
// ─── Chord substitution options ───────────────────────────────────────────────
// For each chord type: array of { type, label, tip } suggestions
export const CHORD_SUBSTITUTIONS = {
maj: [
{ type: 'maj7', tip: 'Add the major 7th — dreamy jazz colour' },
{ type: 'add9', tip: 'Add the 9th — modern, open, Coldplay-ish' },
{ type: 'maj6', tip: 'Add major 6th — vintage Django jazz sweetness' },
{ type: 'sus2', tip: 'Replace 3rd with 2nd — airy and ambiguous' },
{ type: 'sus4', tip: 'Suspend then resolve — creates rhythmic motion' },
],
min: [
{ type: 'min7', tip: 'Add the minor 7th — smooth soul and jazz' },
{ type: 'add9', tip: 'Add 9th over minor — bittersweet modern ache (Radiohead)' },
{ type: 'min6', tip: 'Add major 6th over minor — flamenco and tango colour' },
{ type: 'sus2', tip: 'Remove 3rd entirely — ambiguous and floating' },
{ type: 'half_dim', tip: 'Flatten the 5th — half-diminished, much darker' },
],
dom7: [
{ type: 'maj7', tip: 'Raise the 7th — softer, much less tension' },
{ type: 'min7', tip: 'Lower the 3rd too — darkens the dominant' },
{ type: 'sus4', tip: 'Replace 3rd with 4th — funky unresolved suspension' },
{ type: 'dim7', tip: 'Diminished substitute — extreme tension before resolution' },
],
maj7: [
{ type: 'maj', tip: 'Simplify — strip back to triad, rawer feel' },
{ type: 'add9', tip: 'Drop 7th, add 9th — brighter and more open' },
{ type: 'maj6', tip: 'Swap 7th for 6th — retro jazz, less ethereal' },
{ type: 'dom7', tip: 'Flatten 7th — suddenly bluesy with tension' },
],
min7: [
{ type: 'min', tip: 'Strip back — rawer, more aggressive feel' },
{ type: 'half_dim', tip: 'Flatten the 5th — half-dim, darker and more tense' },
{ type: 'min6', tip: 'Add major 6th — sophisticated jazz minor colour' },
{ type: 'dom7', tip: 'Make it dominant — strong pull to resolve' },
],
dim: [
{ type: 'dim7', tip: 'Add dim7 — fully symmetric, equally unstable' },
{ type: 'half_dim', tip: 'Half-dim — softer, more melodic tension' },
{ type: 'min', tip: 'Simplify to minor — much less dissonant' },
],
dim7: [
{ type: 'dim', tip: 'Remove 7th — triad version, slightly less tense' },
{ type: 'half_dim', tip: 'Raise one note to half-dim — softer resolution' },
{ type: 'min7', tip: 'Raise ♭5 — from dark to smooth in one note' },
],
aug: [
{ type: 'maj', tip: 'Resolve the aug5 down to 5 — release the tension' },
{ type: 'dom7', tip: 'Add ♭7 over aug — double tension before a big resolution' },
],
sus4: [
{ type: 'maj', tip: 'Resolve — drop the 4th to the 3rd (classic sus → maj)' },
{ type: 'sus2', tip: 'Switch suspension — from 4th to 2nd, different feel' },
{ type: 'dom7', tip: 'Add ♭7 too — 7sus4, funky and unresolved' },
],
sus2: [
{ type: 'maj', tip: 'Fill in the 3rd — resolve the suspension clearly' },
{ type: 'sus4', tip: 'Swap 2nd for 4th — different flavour of openness' },
{ type: 'add9', tip: 'Add the 3rd back — sus2 becomes a richer add9' },
],
half_dim: [
{ type: 'dim7', tip: 'Add dim7 — more symmetric and tense' },
{ type: 'min7', tip: 'Raise the ♭5 to 5 — suddenly much smoother' },
{ type: 'min', tip: 'Strip back — simple minor triad' },
],
maj6: [
{ type: 'maj7', tip: 'Swap 6th for 7th — more modern jazz, more ethereal' },
{ type: 'add9', tip: 'Replace 6th with 9th — brighter, contemporary feel' },
],
min6: [
{ type: 'min7', tip: 'Swap 6th for 7th — smoother, less exotic' },
{ type: 'half_dim', tip: 'Enharmonic trick — min6 and half-dim share notes' },
],
add9: [
{ type: 'maj7', tip: 'Add the 7th — full maj9 sound, very lush' },
{ type: 'sus2', tip: 'Remove the 3rd — purely suspended' },
{ type: 'maj', tip: 'Strip the 9th — clean triad' },
],
}
// ─── Compute chord names for a famous progression in a given key ──────────────
export function progressionInKey(famousProg, keyRoot) {
const rootPc = NOTES.indexOf(keyRoot)
if (rootPc === -1) return []
return famousProg.degrees.map((d, i) => {
const pc = (rootPc + d) % 12
const type = famousProg.qualities[i] ?? 'maj'
return chordName(pc, type)
})
}
// Style variation chords in a given key
export function styleVariationInKey(styleVar, famousProg, keyRoot) {
if (!styleVar.qualities?.length) return []
const rootPc = NOTES.indexOf(keyRoot)
if (rootPc === -1) return []
return famousProg.degrees.slice(0, styleVar.qualities.length).map((d, i) => {
const pc = (rootPc + d) % 12
return chordName(pc, styleVar.qualities[i] ?? 'maj')
})
}
// ─── Similarity matching ──────────────────────────────────────────────────────
export function findSimilarProgressions(detectedChords, keyInfo) {
if (!detectedChords?.length || !keyInfo?.root) return []
const rootPc = NOTES.indexOf(keyInfo.root)
if (rootPc === -1) return []
// Convert detected chords to semitone offsets from key root
const detectedPcs = detectedChords
.map(c => { const p = parseChord(c); return p ? (p.rootPc - rootPc + 12) % 12 : null })
.filter(v => v !== null)
if (!detectedPcs.length) return []
const detectedSet = new Set(detectedPcs)
const results = []
for (const prog of FAMOUS_PROGRESSIONS) {
const progPcs = prog.degrees.map(d => d % 12)
const progSet = new Set(progPcs)
// Rotation match: does detected sequence appear in famous prog (as cyclic rotation)?
let rotScore = 0
const compareLen = Math.min(detectedPcs.length, progPcs.length)
for (let rot = 0; rot < progPcs.length; rot++) {
let hits = 0
for (let i = 0; i < compareLen; i++) {
if (detectedPcs[i] === progPcs[(rot + i) % progPcs.length]) hits++
}
rotScore = Math.max(rotScore, hits / compareLen)
}
// Jaccard similarity (chord-set overlap regardless of order)
const inter = [...detectedSet].filter(d => progSet.has(d)).length
const union = new Set([...detectedSet, ...progSet]).size
const jaccardScore = inter / union
const score = Math.max(rotScore, jaccardScore * 0.8)
if (score >= 0.35) results.push({ ...prog, score })
}
return results.sort((a, b) => b.score - a.score).slice(0, 4)
}
// ─── Substitutions for a chord name ──────────────────────────────────────────
export function getChordSubstitutions(chordName) {
const p = parseChord(chordName)
if (!p) return []
const subs = CHORD_SUBSTITUTIONS[p.type] ?? []
return subs.map(sub => ({
...sub,
chord: NOTES[p.rootPc] + (CHORD_TYPES[sub.type]?.suffix ?? ''),
}))
}