From f6ebbd37b319a6e073b4509a9f361eae9b4c4624 Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Tue, 21 May 2024 15:58:38 +0300 Subject: [PATCH] Generalize Diamond-mos notation to absurd scales ref #27 --- src/__tests__/notation.spec.ts | 29 ++++++++++++++++--- src/notation.ts | 52 ++++++++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/__tests__/notation.spec.ts b/src/__tests__/notation.spec.ts index 730e71c..4733e8a 100644 --- a/src/__tests__/notation.spec.ts +++ b/src/__tests__/notation.spec.ts @@ -1,8 +1,24 @@ import {describe, expect, it} from 'vitest'; -import {generateNotation} from '../notation'; +import {generateNotation, nthNominal} from '../notation'; import {dot} from 'xen-dev-utils'; -describe('Diamond mos notation generator', () => { +describe('Generalized Diamond-mos nominals', () => { + it('has 17 standard nominals', () => { + expect([...Array(17).keys()].map(nthNominal).join('')).toBe( + 'JKLMNOPQRSTUVWXYZ' + ); + }); + it('has 289 two-character nominals', () => { + for (let i = 0; i < 289; ++i) { + expect(nthNominal(17 + i)).toHaveLength(2); + } + }); + it('has JJJ as the first three-character nominal', () => { + expect(nthNominal(17 + 17 * 17)).toBe('JJJ'); + }); +}); + +describe('Diamond-mos notation generator', () => { it('generates the config for diatonic major', () => { const notation = generateNotation('LLsLLLs'); const basic = [2, 1]; @@ -141,8 +157,13 @@ describe('Diamond mos notation generator', () => { expect(scale.has('Z')).toBe(true); }); - it('rejects above nominal Z', () => { - expect(() => generateNotation('LsLsLsLsLsLsLsLsLs')).toThrow(); + it('accepts above nominal Z', () => { + const {scale} = generateNotation('LsLsLsLsLsLsLsLsLs'); + expect(scale.has('J')).toBe(true); + expect(scale.has('Z')).toBe(true); + expect(scale.has('JJ')).toBe(true); + expect(scale.has('JK')).toBe(false); + expect(scale.has('KJ')).toBe(false); }); it('it rejects all L', () => { diff --git a/src/notation.ts b/src/notation.ts index a18c573..cfa046d 100644 --- a/src/notation.ts +++ b/src/notation.ts @@ -4,7 +4,7 @@ import {mosGeneratorMonzo} from './helpers'; /** * Valid nominals for absolute pitches in [Diamond mos notation](https://en.xen.wiki/w/Diamond-mos_notation). */ -export type DiamondMosNominal = +export type DiamondMosAlphabet = | 'J' | 'K' | 'L' @@ -53,7 +53,7 @@ export type DiamondMosNotation = { /** * Counts of [L, s] steps for every available nominal with J at unison. Add equaves to reach other octaves. */ - scale: Map; + scale: Map; /** * Interval of equivalence / octave. */ @@ -72,6 +72,45 @@ export type DiamondMosNotation = { brightGenerator: MosMonzo; }; +/** Single characters of valid nominals. */ +export const DIAMOND_MOS_ALPHABET: DiamondMosAlphabet[] = [ + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z', +]; + +/** + * Obtain the 0-indexed nth generalized Diamond-mos nominal. + * @param n Index of the nominal. + * @returns A single character from J through Z or a multi-character string like 'JJ' starting from n = 17. + */ +export function nthNominal(n: number): string { + if (n < 0 || !Number.isInteger(n)) { + throw new Error('Invalid nominal index'); + } + if (n >= DIAMOND_MOS_ALPHABET.length) { + return ( + nthNominal(Math.floor(n / DIAMOND_MOS_ALPHABET.length) - 1) + + DIAMOND_MOS_ALPHABET[n % DIAMOND_MOS_ALPHABET.length] + ); + } + return DIAMOND_MOS_ALPHABET[n]; +} + /** * Generate configuration for [Diamond mos notation](https://en.xen.wiki/w/Diamond-mos_notation). * @@ -80,13 +119,13 @@ export type DiamondMosNotation = { * @returns Configuration for notation software. */ export function generateNotation(mode: string): DiamondMosNotation { - const scale = new Map(); - let code = 'J'.charCodeAt(0); + const scale = new Map(); + let i = 0; const monzo: MosMonzo = [0, 0]; let hasLarge = false; let hasSmall = false; for (const character of mode) { - scale.set(String.fromCharCode(code++) as DiamondMosNominal, [...monzo]); + scale.set(nthNominal(i++), [...monzo]); if (character === 'L') { monzo[0]++; hasLarge = true; @@ -100,9 +139,6 @@ export function generateNotation(mode: string): DiamondMosNotation { if (!hasLarge || !hasSmall) { throw new Error("The scale must contain both 'L' and 's' steps."); } - if (code > 'Z'.charCodeAt(0) + 1) { - throw new Error('Out of Diamond mos nominals.'); - } const equave: MosMonzo = [...monzo]; const numPeriods = gcd(equave[0], equave[1]); const period: MosMonzo = [equave[0] / numPeriods, equave[1] / numPeriods];