import { assert, assertEquals } from "@std/assert"; import { isKeyRelative } from "../theory/chord.ts"; import { rekey, unpinRedundant, voiceTrack } from "./harmony.ts"; import { type ChordTrack, newChordTrack, newProject, placeChord, type Project, projectKey, } from "./model.ts"; const SYMBOLS = ["I", "vi", "IV/A", "V7/V", "bVI", "iiø7", "4", "57", "Cmaj7", "b7", "ii"]; function sketch(): Project { const p = newProject(); const track = newChordTrack(); SYMBOLS.forEach((symbol, i) => placeChord(track, { id: `${i}`, start: i * 4, dur: 4, symbol }), ); track.chords[10].notes = [38, 57, 62, 65]; // ii, hand-voiced p.tracks = [track]; return p; } const chords = (p: Project) => p.tracks[0] as ChordTrack; const sounding = (p: Project) => voiceTrack(chords(p), projectKey(p)).map(v => v.notes); const symbols = (p: Project) => chords(p).chords.map(c => c.symbol); Deno.test("changing key or mode changes no voicing", () => { const p = sketch(); const before = sounding(p); for (const key of [ { tonic: "A", mode: "aeolian" }, { tonic: "Eb", mode: "dorian" }, { tonic: "F#", mode: "lydian" }, { tonic: "C", mode: "ionian" }, ] as const) { rekey(p, key); assertEquals(sounding(p), before, `${key.tonic} ${key.mode}: ${symbols(p)}`); } // everything written relative to the key still is, though a scale degree the key has no chord // for (F major in E♭ dorian) has to become a roman numeral for (const [i, symbol] of symbols(p).entries()) { if (SYMBOLS[i] !== "Cmaj7") assert(isKeyRelative(symbol!), symbol); } }); Deno.test("chords written relative to the key stay that way, renamed for the new one", () => { const p = sketch(); rekey(p, { tonic: "A", mode: "aeolian" }); assertEquals(symbols(p), [ "III", "i", "VI/A", "IV7", "#VII", "ivø7", "6", "77", "Cmaj7", "bII", "iv", ]); for (const [i, symbol] of symbols(p).entries()) { if (SYMBOLS[i] !== "Cmaj7") assert(isKeyRelative(symbol!), symbol); } }); function threeChords(): Project { const p = newProject(); const track = newChordTrack(); for (const [i, symbol] of ["C", "Am", "F"].entries()) { placeChord(track, { id: symbol, start: i * 4, dur: 4, symbol }); } p.tracks = [track]; return p; } const pinned = (p: Project) => chords(p).chords.map(c => c.notes !== undefined); Deno.test("hand-set notes that are just the auto-voicing get unpinned", () => { const p = threeChords(); const auto = sounding(p); // as if Am were inverted up, then back down chords(p).chords[1].notes = [...auto[1]]; const settled = unpinRedundant(p); assertEquals(pinned(settled), [false, false, false]); assertEquals(sounding(settled), auto); }); Deno.test("a real hand-voicing stays pinned, and so does what follows it", () => { const p = threeChords(); const [bass, ...upper] = sounding(p)[1]; chords(p).chords[1].notes = [bass, ...upper.map(n => n + 12)]; const settled = unpinRedundant(p); assertEquals(pinned(settled), [false, true, false]); assertEquals(settled, p); // nothing to change: the very same project back }); Deno.test("notes without a symbol, voiced as their chord would be, become that chord", () => { const p = threeChords(); const auto = sounding(p); const am = chords(p).chords[1]; delete am.symbol; am.notes = [...auto[1]]; const settled = unpinRedundant(p); assertEquals(chords(settled).chords[1], { id: "Am", start: 4, dur: 4, symbol: "Am" }); assertEquals(sounding(settled), auto); });