char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
c0bb435
main
1import { assert , assertEquals } from "@std/assert" ; 2import { analyze , origin , palette , romanOf } from "./analysis.ts" ; 3import { 4chordMatches , 5chordName , 6chordPcs , 7detectChord , 8detectChords , 9normalizeTones , 10parseChord , 11prettySymbol , 12} from "./chord.ts" ; 13import { describeTones } from "./describe.ts" ; 14import { type Key , MODES , type ModeId , stepInScale } from "./key.ts" ; 15import { parseNote , pcOf , spellPc } from "./pitch.ts" ; 16import { invertVoicing , voiceChord } from "./voicing.ts" ; 17 18const key = ( tonic :string , mode :ModeId = "ionian" ) :Key => ({ 19tonic :parseNote ( tonic ) ! . note , 20 mode, 21}); 22const C = key ( "C" ); 23 24const name = ( src :string , k = C ) => { 25const chord = parseChord ( src , k ); 26return chord && chordName ( chord ); 27}; 28const pcs = ( src :string , k = C ) => { 29const chord = parseChord ( src , k ); 30return chord && chordPcs ( chord ); 31}; 32 33Deno . test ( "letter chord symbols in their many spellings" , () => { 34assertEquals ( name ( "C" ), "C" ); 35assertEquals ( name ( "am" ), "Am" ); 36assertEquals ( name ( "Cmin7" ), "Cm7" ); 37assertEquals ( name ( "C-7" ), "Cm7" ); 38assertEquals ( name ( "C-7b5" ), "Cm7♭5" ); 39assertEquals ( name ( "Cø" ), "Cm7♭5" ); 40assertEquals ( name ( "C°7" ), "Cdim7" ); 41assertEquals ( name ( "CΔ" ), "Cmaj7" ); 42assertEquals ( name ( "CM7" ), "Cmaj7" ); 43assertEquals ( name ( "Cmaj" ), "C" ); 44assertEquals ( name ( "Cm(maj7)" ), "CmMaj7" ); 45assertEquals ( name ( "C+" ), "Caug" ); 46assertEquals ( name ( "Bbmaj9" ), "B♭maj9" ); 47assertEquals ( name ( "F#m7/C#" ), "F♯m7/C♯" ); 48assertEquals ( name ( "C6/9" ), "C6/9" ); 49assertEquals ( name ( "C6/9/E" ), "C6/9/E" ); 50assertEquals ( name ( "Csus" ), "Csus4" ); 51assertEquals ( name ( "C7sus4" ), "C7sus4" ); 52assertEquals ( name ( "C/C" ), "C" ); 53assertEquals ( pcs ( "C7(b9,#11)" ), [ 0 , 1 , 4 , 6 , 7 , 10 ]); 54assertEquals ( pcs ( "C7alt" ), [ 0 , 1 , 3 , 4 , 6 , 8 , 10 ]); 55assertEquals ( pcs ( "C/D" ), [ 0 , 2 , 4 , 7 ]); 56}); 57 58Deno . test ( "rejects nonsense" , () => { 59for ( const bad of [ "" , "H" , "Cfoo" , "C7x" , "8" , "vii/" ]) { 60assertEquals ( parseChord ( bad , C ), undefined , bad ); 61} 62}); 63 64Deno . test ( "every named chord quality parses and detects as itself" , () => { 65// prettier-ignore 66const qualities = [ 67"" , 68"m" , 69"dim" , 70"aug" , 71"sus4" , 72"sus2" , 73"5" , 74"6" , 75"m6" , 76"6/9" , 77"add9" , 78"madd9" , 79"7" , 80"maj7" , 81"m7" , 82"m7b5" , 83"dim7" , 84"mMaj7" , 85"7sus4" , 86"7b5" , 87"7#5" , 88"maj7#5" , 89"9" , 90"maj9" , 91"m9" , 92"9sus4" , 93"7b9" , 94"7#9" , 95"maj7#11" , 96"9#11" , 97"7b9#11" , 98"11" , 99"m11" , 100"maj11" , 101"13" , 102"maj13" , 103"m13" , 104"13b9" , 105"13#11" , 106"7b13" , 107]; 108for ( const suffix of qualities ) { 109const chord = parseChord ( "C" + suffix , C ) ! ; 110assertEquals ( chordName ( chord ), "C" + prettySymbol ( suffix ), suffix ); 111const midis = chordPcs ( chord ). map ( pc => 60 + pc ); 112assertEquals ( chordName ( detectChord ( midis , C ) ! ), "C" + prettySymbol ( suffix ), suffix ); 113} 114}); 115 116Deno . test ( "roman numerals are relative to the major scale of the key" , () => { 117assertEquals ( name ( "bVI" ), "A♭" ); 118assertEquals ( name ( "iv" ), "Fm" ); 119assertEquals ( name ( "bVIImaj7" ), "B♭maj7" ); 120assertEquals ( name ( "viiø7" ), "Bm7♭5" ); 121assertEquals ( name ( "vii°7" ), "Bdim7" ); 122assertEquals ( name ( "I/E" ), "C/E" ); 123assertEquals ( name ( "bVI" , key ( "Eb" )), "C♭" ); 124assertEquals ( name ( "ii7" , key ( "F#" )), "G♯m7" ); 125assertEquals ( name ( "V" , key ( "A" , "aeolian" )), "E" ); 126}); 127 128Deno . test ( "roman numerals go by pitch, whatever the spelling" , () => { 129const roman = ( src :string , k :Key ) => romanOf ( parseChord ( src , k ) ! , k ); 130assertEquals ( roman ( "G#m" , key ( "Ab" , "dorian" )), "i" ); 131assertEquals ( roman ( "Abm" , key ( "G#" , "dorian" )), "i" ); 132assertEquals ( roman ( "C#7" , key ( "Db" )), "I7" ); 133assertEquals ( roman ( "F#" , key ( "Gb" , "lydian" )), "I" ); 134assertEquals ( roman ( "A#m7" , key ( "Bb" , "aeolian" )), "i7" ); 135assertEquals ( roman ( "C#m" , key ( "Ab" , "dorian" )), "iv" ); 136assertEquals ( roman ( "D#m" , key ( "Ab" , "dorian" )), "v" ); 137assertEquals ( roman ( "F#" , key ( "Ab" , "dorian" )), "VII" ); 138// one usual name for most intervals, however they're spelled 139assertEquals ([ roman ( "D#m" , C ), roman ( "Ebm" , C )], [ "biii" , "biii" ]); 140assertEquals ([ roman ( "Ab" , C ), roman ( "G#" , C )], [ "bVI" , "bVI" ]); 141assertEquals ([ roman ( "Fb" , C ), roman ( "B#" , C ), roman ( "Cb" , C )], [ "III" , "I" , "VII" ]); 142// but the spelling decides at the tritone, and for diminished chords 143assertEquals ([ roman ( "F#" , C ), roman ( "Gb" , C )], [ "#IV" , "bV" ]); 144assertEquals ([ roman ( "C#dim7" , C ), roman ( "Dbdim7" , C )], [ "#i°7" , "bii°7" ]); 145assertEquals ([ roman ( "G#dim" , C ), roman ( "Abdim" , C )], [ "#v°" , "bvi°" ]); 146}); 147 148Deno . test ( "in other modes, roman numerals count that mode's degrees" , () => { 149const Am = key ( "A" , "aeolian" ); 150const roman = ( src :string , k :Key ) => romanOf ( parseChord ( src , k ) ! , k ); 151// what the chords are called 152assertEquals ( 153[ "Am" , "Bdim" , "C" , "Dm" , "Em" , "F" , "G" ]. map ( c => roman ( c , Am )), 154[ "i" , "ii°" , "III" , "iv" , "v" , "VI" , "VII" ], 155); 156assertEquals ( 157[ roman ( "E" , Am ), roman ( "G#dim" , Am ), roman ( "F#m" , Am ), roman ( "D" , Am )], 158[ "V" , "#vii°" , "#vi" , "IV" ], 159); 160assertEquals ( 161[ roman ( "F" , key ( "C" , "lydian" )), roman ( "Bb" , key ( "C" , "mixolydian" ))], 162[ "bIV" , "VII" ], 163); 164assertEquals ( roman ( "Db" , key ( "C" , "phrygian" )), "II" ); 165// and what typing them means 166assertEquals ( 167[ "III" , "VI" , "VII" , "V" , "#vi" , "bIII" ]. map ( r => name ( r , Am )), 168[ "C" , "F" , "G" , "E" , "F♯m" , "C♭" ], 169); 170assertEquals ( name ( "VII" , key ( "C" , "mixolydian" )), "B♭" ); 171assertEquals ( name ( "bIV" , key ( "C" , "lydian" )), "F" ); 172// secondaries still count in the major scale of their target: V/III in A minor is G 173assertEquals ( name ( "V7/III" , Am ), "G7" ); 174}); 175 176Deno . test ( "secondary numerals tonicise their target" , () => { 177assertEquals ( name ( "V7/V" ), "D7" ); 178assertEquals ( name ( "V/ii" ), "A" ); 179assertEquals ( name ( "vii°7/V" ), "F♯dim7" ); 180assertEquals ( name ( "V7/IV" , key ( "Bb" )), "B♭7" ); 181}); 182 183Deno . test ( "scale degrees follow the key's mode, and altered degrees borrow" , () => { 184assertEquals ( name ( "1" ), "C" ); 185assertEquals ( name ( "2" ), "Dm" ); 186assertEquals ( name ( "57" ), "G7" ); 187assertEquals ( name ( "17" ), "Cmaj7" ); 188assertEquals ( name ( "6" , key ( "C" , "aeolian" )), "A♭" ); 189assertEquals ( name ( "b6" ), "A♭" ); 190assertEquals ( name ( "b7" ), "B♭" ); 191assertEquals ( name ( "b2" ), "D♭" ); 192assertEquals ( name ( "#4" ), "F♯dim" ); 193}); 194 195Deno . test ( "chord detection from notes" , () => { 196const detect = ( midis :number [], k = C ) => { 197const chord = detectChord ( midis , k ); 198return chord && chordName ( chord ); 199}; 200assertEquals ( detect ([ 48 , 64 , 67 , 70 ]), "C7" ); 201assertEquals ( detect ([ 48 , 52 , 55 , 57 ]), "C6" ); 202assertEquals ( detect ([ 45 , 60 , 64 , 67 ]), "Am7" ); 203assertEquals ( detect ([ 52 , 60 , 67 ]), "C/E" ); 204assertEquals ( detect ([ 48 , 52 , 58 ]), "C7" ); 205assertEquals ( detect ([ 56 , 60 , 63 ]), "A♭" ); 206assertEquals ( detect ([ 61 , 65 , 68 ], key ( "E" )), "C♯" ); 207assertEquals ( detect ([ 60 ]), undefined ); 208assertEquals ( detect ([]), undefined ); 209}); 210 211Deno . test ( "11ths, 13ths, and chords over a foreign bass" , () => { 212const detect = ( midis :number [], k = C ) => { 213const chord = detectChord ( midis , k ); 214return chord && chordName ( chord ); 215}; 216const alternatives = ( midis :number []) => detectChords ( midis , C ). map ( chordName ); 217// e♭ with d f b♭ over it: a b♭ triad on an e♭ bass, or e♭ maj7 with a 2nd for its 3rd 218assertEquals ( detect ([ 39 , 62 , 65 , 70 ]), "E♭maj7sus2" ); 219assert ( alternatives ([ 39 , 62 , 65 , 70 ]). includes ( "B♭/E♭" )); 220assertEquals ( detect ([ 36 , 58 , 62 , 65 ]), "C9sus4" ); // c + b♭ d f: "B♭/C", properly 221assertEquals ( detect ([ 36 , 52 , 58 , 62 , 65 ]), "C11" ); // with the 3rd too 222assertEquals ( detect ([ 36 , 52 , 58 , 62 , 69 ]), "C13" ); 223assertEquals ( detect ([ 36 , 52 , 58 , 69 ]), "C13" ); // shell voicing: 3rd, 7th, 13th 224assertEquals ( detect ([ 36 , 51 , 58 , 62 , 65 ]), "Cm11" ); 225assertEquals ( detect ([ 36 , 52 , 58 , 61 , 69 ]), "C13♭9" ); 226assertEquals ( detect ([ 36 , 52 , 59 , 62 , 66 ]), "Cmaj9♯11" ); 227assertEquals ( detect ([ 36 , 52 , 58 , 63 ]), "C7♯9" ); 228assertEquals ( detect ([ 36 , 52 , 58 , 68 ]), "C7♯5" ); 229assertEquals ( detect ([ 36 , 55 , 58 , 62 , 64 ]), "C9" ); 230assertEquals ( detect ([ 38 , 60 , 64 , 67 ]), "D9sus4" ); // "C/D" 231assertEquals ( detect ([ 36 , 51 , 55 , 56 ]), "A♭maj7/C" ); 232}); 233 234Deno . test ( "every set of notes that gets a name reads back as those notes" , () => { 235// all 2048 sets of intervals above a root. a name may imply a fifth, or a 9th under an 11th 236// or 13th, but must never leave out or add anything else 237for ( let bits = 0 ; bits < 1 << 11 ; bits ++ ) { 238const tones = [ 0 , ...[ ...Array ( 11 ). keys ()]. filter ( i => bits & ( 1 << i )). map ( i => i + 1 )]; 239const described = describeTones ( tones ); 240if ( ! described ) continue ; 241const back = parseChord ( "C" + described . suffix , C ); 242assert ( back , `C ${ described . suffix } doesn't parse (for ${ tones } )` ); 243const extra = back . tones . filter ( t => ! tones . includes ( t )); 244const missing = tones . filter ( t => ! back . tones . includes ( t )); 245assertEquals ( missing , [], `C ${ described . suffix } loses ${ missing } of ${ tones } ` ); 246assert ( 247extra . every ( t => t === 7 || t === 2 ), 248`C ${ described . suffix } adds ${ extra } to ${ tones } ` , 249); 250} 251}); 252 253Deno . test ( "common shapes all get a name" , () => { 254const named = ( tones :number []) => describeTones ( normalizeTones ( tones )) !== undefined ; 255for ( const tones of [ 256[ 0 , 4 , 7 ], 257[ 0 , 3 , 7 ], 258[ 0 , 4 , 10 ], 259[ 0 , 3 , 10 ], 260[ 0 , 4 , 11 ], 261[ 0 , 5 , 10 ], 262[ 0 , 2 , 7 , 11 ], 263[ 0 , 4 , 9 , 10 ], 264[ 0 , 1 , 4 , 10 ], 265]) { 266assert ( named ( tones ), String ( tones )); 267} 268}); 269 270Deno . test ( "notes match a chord only with the same pitches and bass" , () => { 271const c = parseChord ( "C" , C ) ! ; 272assert ( chordMatches ( c , [ 48 , 64 , 67 , 72 ])); 273assert ( ! chordMatches ( c , [ 52 , 60 , 67 ])); 274assert ( ! chordMatches ( c , [ 48 , 64 , 67 , 70 ])); 275assert ( chordMatches ( parseChord ( "C/E" , C ) ! , [ 52 , 60 , 67 ])); 276}); 277 278Deno . test ( "spelling favours the usual names of chromatic degrees" , () => { 279const spell = ( pc :number , k = C ) => 280chordName ({ root :spellPc ( pc , k . tonic ), tones :[ 0 ], suffix :"" }); 281assertEquals ( 282[ 1 , 3 , 6 , 8 , 10 ]. map ( pc => spell ( pc )), 283[ "D♭" , "E♭" , "F♯" , "A♭" , "B♭" ], 284); 285assertEquals ( spell ( 3 , key ( "E" )), "D♯" ); 286assertEquals ( spell ( 11 , key ( "Gb" )), "C♭" ); 287assertEquals ( spell ( 4 , key ( "Gb" )), "F♭" ); 288assertEquals ( spell ( 7 , key ( "Gb" )), "G" ); 289}); 290 291Deno . test ( 292"analysis: the key's axis first, then parallel modes, and secondary dominants" , 293() => { 294const analysis = ( src :string , k = C ) => analyze ( parseChord ( src , k ) ! , k ); 295assertEquals ( analysis ( "Am" ), { roman :"vi" , diatonic :true }); 296// ♭VI, iv and ♭VII are in E♭ major, the flat vertex: but that's no more than C minor, so 297// they're borrowed from that. G♭ major (the inverse) has ♭II 298assertEquals ( analysis ( "Ab" ), { roman :"bVI" , diatonic :false , borrowedFrom :"aeolian" }); 299assertEquals ( analysis ( "Fm" ). vertex , undefined ); 300assertEquals ( analysis ( "Bb" ). vertex , undefined ); 301assertEquals ( analysis ( "Db" ). vertex , "inverse" ); 302// A major (the sharp vertex) has II and III 303assertEquals ( analysis ( "D" ). vertex , "sharp" ); 304// E on its own is A major's: it's only a V/vi on its way to Am 305assertEquals ( analysis ( "E" ), { roman :"III" , diatonic :false , vertex :"sharp" }); 306// nor is D7, with nothing after it: lydian's II7 307assertEquals ( analysis ( "D7" ), { roman :"II7" , diatonic :false , borrowedFrom :"lydian" }); 308assertEquals ( analysis ( "F#dim" ), { roman :"#iv°" , diatonic :false , borrowedFrom :"lydian" }); 309assertEquals ( analysis ( "Eb+" ), { 310roman :"bIII+" , 311diatonic :false , 312borrowedFrom :"melodicMinor" , 313}); 314// in a minor key, from its own axis: C minor's are E♭ and G♭ minor, and A minor, which is 315// C major's notes, so borrowing from that 316assertEquals ( analysis ( "G" , key ( "C" , "aeolian" )). borrowedFrom , "ionian" ); 317assertEquals ( analysis ( "G" , key ( "C" , "aeolian" )). vertex , undefined ); 318assertEquals ( analysis ( "Db" , key ( "C" , "aeolian" )). vertex , "flat" ); 319}, 320); 321 322Deno . test ( "palette romans read back as the same chords, in every key and mode" , () => { 323for ( const tonic of [ "C" , "Eb" , "F#" , "Bb" , "Db" , "B" ]) { 324for ( const mode of Object . keys ( MODES ) as ModeId []) { 325const k = key ( tonic , mode ); 326for ( const sevenths of [ false , true ]) { 327for ( const row of palette ( k , sevenths )) { 328for ( const { chord, symbol} of row . entries ) { 329const back = parseChord ( symbol , k ); 330assertEquals ( 331back && chordPcs ( back ), 332chordPcs ( chord ), 333` ${ symbol } in ${ tonic } ${ mode } ` , 334); 335assertEquals ( back && pcOf ( back . root ), pcOf ( chord . root )); 336assertEquals ( back && romanOf ( back , k ), symbol ); 337} 338} 339} 340} 341} 342}); 343 344Deno . test ( "what to point out: the near vertices, then parallel modes, then the inverse" , () => { 345const from = ( src :string , k = C ) => origin ( analyze ( parseChord ( src , k ) ! , k )); 346assertEquals ( 347[ "C" , "Am" , "G7" ]. map ( c => from ( c )), 348[ "home" , "home" , "home" ], 349); 350// A♭ is in the parallel minor; D is in the sharp vertex (A major), but lydian explains it 351assertEquals ([ from ( "Ab" ), from ( "Fm" ), from ( "D" )], [ "aeolian" , "aeolian" , "lydian" ]); 352// D♭ is in the inverse vertex (G♭) and in phrygian: the mode wins 353assertEquals ( from ( "Db" ), "phrygian" ); 354// G♭ is on the inverse vertex and in locrian; B is only on the inverse vertex (G♭ major's C♭) 355assertEquals ([ from ( "Gb" ), from ( "B" )], [ "locrian" , "inverse" ]); 356assertEquals ( from ( "F#dim" ), "lydian" ); 357assertEquals ( from ( "E+" ), undefined ); 358}); 359 360Deno . test ( "borrowing is from the parallel major or minor first" , () => { 361const borrowed = ( src :string , k :Key ) => analyze ( parseChord ( src , k ) ! , k ). borrowedFrom ; 362// ♭VII in major: aeolian has it as well as mixolydian 363assertEquals ( borrowed ( "Bb" , C ), "aeolian" ); 364// IV in minor: ionian has it as well as dorian 365assertEquals ( borrowed ( "F" , key ( "C" , "aeolian" )), "ionian" ); 366// in neither: the nearest other mode 367assertEquals ([ borrowed ( "D" , C ), borrowed ( "Db" , C )], [ "lydian" , "phrygian" ]); 368// a mode in neither: its parallel minor (D minor’s ♭VI), then major 369assertEquals ( borrowed ( "Bb" , key ( "D" , "dorian" )), "aeolian" ); 370assertEquals ( borrowed ( "F#m" , key ( "D" , "dorian" )), "ionian" ); 371}); 372 373Deno . test ( "the palette: the key, the vertices of its axis, then its parallel modes" , () => { 374const rows = ( k :Key ) => 375palette ( k , false ). map ( r => ` ${ r . relation } ${ chordName ( r . entries [ 0 ]. chord )} ${ r . key . mode } ` ); 376// the vertex that's only the parallel minor (or major) is that, in its place on the axis 377assertEquals ( rows ( key ( "C" )). slice ( 0 , 4 ), [ 378"home C ionian" , 379"parallel Cm aeolian" , 380"sharp A ionian" , 381"inverse G♭ ionian" , 382]); 383assertEquals ( palette ( key ( "C" ), false )[ 1 ]. vertex , "flat" ); 384assertEquals ( rows ( key ( "A" , "aeolian" )). slice ( 0 , 4 ), [ 385"home Am aeolian" , 386"flat Cm aeolian" , 387"parallel A ionian" , 388"inverse E♭m aeolian" , 389]); 390// and isn't there again among the other modes 391assertEquals ( palette ( key ( "C" ), false ). filter ( r => r . key . mode === "aeolian" ). length , 1 ); 392// where none of them is, they're all there 393assertEquals ( 394palette ( key ( "D" , "dorian" ), false ) 395. slice ( 1 , 4 ) 396. map ( r => r . relation ), 397[ "flat" , "sharp" , "inverse" ], 398); 399// no double accidentals in anything reached from the key 400for ( const tonic of [ "C" , "G" , "F" , "A" , "Eb" , "B" , "F#" , "Db" ]) { 401for ( const mode of [ "ionian" , "aeolian" , "dorian" ] as ModeId []) { 402for ( const row of palette ( key ( tonic , mode ), true )) { 403for ( const { symbol} of row . entries ) 404assert ( ! /bb|## / . test ( symbol ), ` ${ tonic } ${ mode } : ${ symbol } ` ); 405} 406} 407} 408// chords from the vertices are written relative to the key, so they follow it 409const sharp = palette ( key ( "C" ), false )[ 2 ]. entries . map ( e => e . symbol ); 410assertEquals ( sharp , [ "VI" , "vii" , "bii" , "II" , "III" , "#iv" , "#v°" ]); 411}); 412 413Deno . test ( "voicings contain the chord, with the bass at the bottom" , () => { 414for ( const src of [ "C" , "Am7" , "F/A" , "G13" , "Dm7b5" , "E7alt" , "C5" ]) { 415const chord = parseChord ( src , C ) ! ; 416const notes = voiceChord ( chord ); 417const bass = pcOf ( chord . bass ?? chord . root ); 418assertEquals ((( notes [ 0 ] % 12 ) + 12 ) % 12 , bass , src ); 419assert ( 420notes . every (( n , i ) => i === 0 || n > notes [ 0 ]), 421src , 422); 423const sounding = new Set ( notes . map ( n => n % 12 )); 424const essential = chordPcs ( chord ). filter ( pc => pc !== ( pcOf ( chord . root ) + 7 ) % 12 ); 425assert ( 426essential . every ( pc => sounding . has ( pc )), 427` ${ src } : ${ notes } ` , 428); 429} 430}); 431 432Deno . test ( "auto voicings move smoothly through a progression" , () => { 433let prev :number [] | undefined ; 434for ( const src of [ "C" , "Am" , "F" , "G7" , "C" , "Ab" , "Bb" , "C" ]) { 435const notes = voiceChord ( parseChord ( src , C ) ! , prev ); 436if ( prev ) { 437const [, ...a ] = prev ; 438const [, ...b ] = notes ; 439const worst = Math . max ( ...b . map ( x => Math . min ( ...a . map ( y => Math . abs ( x - y ))))); 440assert ( worst <= 4 , ` ${ src } : ${ a } -> ${ b } ` ); 441} 442prev = notes ; 443} 444}); 445 446Deno . test ( "inverting a voicing keeps its pitch classes and bass" , () => { 447const notes = voiceChord ( parseChord ( "Cmaj7" , C ) ! ); 448const up = invertVoicing ( notes , 1 ); 449const down = invertVoicing ( notes , - 1 ); 450for ( const v of [ up , down ]) { 451assertEquals ( v [ 0 ], notes [ 0 ]); 452assertEquals ( new Set ( v . map ( n => n % 12 )), new Set ( notes . map ( n => n % 12 ))); 453} 454assert ( up . at ( - 1 ) ! > notes . at ( - 1 ) ! ); 455assert ( down . at ( - 1 ) ! < notes . at ( - 1 ) ! ); 456}); 457 458Deno . test ( "stepping through the scale" , () => { 459const up = ( pitch :number , k :Key , by = 1 ) => stepInScale ( pitch , k , by ); 460// C major, from middle C: D, E, … and back 461assertEquals ( 462[ 1 , 2 , 3 , 6 , 7 ]. map ( n => up ( 60 , C , n )), 463[ 62 , 64 , 65 , 71 , 72 ], 464); 465assertEquals ( 466[ - 1 , - 2 , - 7 ]. map ( n => up ( 60 , C , n )), 467[ 59 , 57 , 48 ], 468); 469assertEquals ([ up ( 64 , C ), up ( 71 , C ), up ( 72 , C , - 1 )], [ 65 , 72 , 71 ]); 470// in the key's mode, from its tonic 471assertEquals ( up ( 69 , key ( "A" , "aeolian" ), 2 ), 72 ); 472assertEquals ( up ( 62 , key ( "D" , "dorian" ), 2 ), 65 ); 473assertEquals ( up ( 68 , key ( "Ab" , "ionian" ), 3 ), 73 ); 474// out of the scale: keeps its inflection 475assertEquals ([ up ( 61 , C ), up ( 61 , C , - 1 ), up ( 68 , key ( "A" , "aeolian" ))], [ 63 , 60 , 70 ]); 476// low and high 477assertEquals ([ up ( 0 , C ), up ( 1 , key ( "B" , "locrian" ), - 1 )], [ 2 , 0 ]); 478}); 479 480Deno . test ( 481"dominants are named for where they lead, and coloured for where their notes are from" , 482() => { 483const read = ( k :Key , progression :string []) => { 484const chords = progression . map ( s => parseChord ( s , k ) ! ); 485return chords . map (( c , i ) => { 486const a = analyze ( c , k , chords [ i + 1 ]); 487return [ origin ( a ), a . secondary ]. filter ( Boolean ). join ( " " ); 488}); 489}; 490// stardust speedway: a sus dominant into VI, with phrygian's B♭; a tritone sub down into i, 491// with the flat vertex's A♭; and the ii9, with the parallel major's F♯ and C♯ 492assertEquals ( 493read ( key ( "A" , "aeolian" ), [ 494"Am9" , 495"C11no3" , 496"Fmaj7" , 497"Bb9add13" , 498"Am9" , 499"Bm9" , 500"Bb9add13" , 501"Am9" , 502]), 503[ "home" , "phrygian V7/VI" , "home" , "flat subV7" , "home" , "ionian" , "flat subV7" , "home" ], 504); 505// in C, E7 on its way to Am has the sharp vertex's G♯ (A major's: A minor's parallel major), 506// D7 on its way to G lydian's F♯, and D♭7 on its way home the inverse's D♭ and C♭ 507assertEquals ( read ( C , [ "D7" , "G7" , "C" , "E7" , "Am" , "Db7" , "C" ]), [ 508"lydian V7/V" , 509"home" , 510"home" , 511"sharp V7/vi" , 512"home" , 513"inverse subV7" , 514"home" , 515]); 516// a dominant only when it goes where it leads, seventh or not: B♭7 back to C is the minor 517// key's ♭VII7, A that doesn't go on to Dm is the sharp vertex's VI, and in C minor, D7 sliding 518// down to D♭7 is lydian's II7 519assertEquals ( read ( C , [ "E" , "Am" , "Bb7" , "C" , "A" , "F" ]), [ 520"sharp V/vi" , 521"home" , 522"aeolian" , 523"home" , 524"sharp" , 525"home" , 526]); 527assertEquals ( read ( key ( "C" , "aeolian" ), [ "Cm" , "Eb7" , "D7" , "Db7" , "Cm" ]), [ 528"home" , 529"phrygian" , 530"lydian" , 531"flat subV7" , 532"home" , 533]); 534// a minor key's major V is the parallel major's, and needs no name as a dominant 535assertEquals ( read ( key ( "D" , "aeolian" ), [ "Dm" , "A" , "Dm" , "A7" , "Dm" ]), [ 536"home" , 537"ionian" , 538"home" , 539"ionian" , 540"home" , 541]); 542}, 543); 544 545// the examples from 8-bit music theory's video on relative axis interchange 546Deno . test ( "modal interchange first; the axis for what it can't explain" , () => { 547const from = ( src :string , k :Key ) => origin ( analyze ( parseChord ( src , k ) ! , k )); 548// "vii minor 7 is modal interchange from lydian, but vii minor 9 is axis interchange" 549assertEquals ([ from ( "Bm7" , C ), from ( "Bm9" , C )], [ "lydian" , "sharp" ]); 550// sharp vertex chords in major: kirby's ♯iv minor 9, and VI major 7 551assertEquals ([ from ( "F#m9" , C ), from ( "Amaj7" , C )], [ "sharp" , "sharp" ]); 552// stardust speedway, in A minor: ii minor 9 "is pretty much only found in our parallel major"; 553// in a minor key that's the sharp vertex, so it goes by that 554const am = key ( "A" , "aeolian" ); 555assertEquals ( from ( "Bm9" , am ), "ionian" ); 556// fire emblem, in G minor: ♭II "from the slightly darker phrygian", not the flat vertex 557assertEquals ( from ( "Ab" , key ( "G" , "aeolian" )), "phrygian" ); 558// death mountain, in A minor: ♭iii minor "could technically be thought of as modal interchange 559// from parallel locrian", but is heard as the flat vertex's tonic 560assertEquals ( from ( "Cm" , am ), "flat" ); 561// slay the spire, in F minor: A♭ minor from the flat vertex, B minor the inverse's tonic 562const fm = key ( "F" , "aeolian" ); 563assertEquals ([ from ( "Abm" , fm ), from ( "Bm" , fm )], [ "flat" , "inverse" ]); 564// napple tale, in E♭: C major the sharp vertex's tonic, D major the inverse's IV 565const eb = key ( "Eb" ); 566assertEquals ([ from ( "C" , eb ), from ( "D" , eb )], [ "sharp" , "inverse" ]); 567// locrian still comes before the inverse: ♭V maj7 is "most often heard as modal interchange 568// from parallel locrian" 569assertEquals ( from ( "Gbmaj7" , C ), "locrian" ); 570});