From 7b6331b7e912e0083a2a4994b563d746c157af46 Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Wed, 8 May 2024 09:05:51 +0300 Subject: [PATCH] Reserve more patterns that could be used for relative or absolute notation Reserve all capital letters as pitch nominals. Reserve all lowercase Greek letters as pitch nominals. Reserve all relative intervals ending in 'ms' for MOS-steps. Replace '=' with '_' as the natural ASCII accidental. Bring back semiquartal nicknames as they can no longer be constructed in-language. Add stubs for a Diamond mos system. ref #308 --- documentation/advanced-dsl.md | 11 +- examples/notation/10edo-neutral.sw | 10 +- examples/notation/10edo-pentanominal.sw | 8 +- examples/notation/11edo.sw | 16 +- examples/notation/12edo.sw | 16 +- examples/notation/13edo-antidiatonic.sw | 16 +- examples/notation/13edo-archeotonic.sw | 8 +- examples/notation/13edo-pentanominal.sw | 12 +- examples/notation/17edo.sw | 16 +- examples/notation/18edo-antineutral.sw | 16 +- examples/notation/19edo-diatonic.sw | 16 +- examples/notation/21edo.sw | 16 +- examples/notation/23edo-antidiatonic.sw | 16 +- examples/notation/24edo-neutral.sw | 16 +- examples/notation/25edo-antidiatonic.sw | 16 +- examples/notation/3edo.sw | 6 +- examples/notation/4edo.sw | 6 +- examples/notation/5edo.sw | 4 +- examples/notation/6edo.sw | 8 +- examples/notation/8edo.sw | 6 +- examples/notation/9edo.sw | 16 +- examples/pythagoras12.sw | 2 +- src/__tests__/pythagorean.spec.ts | 2 +- src/diamond-mos.ts | 195 +++++++++++++++++++ src/expression.ts | 15 +- src/grammars/sonic-weave.pegjs | 83 +++++++- src/parser/__tests__/expression.spec.ts | 6 +- src/parser/__tests__/sonic-weave-ast.spec.ts | 25 ++- src/parser/__tests__/source.spec.ts | 30 +-- src/parser/__tests__/stdlib.spec.ts | 2 +- src/parser/expression.ts | 41 +++- src/pythagorean.ts | 73 ++++++- src/stdlib/builtin.ts | 2 +- 33 files changed, 565 insertions(+), 166 deletions(-) create mode 100644 src/diamond-mos.ts diff --git a/documentation/advanced-dsl.md b/documentation/advanced-dsl.md index 2b9c94af..e31b0944 100644 --- a/documentation/advanced-dsl.md +++ b/documentation/advanced-dsl.md @@ -393,9 +393,16 @@ C♮5 "Octave" ``` #### Nicknames -Lumi, the lead developer, likes to call "C + P4 / 2" "φ" or "phi". Its octave complement is called "ψ" or "psi" and "beta half-flat" has the nickname "χ" or "chi". The last interordinal would be "ω" or "ome(ga)" but it tends to jump around depending on the mood and if the semiquartal scale was sullied by adding a flat sign ♭ in the mix or not. +The semifourth against C has a nickname "φ" or "phi". Other nicknames include: -Absolute semiquartals might make a comeback as an opt-in, but for now they've been excluded from the main grammar. +| Expression | Standard | Nickname | ASCII | +| ------------- | -------- | -------- | ------ | +| `C4 + P4 / 2` | `αd4` | `φ4` | `phi4` | +| `C5 - P4 / 2` | `εd4` | `ψ4` | `psi4` | +| `φ4 + M2` | `βd4` | `χ4` | `chi4` | +| `ψ4 + M2` | `ζt4` | `ω4` | `ome4` | + +The scale C, D, φ, χ, F, G, A, ψ, ω, (C) is the 6|2 (*Stellerian*) mode of [5L 4s](https://en.xen.wiki/w/5L_4s) spellable without accidentals. ## Quarter-augmented Pythagorean notation As previously mentioned the fifth spans 4 degrees so we can split it again without breaking the ordinal notation. diff --git a/examples/notation/10edo-neutral.sw b/examples/notation/10edo-neutral.sw index 14d09082..0c87da49 100644 --- a/examples/notation/10edo-neutral.sw +++ b/examples/notation/10edo-neutral.sw @@ -1,14 +1,14 @@ "Recommended notation for 10 divisions of the octave" -C=4 = mtof(60) +C_4 = mtof(60) Dd4 -D=4 +D_4 Ed4 -F=4 +F_4 Gd4 -G=4 +G_4 Ad4 Bb4 Bd4 -C=5 +C_5 10@ diff --git a/examples/notation/10edo-pentanominal.sw b/examples/notation/10edo-pentanominal.sw index 3d7eb8e4..b0c1e8b5 100644 --- a/examples/notation/10edo-pentanominal.sw +++ b/examples/notation/10edo-pentanominal.sw @@ -1,14 +1,14 @@ "Recommended notation for 20 divisions of the octave" C♮4 = mtof(60) ^C4 -αd4 "φ♮4" -^αd4 "^φ4" +φ♮4 +^φ4 F♮4 ^F4 G♮4 ^G4 -εd4 "ψ♮4" -^εd4 "^ψ4" +ψ♮4 +^ψ4 C♮5 10@ diff --git a/examples/notation/11edo.sw b/examples/notation/11edo.sw index 16ce59ff..58c2089e 100644 --- a/examples/notation/11edo.sw +++ b/examples/notation/11edo.sw @@ -1,15 +1,15 @@ "Recommended notation for 11 divisions of the octave" -C=4 = mtof(60) -D=4 -E=4 +C_4 = mtof(60) +D_4 +E_4 ^E4 vF4 -F=4 -G=4 -A=4 -B=4 +F_4 +G_4 +A_4 +B_4 ^B4 vC5 -C=5 +C_5 11@ diff --git a/examples/notation/12edo.sw b/examples/notation/12edo.sw index 234baabb..f4b44cd2 100644 --- a/examples/notation/12edo.sw +++ b/examples/notation/12edo.sw @@ -1,16 +1,16 @@ "Recommended notation for 12 divisions of the octave" -C=4 = mtof(60) +C_4 = mtof(60) C#4 -D=4 +D_4 Eb4 -E=4 -F=4 +E_4 +F_4 F#4 -G=4 +G_4 G#4 -A=4 +A_4 Bb4 -B=4 -C=5 +B_4 +C_5 12@ diff --git a/examples/notation/13edo-antidiatonic.sw b/examples/notation/13edo-antidiatonic.sw index 29a3fbb7..baa26ab0 100644 --- a/examples/notation/13edo-antidiatonic.sw +++ b/examples/notation/13edo-antidiatonic.sw @@ -1,17 +1,17 @@ "Recommended notation for 13 divisions of the octave" -C=4 = mtof(60) -D=4 -E=4 +C_4 = mtof(60) +D_4 +E_4 ^E4 vvF4 vF4 -F=4 -G=4 -A=4 -B=4 +F_4 +G_4 +A_4 +B_4 ^B4 ^^B4 vC5 -C=5 +C_5 13b@ diff --git a/examples/notation/13edo-archeotonic.sw b/examples/notation/13edo-archeotonic.sw index 07051a52..cf80d631 100644 --- a/examples/notation/13edo-archeotonic.sw +++ b/examples/notation/13edo-archeotonic.sw @@ -1,9 +1,9 @@ "Recommended notation for 13 divisions of the octave" -C=4 = mtof(60) +C_4 = mtof(60) ^C4 -D=4 +D_4 ^D4 -E=4 +E_4 ^E4 F#4 ^F#4 @@ -12,6 +12,6 @@ Ab4 vBb4 Bb4 vC5 -C=5 +C_5 13@2.9 diff --git a/examples/notation/13edo-pentanominal.sw b/examples/notation/13edo-pentanominal.sw index bc30e4e8..26857481 100644 --- a/examples/notation/13edo-pentanominal.sw +++ b/examples/notation/13edo-pentanominal.sw @@ -1,17 +1,17 @@ "Recommended notation for 13 divisions of the octave" -C=4 = mtof(60) +C_4 = mtof(60) ^C4 vD4 -D=4 +D_4 ^D4 -F=4 +F_4 ^F4 vG4 -G=4 +G_4 ^G4 vA4 -A=4 +A_4 ^A4 -C=5 +C_5 13@ diff --git a/examples/notation/17edo.sw b/examples/notation/17edo.sw index e90b77ed..528c3d56 100644 --- a/examples/notation/17edo.sw +++ b/examples/notation/17edo.sw @@ -1,21 +1,21 @@ "Recommended notation for 17 divisions of the octave" -C=4 = mtof(60) +C_4 = mtof(60) Ct4 Dd4 -D=4 +D_4 Dt4 Ed4 -E=4 -F=4 +E_4 +F_4 Ft4 Gd4 -G=4 +G_4 Gt4 Ad4 -A=4 +A_4 At4 Bd4 -B=4 -C=5 +B_4 +C_5 17@ diff --git a/examples/notation/18edo-antineutral.sw b/examples/notation/18edo-antineutral.sw index 7e476937..579d3299 100644 --- a/examples/notation/18edo-antineutral.sw +++ b/examples/notation/18edo-antineutral.sw @@ -1,22 +1,22 @@ "Recommended notation for 18 divisions of the octave" -C=4 = mtof(60) +C_4 = mtof(60) Cd4 -D=4 +D_4 Dd4 -E=4 +E_4 Ed4 Eb4 Ft4 -F=4 +F_4 Gt4 -G=4 +G_4 At4 -A=4 +A_4 Ab4 -B=4 +B_4 Bd4 Bb4 Ct5 -C=5 +C_5 18b@ diff --git a/examples/notation/19edo-diatonic.sw b/examples/notation/19edo-diatonic.sw index 2f6f4288..537b8963 100644 --- a/examples/notation/19edo-diatonic.sw +++ b/examples/notation/19edo-diatonic.sw @@ -1,23 +1,23 @@ "Recommended notation for 19 divisions of the octave" -C=4 = mtof(60) +C_4 = mtof(60) C#4 Db4 -D=4 +D_4 D#4 Eb4 -E=4 +E_4 E#4 -F=4 +F_4 F#4 Gb4 -G=4 +G_4 G#4 Ab4 -A=4 +A_4 A#4 Bb4 -B=4 +B_4 B#4 -C=5 +C_5 19@ diff --git a/examples/notation/21edo.sw b/examples/notation/21edo.sw index e7379153..8ffb196b 100644 --- a/examples/notation/21edo.sw +++ b/examples/notation/21edo.sw @@ -1,25 +1,25 @@ "Recommended notation for 21 divisions of the octave" -C=4 = mtof(60) +C_4 = mtof(60) ^C4 vD4 -D=4 +D_4 ^D4 vE4 -E=4 +E_4 ^E4 vF4 -F=4 +F_4 ^F4 vG4 -G=4 +G_4 ^G4 vA4 -A=4 +A_4 ^A4 vB4 -B=4 +B_4 ^B4 vC5 -C=5 +C_5 21@ diff --git a/examples/notation/23edo-antidiatonic.sw b/examples/notation/23edo-antidiatonic.sw index bc88996d..750a7749 100644 --- a/examples/notation/23edo-antidiatonic.sw +++ b/examples/notation/23edo-antidiatonic.sw @@ -1,27 +1,27 @@ "Recommended notation for 23 divisions of the octave" -C=4 = mtof(60) +C_4 = mtof(60) ^C4 vD4 -D=4 +D_4 ^D4 vE4 -E=4 +E_4 ^E4 ^^E4 vF4 -F=4 +F_4 ^F4 vG4 -G=4 +G_4 ^G4 vA4 -A=4 +A_4 ^A4 vB4 -B=4 +B_4 ^B4 ^^B4 vC5 -C=5 +C_5 23@ diff --git a/examples/notation/24edo-neutral.sw b/examples/notation/24edo-neutral.sw index 8e749d41..16d5af04 100644 --- a/examples/notation/24edo-neutral.sw +++ b/examples/notation/24edo-neutral.sw @@ -1,28 +1,28 @@ "Recommended notation for 24 divisions of the octave" -C=4 = mtof(60) +C_4 = mtof(60) Ct4 C#4 Dd4 -D=4 +D_4 Dt4 Eb4 Ed4 -E=4 +E_4 Et4 -F=4 +F_4 Ft4 F#4 Gd4 -G=4 +G_4 Gt4 G#4 Ad4 -A=4 +A_4 At4 Bb4 Bd4 -B=4 +B_4 Bt4 -C=5 +C_5 24@ diff --git a/examples/notation/25edo-antidiatonic.sw b/examples/notation/25edo-antidiatonic.sw index a08e4612..0b8a363c 100644 --- a/examples/notation/25edo-antidiatonic.sw +++ b/examples/notation/25edo-antidiatonic.sw @@ -1,29 +1,29 @@ "Recommended notation for 25 divisions of the octave" -C=4 = mtof(60) +C_4 = mtof(60) ^C4 vD4 -D=4 +D_4 ^D4 vE4 -E=4 +E_4 ^E4 ^^E4 // Or Eb4 if you don't mind the direction vvF4 // Or F#4 if you don't mind the direction vF4 -F=4 +F_4 ^F4 vG4 -G=4 +G_4 ^G4 vA4 -A=4 +A_4 ^A4 vB4 -B=4 +B_4 ^B4 ^^B4 // Or Bb4 vvC5 // Or Cb5 vC5 -C=5 +C_5 25b@ diff --git a/examples/notation/3edo.sw b/examples/notation/3edo.sw index 36cf2e64..bed0fb22 100644 --- a/examples/notation/3edo.sw +++ b/examples/notation/3edo.sw @@ -1,7 +1,7 @@ "Recommended notation for 3 divisions of the octave" -C=4 = mtof(60) -E=4 +C_4 = mtof(60) +E_4 Ab4 -C=5 +C_5 3@2.81 diff --git a/examples/notation/4edo.sw b/examples/notation/4edo.sw index 5dd27395..f99a8b3a 100644 --- a/examples/notation/4edo.sw +++ b/examples/notation/4edo.sw @@ -1,8 +1,8 @@ "Recommended notation for 4 divisions of the octave" -C=4 = mtof(60) +C_4 = mtof(60) Eb4 F#4 -A=4 -C=5 +A_4 +C_5 4@2.27 diff --git a/examples/notation/5edo.sw b/examples/notation/5edo.sw index 9905c378..2a093732 100644 --- a/examples/notation/5edo.sw +++ b/examples/notation/5edo.sw @@ -1,9 +1,9 @@ "Recommended notation for 5 divisions of the octave" C4 = mtof(60) -αd4 "φ4" +φ4 F4 G4 -εd4 "ψ4" +ψ4 C5 5@ diff --git a/examples/notation/6edo.sw b/examples/notation/6edo.sw index f4fa07f7..5c7f2564 100644 --- a/examples/notation/6edo.sw +++ b/examples/notation/6edo.sw @@ -1,10 +1,10 @@ "Recommended notation for 6 divisions of the octave" -C=4 = mtof(60) -D=4 -E=4 +C_4 = mtof(60) +D_4 +E_4 Gb4 Ab4 Bb4 -C=5 +C_5 6@2.9 diff --git a/examples/notation/8edo.sw b/examples/notation/8edo.sw index 48f1d5c0..e5f515c1 100644 --- a/examples/notation/8edo.sw +++ b/examples/notation/8edo.sw @@ -1,12 +1,12 @@ "Recommended notation for 8 divisions of the octave" -C=4 = mtof(60) +C_4 = mtof(60) Dd4 Eb4 Et4 F#4 Gt4 -A=4 +A_4 Bd4 -C=5 +C_5 8@2.27 diff --git a/examples/notation/9edo.sw b/examples/notation/9edo.sw index 28630a8a..0460f4d5 100644 --- a/examples/notation/9edo.sw +++ b/examples/notation/9edo.sw @@ -1,13 +1,13 @@ "Recommended notation for 9 divisions of the octave" -C=4 = mtof(60) -D=4 -E=4 +C_4 = mtof(60) +D_4 +E_4 vF4 // Or F#4 if you don't mind the direction -F=4 -G=4 -A=4 -B=4 +F_4 +G_4 +A_4 +B_4 ^B4 // Or Bb4 if you don't mind the direction -C=5 +C_5 9@ diff --git a/examples/pythagoras12.sw b/examples/pythagoras12.sw index 93077942..6d6d56ea 100644 --- a/examples/pythagoras12.sw +++ b/examples/pythagoras12.sw @@ -37,4 +37,4 @@ B♭4 black B♮4 white // The equal sign is the ASCII equivalent of ♮ -C=5 white +C_5 white diff --git a/src/__tests__/pythagorean.spec.ts b/src/__tests__/pythagorean.spec.ts index a0d33925..44efde5d 100644 --- a/src/__tests__/pythagorean.spec.ts +++ b/src/__tests__/pythagorean.spec.ts @@ -155,7 +155,7 @@ describe('Absolute Pythagorean interval construction from parts', () => { ['B', [{fraction: '', accidental: 'b'}], 3, 3, -2], ['E', [{fraction: '', accidental: 'd'}], 4, -0.5, 0.5], ['C', [{fraction: 'q', accidental: '#'}], 4, -2.75, 1.75], - ['C', [{fraction: '', accidental: '='}], 5, 1, 0], + ['C', [{fraction: '', accidental: '_'}], 5, 1, 0], ])('constructs %s%s', (nominal, accidentals, octave, twos, threes) => { const node: AbsolutePitch = { type: 'AbsolutePitch', diff --git a/src/diamond-mos.ts b/src/diamond-mos.ts new file mode 100644 index 00000000..d055c459 --- /dev/null +++ b/src/diamond-mos.ts @@ -0,0 +1,195 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import {mmod} from 'xen-dev-utils'; +import {TimeMonzo, TimeReal} from './monzo'; +import { + ACCIDENTAL_VECTORS, + Accidental, + AugmentedQuality, + IntervalQuality, + VULGAR_FRACTIONS, + VulgarFraction, +} from './pythagorean'; +import {ZERO, hasOwn} from './utils'; + +export type MosDegree = { + center: TimeMonzo; + imperfect: boolean; + mid?: TimeMonzo; +}; + +/** + * Configuration for a scale notated in Diamond mos. + * May define a non-MOS scale as a result of accidental or intentional misconfiguration. + */ +export type MosConfig = { + /** + * Current value of middle J. + */ + J4: TimeMonzo; + /** + * Interval of equivalence. The distance between J4 and J5. + */ + equave: TimeMonzo; + /** + * Period of repetition. + */ + period: TimeMonzo; + /** + * Current value of the '&' accidental. + */ + am: TimeMonzo; + /** + * Current value of the 'e' accidental. + */ + semiam: TimeMonzo; + /** + * Relative scale from J onwards. Echelon depends on J. Use equave to reach higher octave numbers. + */ + scale: Map; + /** + * Intervals for relative notation. Use period to reach larger intervals. + */ + degrees: MosDegree[]; +}; + +export type MosStep = { + type: 'MosStep'; + quality: IntervalQuality; + augmentations?: AugmentedQuality[]; + degree: 0; +}; + +export type MosAccidental = '&' | 'e' | 'a' | '@'; + +export type SplitMosAccidental = { + fraction: VulgarFraction; + accidental: Accidental | MosAccidental; +}; + +export type MosNominal = + | 'J' + | 'K' + | 'L' + | 'M' + | 'N' + | 'O' + | 'P' + | 'Q' + | 'R' + | 'S' + | 'T' + | 'U' + | 'V' + | 'W' + | 'X' + | 'Y' + | 'Z'; + +/** + * Absolute Diamond-mos pitch. + */ +export type AbsoluteMosPitch = { + type: 'AbsolutePitch'; + nominal: MosNominal; + accidentals: SplitMosAccidental[]; + octave: number; +}; + +export function mosMonzo(node: MosStep, config: MosConfig): TimeMonzo { + const baseDegree = mmod(Math.abs(node.degree), config.degrees.length); + const mosDegree = config.degrees[baseDegree]; + const quality = node.quality.quality; + let inflection = new TimeMonzo(ZERO, []); + if ( + quality === 'a' || + quality === 'Â' || + quality === 'aug' || + quality === 'Aug' + ) { + inflection = config.am; + } else if (quality === 'd' || quality === 'dim') { + inflection = config.am.inverse(); + } else if (quality === 'M') { + inflection = config.semiam; + } else if (quality === 'm') { + inflection = config.semiam.inverse(); + } + if (node.quality.fraction !== '') { + const fraction = VULGAR_FRACTIONS.get(node.quality.fraction)!; + const fractionalInflection = inflection.pow(fraction); + if (fractionalInflection instanceof TimeReal) { + throw new Error('Failed to fractionally inflect mosstep.'); + } + inflection = fractionalInflection; + } + + for (const augmentation of node.augmentations ?? []) { + if (augmentation === 'd' || augmentation === 'dim') { + inflection = inflection.div(config.am) as TimeMonzo; + } else { + inflection = inflection.mul(config.am) as TimeMonzo; + } + } + + // Non-perfect intervals need an extra half-augmented widening + if (mosDegree.imperfect) { + if ( + quality === 'a' || + quality === 'Â' || + quality === 'aug' || + quality === 'Aug' + ) { + inflection = inflection.mul(config.semiam) as TimeMonzo; + } else if (quality === 'd' || quality === 'dim') { + inflection = inflection.div(config.semiam) as TimeMonzo; + } + } else if (quality === 'n') { + if (!mosDegree.mid) { + throw new Error('Missing mid mosstep quality.'); + } + return mosDegree.mid; + } + return mosDegree.center.mul(inflection) as TimeMonzo; +} + +function mosInflection( + accidental: MosAccidental | Accidental, + config: MosConfig +) { + switch (accidental) { + case '&': + return config.am; + case 'e': + return config.semiam; + case '@': + return config.am.inverse(); + case 'a': + return config.semiam.inverse(); + } + if (!ACCIDENTAL_VECTORS.has(accidental)) { + throw new Error(`Accidental ${accidental} is unassigned.`); + } + const vector = ACCIDENTAL_VECTORS.get(accidental)!; + return new TimeMonzo(ZERO, vector); +} + +export function absoluteMosMonzo( + node: AbsoluteMosPitch, + config: MosConfig +): TimeMonzo { + if (!config.scale.has(node.nominal)) { + throw new Error(`Nominal ${node.nominal} is unassigned.`); + } + let result = config.scale.get(node.nominal)!.clone(); + for (const accidental of node.accidentals) { + const inflection = mosInflection(accidental.accidental, config); + + const fraction = VULGAR_FRACTIONS.get(accidental.fraction)!; + const fractionalInflection = inflection.pow(fraction); + if (fractionalInflection instanceof TimeReal) { + throw new Error('Failed to fracture mos accidental.'); + } + result = result.mul(fractionalInflection) as TimeMonzo; + } + return result; +} diff --git a/src/expression.ts b/src/expression.ts index bc5c393b..3b86a7f1 100644 --- a/src/expression.ts +++ b/src/expression.ts @@ -7,6 +7,7 @@ import { validateBigInt, } from './utils'; import {Pythagorean, AbsolutePitch} from './pythagorean'; +import {AbsoluteMosPitch, MosStep} from './diamond-mos'; import {Fraction, bigAbs, lcm} from 'xen-dev-utils'; /** @@ -171,7 +172,7 @@ export type AbsoluteFJS = { type: 'AbsoluteFJS'; ups: number; lifts: number; - pitch: AbsolutePitch; + pitch: AbsolutePitch | AbsoluteMosPitch; superscripts: FJSInflection[]; subscripts: FJSInflection[]; }; @@ -185,6 +186,15 @@ export type AspiringAbsoluteFJS = { flavor: FJSFlavor; }; +export type MosStepLiteral = { + type: 'MosStepLiteral'; + ups: number; + lifts: number; + mosStep: MosStep; + superscripts: FJSInflection[]; + subscripts: FJSInflection[]; +}; + export type WartsLiteral = { type: 'WartsLiteral'; equave: string; @@ -248,6 +258,7 @@ export type IntervalLiteral = | AspiringFJS | AbsoluteFJS | AspiringAbsoluteFJS + | MosStepLiteral | HertzLiteral | SecondLiteral | ReciprocalLogarithmicHertzLiteral @@ -1122,6 +1133,7 @@ export function literalToJSON(literal?: IntervalLiteral) { case 'AspiringFJS': case 'AbsoluteFJS': case 'AspiringAbsoluteFJS': + case 'MosStepLiteral': case 'HertzLiteral': case 'SecondLiteral': case 'ReciprocalLogarithmicHertzLiteral': @@ -1171,6 +1183,7 @@ export function literalFromJSON(object: any): IntervalLiteral | undefined { case 'AspiringFJS': case 'AbsoluteFJS': case 'AspiringAbsoluteFJS': + case 'MosStepLiteral': case 'HertzLiteral': case 'SecondLiteral': case 'ReciprocalLogarithmicHertzLiteral': diff --git a/src/grammars/sonic-weave.pegjs b/src/grammars/sonic-weave.pegjs index 464b1883..63050783 100644 --- a/src/grammars/sonic-weave.pegjs +++ b/src/grammars/sonic-weave.pegjs @@ -992,9 +992,10 @@ Primary / StepLiteral / ScalarMultiple / ColorLiteral + / SquareSuperparticular / FJS + / MosStepLiteral / AbsoluteFJS - / SquareSuperparticular / Identifier / TemplateArgument / ArrayLiteral @@ -1348,6 +1349,23 @@ SplitPythagorean }; } +MosStep + = quality: AugmentedQuality augmentations: AugmentedToken* degree: SignedBasicInteger 'ms' { + return { + type: 'MosStep', + quality, + augmentations, + degree, + }; + } + / quality: (ImperfectQuality / PerfectQuality) degree: SignedBasicInteger 'ms' { + return { + type: 'MosStep', + quality, + degree, + }; + } + InflectionFlavor = 'n' / 'l' / 'h' / 'm' / 's' / 'f' / 'c' / 'q' / 't' / '' Inflections @@ -1399,8 +1417,21 @@ FJS }; } +MosStepLiteral + = upsAndDowns: UpsAndDowns + mosStep: MosStep + hyperscripts: Hyperscripts { + return { + ...upsAndDowns, + type: 'MosStepLiteral', + mosStep, + superscripts: hyperscripts.superscripts, + subscripts: hyperscripts.subscripts, + }; + } + AccidentalSign - = '𝄪' / '𝄫' / '𝄲' / '𝄳' / [x♯#‡t♮=d♭b] + = '𝄪' / '𝄫' / '𝄲' / '𝄳' / [x♯#‡t♮_d♭b&ea@] Accidental 'accidental' = fraction: VulgarFraction accidental: AccidentalSign { @@ -1411,8 +1442,34 @@ Accidental 'accidental' } PitchNominal 'pitch nominal' - = 'alp' / 'bet' / 'gam' / 'del' / 'eps' / 'zet' / 'eta' / [α-ηA-G] - + = [α-ωA-Z] + / 'alp' + / 'bet' + / 'gam' + / 'del' + / 'eps' + / 'zet' + / 'eta' + / 'the' + / 'iot' + / 'kap' + / 'lam' + / 'muu' // 'mu' is too short + / 'nuu' // 'nu' + / 'xii' // 'xi' + / 'omi' + / 'pii' // 'pi' + / 'rho' + / 'fsi' // Final sigma + / 'sig' + / 'tau' + / 'ups' + / 'phi' + / 'chi' + / 'psi' + / 'ome' + +// Some pitches like M3 or S9 are inaccessible due to other rules and require accidentals to disambiguate. AbsolutePitch = nominal: PitchNominal accidentals: Accidental* octave: SignedBasicInteger { return { @@ -1463,15 +1520,23 @@ ArrowFunction }; } -// This rule is a faster version of the part of (FJS / AbsoluteFJS / SquareSuperparticular) which overlaps with identifiers. +// This rule is a faster version of the part of (FJS / AbsoluteFJS / (SquareSuperparticular)) which overlaps with identifiers. ReservedPattern - = [sqQ]? (AugmentedToken+ / [mMnP]) [0-9]+ ([_v] [0-9])* - / PitchNominal [sqQxdbrp]* [0-9]+ ([_v] [0-9])* - / 'S' [0-9]+ + = [sqQ]? (AugmentedToken+ / [mMnP]) [0-9]+ 'ms'? ([_v] [0-9])* + / PitchNominal [sqQxdb_ae]* [0-9]+ ([_v] [0-9])* + +// TODO: Figure out where to put this +InvalidIdentifier + = word:IdentifierName { + if (RESERVED_WORDS.has(word)) { + error(`${word} is a reserved keyword`); + } + error(`${word} is a reserved pattern`); + } ValidIdentifierName = @word:IdentifierName &{ - return !RESERVED_WORDS.has(word) + return !RESERVED_WORDS.has(word); } Identifier diff --git a/src/parser/__tests__/expression.spec.ts b/src/parser/__tests__/expression.spec.ts index 3b453d4b..8defc56c 100644 --- a/src/parser/__tests__/expression.spec.ts +++ b/src/parser/__tests__/expression.spec.ts @@ -363,10 +363,10 @@ describe('SonicWeave expression evaluator', () => { 'logarithmic', ]) { const value = evaluate( - `A=4 = 440 Hz = 27/16; ${conversion}(${tier}(3.141592653589793r${hz}${tolerance}))` + `A4 = 440 Hz = 27/16; ${conversion}(${tier}(3.141592653589793r${hz}${tolerance}))` ) as Interval; const iterated = evaluate( - `A=4 = 440 Hz = 27/16; ${value.toString()}` + `A4 = 440 Hz = 27/16; ${value.toString()}` ) as Interval; expect(iterated.domain).toBe(value.domain); expect(iterated.valueOf()).toBeCloseTo(value.valueOf()); @@ -376,7 +376,7 @@ describe('SonicWeave expression evaluator', () => { it('has a string representation for an absurd absolute quantity', () => { const value = evaluate( - 'A=4 = 440Hz = 27/16; absolute(fraction(3.141592653589793r, -0.1))' + 'A4 = 440Hz = 27/16; absolute(fraction(3.141592653589793r, -0.1))' ) as Interval; const iterated = evaluate(value.toString()) as Interval; expect(iterated.domain).toBe(value.domain); diff --git a/src/parser/__tests__/sonic-weave-ast.spec.ts b/src/parser/__tests__/sonic-weave-ast.spec.ts index ca3dd56d..6fe19b03 100644 --- a/src/parser/__tests__/sonic-weave-ast.spec.ts +++ b/src/parser/__tests__/sonic-weave-ast.spec.ts @@ -563,12 +563,12 @@ describe('SonicWeave Abstract Syntax Tree parser', () => { expect(ast.expression.type).toBe('SquareSuperparticular'); }); - it('differentiates natural accidentals from variable declaration', () => { - const ast = parseSingle('D=4'); + it('uses underscores as natural accidentals', () => { + const ast = parseSingle('D_4'); expect(ast.expression.type).toBe('AbsoluteFJS'); }); - it("still parses variable declaration when there's no conflict with FJS", () => { + it('parses single letter variable assignment', () => { const ast = parseSingle('d=4'); expect(ast.type).toBe('AssignmentStatement'); }); @@ -1114,6 +1114,25 @@ describe('SonicWeave Abstract Syntax Tree parser', () => { module: 'foo', }); }); + + it('has MOS-step syntax', () => { + const ast = parseSingle('P0ms'); + expect(ast).toEqual({ + type: 'ExpressionStatement', + expression: { + ups: 0, + lifts: 0, + type: 'MosStepLiteral', + mosStep: { + type: 'MosStep', + quality: {fraction: '', quality: 'P'}, + degree: 0, + }, + superscripts: [], + subscripts: [], + }, + }); + }); }); describe('Automatic semicolon insertion', () => { diff --git a/src/parser/__tests__/source.spec.ts b/src/parser/__tests__/source.spec.ts index 16112ec8..7ddb9b89 100644 --- a/src/parser/__tests__/source.spec.ts +++ b/src/parser/__tests__/source.spec.ts @@ -196,7 +196,7 @@ describe('SonicWeave parser', () => { }); it('supports pythagorean absolute notation', () => { - const scale = parseSource('C4 = 262 Hz; A=4;'); + const scale = parseSource('C4 = 262 Hz; A4;'); expect(scale).toHaveLength(1); expect(scale[0].value.valueOf()).toBeCloseTo(442.12); }); @@ -382,12 +382,6 @@ describe('SonicWeave parser', () => { const scale = parseSource(` ^ = P4 / 2 - M2; - const φ0 = P4 / 2; - const χ0 = φ0 + M2; - const ψ0 = P8 - P4 / 2; - const ω0 = ψ0 + M2; - const [vχ0, vω0] = v{[χ0, ω0]}; - C0 = 1/1; ^C0; vD0; @@ -495,10 +489,10 @@ describe('SonicWeave parser', () => { it('supports other roots besides C4', () => { const scale = parseSource(` - A=3 = 200 Hz - D=4 - E=4 - A=4 + A_3 = 200 Hz + D_4 + E_4 + A_4 relative; `); const ratios = scale.map(i => i.value.valueOf()); @@ -562,7 +556,7 @@ describe('SonicWeave parser', () => { it('can expand customized scales', () => { const visitor = evaluateSource( - 'A=4 = 440 Hz = 1/1;^D4;A=4 = 432 Hz;^ = 2°;const syn=81/80;vD4~*syn;3;$[-1]=5;', + 'A4 = 440 Hz = 1/1;^D4;A4 = 432 Hz;^ = 2°;const syn=81/80;vD4~*syn;3;$[-1]=5;', false ); expect(visitor.expand(getSourceVisitor(false).rootContext!)).toBe( @@ -1458,4 +1452,16 @@ describe('SonicWeave parser', () => { ) ).toThrow('Illegal BreakStatement inside a deferred block.'); }); + + it('rejects unassigned absolute pitches (ASCII)', () => { + expect(() => evaluateSource('fsi#4')).toThrow('Nominal fsi is unassigned.'); + }); + + it('rejects unassigned absolute pitches (unicode)', () => { + expect(() => evaluateSource('ς♭2')).toThrow('Nominal ς is unassigned.'); + }); + + it('rejects unassigned accidentals', () => { + expect(() => evaluateSource('Ce4')).toThrow('Accidental e is unassigned.'); + }); }); diff --git a/src/parser/__tests__/stdlib.spec.ts b/src/parser/__tests__/stdlib.spec.ts index 701de5bf..7467d55a 100644 --- a/src/parser/__tests__/stdlib.spec.ts +++ b/src/parser/__tests__/stdlib.spec.ts @@ -880,7 +880,7 @@ describe('SonicWeave standard library', () => { }); it('can declare reference frequency at the same time as reference pitch', () => { - const two = evaluateExpression('A=4 = 440z = 1/1; str(relin(A=5))'); + const two = evaluateExpression('A_4 = 440z = 1/1; str(relin(A5))'); expect(two).toBe('2'); }); diff --git a/src/parser/expression.ts b/src/parser/expression.ts index e1059044..1d6ca9fe 100644 --- a/src/parser/expression.ts +++ b/src/parser/expression.ts @@ -19,6 +19,7 @@ import { IntervalLiteral, SparseOffsetVal, SquareSuperparticular, + MosStepLiteral, } from '../expression'; import { Interval, @@ -55,7 +56,7 @@ import { BinaryPrefix, F, } from '../utils'; -import {pythagoreanMonzo, absoluteMonzo} from '../pythagorean'; +import {pythagoreanMonzo, absoluteMonzo, AbsolutePitch} from '../pythagorean'; import {inflect} from '../fjs'; import { STEP_ELEMENT, @@ -93,6 +94,12 @@ import { UpdateOperator, } from '../ast'; import {type StatementVisitor} from './statement'; +import { + AbsoluteMosPitch, + MosConfig, + absoluteMosMonzo, + mosMonzo, +} from '../diamond-mos'; /** * Local context within a SonicWeave code block or a function. @@ -174,6 +181,16 @@ const CENT_REAL = new TimeReal(0, 1.0005777895065548); const RECIPROCAL_CENT_MONZO = new TimeMonzo(ZERO, [F(1200)]); const HERTZ_MONZO = new TimeMonzo(NEGATIVE_ONE, []); +const PLACEHOLDER_MOS_CONFIG: MosConfig = { + J4: TEN_MONZO, + equave: TEN_MONZO, + period: TEN_MONZO, + am: TEN_MONZO, + semiam: TEN_MONZO, + scale: new Map(), + degrees: [], +}; + function typesCompatible( a: IntervalLiteral | undefined, b: IntervalLiteral | undefined @@ -342,6 +359,8 @@ export class ExpressionVisitor { return this.visitFJS(node); case 'AbsoluteFJS': return this.visitAbsoluteFJS(node); + case 'MosStepLiteral': + return this.visitMosStepLiteral(node); case 'HertzLiteral': return this.visitHertzLiteral(node); case 'SecondLiteral': @@ -637,7 +656,7 @@ export class ExpressionVisitor { protected upLift( value: TimeMonzo | TimeReal, - node: MonzoLiteral | FJS | AbsoluteFJS + node: MonzoLiteral | FJS | AbsoluteFJS | MosStepLiteral ) { if (!this.rootContext) { throw new Error('Root context required for uplift.'); @@ -751,7 +770,12 @@ export class ExpressionVisitor { protected visitAbsoluteFJS(node: AbsoluteFJS) { const relativeToC4 = inflect( - absoluteMonzo(node.pitch), + /[J-Z]/.test(node.pitch.nominal) + ? absoluteMosMonzo( + node.pitch as AbsoluteMosPitch, + PLACEHOLDER_MOS_CONFIG + ) + : absoluteMonzo(node.pitch as AbsolutePitch), node.superscripts, node.subscripts ); @@ -766,6 +790,17 @@ export class ExpressionVisitor { return result; } + protected visitMosStepLiteral(node: MosStepLiteral) { + const monzo = inflect( + mosMonzo(node.mosStep, PLACEHOLDER_MOS_CONFIG), + node.superscripts, + node.subscripts + ); + const result = this.upLift(monzo, node); + this.rootContext!.fragiles.push(result); + return result; + } + protected visitAccessExpression(node: AccessExpression): SonicWeaveValue { const object = arrayRecordOrString( this.visit(node.object), diff --git a/src/pythagorean.ts b/src/pythagorean.ts index 0233ed8a..bf5f0901 100644 --- a/src/pythagorean.ts +++ b/src/pythagorean.ts @@ -92,14 +92,14 @@ export type Pythagorean = { * Absolute pitch nominal: Traditional Pythagorean or semioctave. */ export type Nominal = - | 'A' + | 'A' // Diatonic | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' - | 'alp' + | 'alp' // Semioctave | 'α' | 'bet' | 'β' @@ -112,7 +112,45 @@ export type Nominal = | 'zet' | 'ζ' | 'eta' - | 'η'; + | 'η' + | 'phi' // Semiquartal + | 'φ' + | 'chi' + | 'χ' + | 'psi' + | 'ψ' + | 'ome' + | 'ω' + | 'H' // Latin reserve + | 'I' + | 'the' // Greek reserve + | 'θ' + | 'iot' + | 'ι' + | 'kap' + | 'κ' + | 'lam' + | 'λ' + | 'muu' // 'mu' is too short + | 'μ' + | 'nuu' + | 'ν' + | 'xii' + | 'ξ' + | 'omi' + | 'ο' + | 'pii' + | 'π' + | 'rho' + | 'ρ' + | 'fsi' // Final sigma + | 'ς' + | 'sig' + | 'σ' + | 'tau' + | 'τ' + | 'ups' + | 'υ'; /** * Musical accidental representing some powers of primes 2 and 3, possibly fractional. @@ -128,7 +166,7 @@ export type Accidental = | '‡' | 't' | '♮' - | '=' + | '_' | 'd' | '♭' | 'b'; @@ -237,11 +275,25 @@ const NOMINAL_VECTORS = new Map([ // B - 1\2 ['bet', [F(-15, 2), FIVE]], ['β', [F(-15, 2), FIVE]], + + // Manual / semiquartal + ['phi', [ONE, NEGATIVE_HALF]], + ['φ', [ONE, NEGATIVE_HALF]], + + ['chi', [NEGATIVE_TWO, SESQUI]], + ['χ', [NEGATIVE_TWO, SESQUI]], + + ['psi', [ZERO, HALF]], + ['ψ', [ZERO, HALF]], + + ['ome', [NEGATIVE_THREE, SEMIFIVE]], + ['ω', [NEGATIVE_THREE, SEMIFIVE]], ]); -const ACCIDENTAL_VECTORS = new Map([ +/** @hidden */ +export const ACCIDENTAL_VECTORS = new Map([ ['♮', [ZERO, ZERO]], - ['=', [ZERO, ZERO]], + ['_', [ZERO, ZERO]], ['♯', [F(-11, 1), SEVEN]], ['#', [F(-11, 1), SEVEN]], @@ -262,7 +314,8 @@ const ACCIDENTAL_VECTORS = new Map([ ['d', [SEMIELEVEN, F(-7, 2)]], ]); -const VULGAR_FRACTIONS = new Map([ +/** @hidden */ +export const VULGAR_FRACTIONS = new Map([ ['', ONE], ['s', HALF], ['½', HALF], @@ -385,9 +438,15 @@ export function pythagoreanMonzo(node: Pythagorean): TimeMonzo { } export function absoluteMonzo(node: AbsolutePitch) { + if (!NOMINAL_VECTORS.has(node.nominal)) { + throw new Error(`Nominal ${node.nominal} is unassigned.`); + } const vector = [...NOMINAL_VECTORS.get(node.nominal)!]; for (const accidental of node.accidentals) { const fraction = VULGAR_FRACTIONS.get(accidental.fraction)!; + if (!ACCIDENTAL_VECTORS.has(accidental.accidental)) { + throw new Error(`Accidental ${accidental.accidental} is unassigned.`); + } const modification = ACCIDENTAL_VECTORS.get(accidental.accidental)!; vector[0] = vector[0].add(modification[0].mul(fraction)); vector[1] = vector[1].add(modification[1].mul(fraction)); diff --git a/src/stdlib/builtin.ts b/src/stdlib/builtin.ts index f47b46ab..fc397719 100644 --- a/src/stdlib/builtin.ts +++ b/src/stdlib/builtin.ts @@ -877,7 +877,7 @@ function labelAbsoluteFJS( interval.label = formatAbsoluteFJS(interval.node, false); interval.color = new Color('white'); for (const accidental of interval.node.pitch.accidentals) { - if (accidental.accidental === '♮' || accidental.accidental === '=') { + if (accidental.accidental === '♮' || accidental.accidental === '_') { continue; } interval.color = new Color('black');