From 01387e2fd6e92e7806d4bcb703e0bf249f862be4 Mon Sep 17 00:00:00 2001 From: Lumi Pakkanen Date: Tue, 14 May 2024 11:17:25 +0300 Subject: [PATCH] Tweak .swi format Swap default order of labels and colors. Describe the .swi interchange format. ref #236 --- README.md | 1 + documentation/cli.md | 2 +- documentation/interchange.md | 155 ++++++++++++++++++++++++++++ examples/interchange.sw | 44 ++++++++ src/cli.ts | 10 +- src/interval.ts | 13 ++- src/parser/__tests__/source.spec.ts | 26 ++--- src/parser/__tests__/stdlib.spec.ts | 34 +++--- 8 files changed, 243 insertions(+), 42 deletions(-) create mode 100644 documentation/interchange.md create mode 100644 examples/interchange.sw diff --git a/README.md b/README.md index 596dafdb..4609a1c3 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ The `sonic-weave` package is many things. - A [command-line interface](https://github.com/xenharmonic-devs/sonic-weave/blob/main/documentation/cli.md) for calculating musical quantities - A TypeScript compatible [npm package](https://github.com/xenharmonic-devs/sonic-weave/blob/main/documentation/package.md) - A [template language](https://github.com/xenharmonic-devs/sonic-weave/blob/main/documentation/tag.md) for running SonicWeave programs inside JavaScript +- An [interchange format](https://github.com/xenharmonic-devs/sonic-weave/blob/main/documentation/interchange.md) (extension `.swi`) You may also be interested in the [technical overview](https://github.com/xenharmonic-devs/sonic-weave/blob/main/documentation/technical.md) of SonicWeave as a programming language. diff --git a/documentation/cli.md b/documentation/cli.md index 677fe090..9845a292 100644 --- a/documentation/cli.md +++ b/documentation/cli.md @@ -116,7 +116,7 @@ Enneatonic 5L 4s subset of 313edo used in Sevish's track Desert Island Rain ``` ### SonicWeave Interchange format -The .swi format is suitable for data interchange between programs. It preserves the internal precision of the SonicWeave runtime. +The [.swi format](https://github.com/xenharmonic-devs/sonic-weave/blob/main/documentation/interchange.md) is suitable for data interchange between programs. It preserves the internal precision of the SonicWeave runtime. ```bash $ npx sonic-weave examples/pajara10.sw --format swi // Created using SonicWeave 0.0.33 diff --git a/documentation/interchange.md b/documentation/interchange.md new file mode 100644 index 00000000..aa2e08e6 --- /dev/null +++ b/documentation/interchange.md @@ -0,0 +1,155 @@ +# SonicWeave - Technical overview +This documentation describes the .swi interchange format for transferring microtonal scales between programs + +## Comments +Comments work as in JavaScript. Everything after `//` is ignored until the end of the line. Everything after `/*` is ignored until `*/` is encountered. (This includes other `/*` i.e. no nested grammar for comments.) +```c +// single line comment +/* +Comment +spanning +multiple +lines. +*/ +``` + +## Empty lines +Empty lines and lines containing only whitespace are ignored. + +## Strings +Strings are JSON encoded. They start and end with `"`. A literal `"` inside a string must be escaped with a backslash, etc.. + +## CSS colors +Valid CSS colors include: + - Special token `niente` indicating no color + - Named color e.g. `red` or `white` + - 4-bit hexadecimal RGB triplet e.g. `#f02` + - 8-bit hexadecimal RGB triplet e.g. `#ff01ab` + - RGB(A) literal e.g `rgba(255 50% 0 / 50%)` + - HSL(A) literal e.g. `hsl(-40deg 25% 70% / .5)` + +## Scale title +The first string in the file indicates the title of the scale. +```c +"The scale title" +``` + +A scale title is mandatory. Untitled scales must provide an empty string. + +## Unison frequency +The reference frequency is given with the syntax `1 = expr` where expressions is a valid absolute interval (described below). +```c +// Set reference frequency to 256 Hz +1 = [1 8>@Hz.2 +``` + +Unison frequency is an optional field. + +## Intervals +The label (a JSON encoded string) and the color (a CSS color or the special token `niente` for no color) are mandatory fields given in that order after an interval literal separated by spaces. + +### Relative intervals +Relative interval are to be interpreted against the reference frequency if it is given. + +#### 23-limit monzos +Intervals with prime factors below 29 are given as ket-vectors a.k.a. monzos starting with `[` and ending with `>`. + +Below the labels indicate the meaning of each monzo: +```c +[> "1/1" niente +[-4 4 -1> "81/80" niente +[7/12> "7 sof 12" niente +[0 10/13> "10 sof 13 ed 3" #0f0 +[1> "P8" white +``` + +#### Higher primes +Prime factors above 23 require an explicit subgroup basis given after `@` separated by periods `.`. Subgroup basis elements must be integers. No check is made to ensure that basis elements are actually prime numbers. +```c +[-1 1>@29.31 "31/29" niente +[-9 1>@2.899 "899/512" rgb(90% 80% 70% / 70%) +``` + +#### Non-primes +The special symbols `-1` and `0` indicate negative numbers and zero respectively. Their vector component must be `1` if present. +```c +[1>@0 "0" niente +[1 1>@-1.2 "-2" niente +``` + +#### Real values +Scalars that cannot be expressed as fractional monzos are given as *real* cents `rc`. The corresponding vector component is a floating-point literal. The special basis element `inf` indicates floating-point infinity. A negative unity component of `inf` indicates real zero. +```c +[-1>@inf "0r" niente +[0.>@rc "1r" niente +[1 1200.>@-1.rc "-2r" niente +[1981.7953553667824>@rc "PI" niente +[1>@inf "inf" niente +``` + +### Absolute pitches +The special basis element `Hz` indicates frequencies. +```c +[1 3 1 1>@Hz.2.5.11 "440 Hz" niente +[-1 2 -2>@Hz.2.5 "10ms" niente +``` + +A tool such as Scale Workshop normalizes durations (periods of oscillation) to frequencies, but the interchange standard support unnormalized values. + +#### Real absolute pitches +The Hz exponent of real frequencies is a floating-point literal. +```c +[1. 1981.7953553667824>@Hz.rc "PI * 1Hz" niente +``` + +### Edosteps +To support tempering after interchanging data, the special `1°` basis element is used. +```c +[5>@1° "/P1" niente +``` + +### Not-a-number +The special token `nan` indicates a value that can't be interpreted as an interval. +```c +nan "asin(2)" niente +``` + +## Example +See [examples/interchange.sw](https://github.com/xenharmonic-devs/sonic-weave/blob/main/examples/interchange.sw) for various extreme values supported by the original SonicWeave runtime. + +```c +// Created using SonicWeave 0.0.37 + +"Various values to test the .swi interchange format" + +1 = [1 1 1 1>@Hz.2.3.37 + +[1>@0 "rational zero" black +[-1>@inf "real zero" rgb(1 1 1) +[> "rational unity" hsl(0deg 0% 100%) +[0.>@rc "real unity" #aaa +[1>@-1 "negative rational unity" niente +[1 0.>@-1.rc "negative real unity" niente +[-1 -1 -1 -1 -1 1>@2.3.5.53.5664905191661.9007199254740991 "" niente +[-13 10 0 -1> "Harrison's comma.\nIt is tempered out in \"septimal meantone\"" niente +[0 0 0 1/9007199254740991> "" niente +[0 0 0 0 9007199254740991> "" niente +[246.80000000000007>@rc "" niente +[7/12> "12-TET \"fifth\"" niente +[0 10/13> "" niente +[-4 0 0 0 0 0 0 0 1> "" niente +[-2 0 0 0 0 0 0 0 1/2> "" niente +[-4 1>@2.29 "" niente +[-2 1/2>@2.29 "" niente +[1> "rational octave" red +[1200.>@rc "real octave" #ff0000 +[1981.7953553667824>@rc "pi" niente +[1. 1981.7953553667824>@Hz.rc "pi Hz" niente +[-1 -2 -2>@Hz.2.5 "" niente +[1 3 1 1>@Hz.2.5.11 "" niente +[1 1 2 1>@1°.Hz.3.37 "" niente +[-5 1 5/2 1 1>@1°.Hz.2.3.37 "" niente +[1>@inf "infinity" niente +[1 1>@-1.inf "negative infinity" niente +nan "not-a-number" niente +``` diff --git a/examples/interchange.sw b/examples/interchange.sw new file mode 100644 index 00000000..740922cb --- /dev/null +++ b/examples/interchange.sw @@ -0,0 +1,44 @@ +"Various values to test the .swi interchange format" + +// Need more components for that sqrt(29/16) +numComponents(20) + +// MOS declaration and this comment should be ignored in .swi output +MOS 5L 2s + +/* +Constants should also be ignored. +*/ +const fif = P4ms +let foo = 123.4rc + +C4 = 222 Hz + +0 "rational zero" black +0r "real zero" rgb(1 1 1) +1 "rational unity" hsl(0deg 0% 100%) +1r "real unity" #aaa +-1 "negative rational unity" +-1r "negative real unity" +9007199254740991/9007199254740990 +59049/57344 "Harrison's comma.\nIt is tempered out in \"septimal meantone\"" +7^(1/9007199254740991) +11^9007199254740991 +foo + foo +fif '12-TET "fifth"' +10\13<3> +23/16 +sqrt(23/16) +29/16 +sqrt(29/16) +2 "rational octave" red +2r "real octave" #ff0000 +PI "pi" +PI * 1Hz "pi Hz" +10ms +440Hz +^G4 +\gam5 +inf "infinity" +-inf "negative infinity" +nan "not-a-number" diff --git a/src/cli.ts b/src/cli.ts index 530072d4..5494d2c8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -64,13 +64,11 @@ export function toSonicWeaveInterchange(source: string) { throw new Error('Missing root context.'); } const lines = [`// Created using SonicWeave ${version}`, '']; - if (context.title) { - lines.push(JSON.stringify(context.title)); - lines.push(''); - } + lines.push(JSON.stringify(context.title)); + lines.push(''); if (context.unisonFrequency) { const unisonFrequency = literalToString( - context.unisonFrequency.asMonzoLiteral() + context.unisonFrequency.asInterchangeLiteral() ); lines.push(`1 = ${unisonFrequency}`); lines.push(''); @@ -78,7 +76,7 @@ export function toSonicWeaveInterchange(source: string) { for (const interval of visitor.currentScale) { const universal = interval.shallowClone(); universal.node = universal.asMonzoLiteral(true); - let line = universal.toString(context); + let line = universal.toString(context, true); if (line.startsWith('(') && line.endsWith(')')) { line = line.slice(1, -1); } diff --git a/src/interval.ts b/src/interval.ts index 6e96aecb..2b245784 100644 --- a/src/interval.ts +++ b/src/interval.ts @@ -1184,19 +1184,22 @@ export class Interval { /** * Convert this interval to a string that faithfully represents it including color and label. * @param context Current root context with information about root pitch and size of ups and lifts. + * @param interchange Boolean flag to always include label and color. * @returns String that has the same value and domain as this interval if evaluated as a SonicWeave expression. */ - toString(context?: RootContext) { + toString(context?: RootContext, interchange = false) { const base = this.str(context); const color = this.color ? this.color.toString() : ''; - if (this.color || this.label) { + if (interchange) { + return `${base} ${JSON.stringify(this.label)} ${color || 'niente'}`; + } else if (color || this.label) { let result = '(' + base; - if (color) { - result += ' ' + color; - } if (this.label) { result += ' ' + JSON.stringify(this.label); } + if (color) { + result += ' ' + color; + } return result + ')'; } return base; diff --git a/src/parser/__tests__/source.spec.ts b/src/parser/__tests__/source.spec.ts index 434da304..687f7b56 100644 --- a/src/parser/__tests__/source.spec.ts +++ b/src/parser/__tests__/source.spec.ts @@ -1354,7 +1354,7 @@ describe('SonicWeave parser', () => { '3/2 white', '5/3 black', '16/9 white', - '2 white "2/1"', + '2 "2/1" white', ]); }); @@ -1365,18 +1365,18 @@ describe('SonicWeave parser', () => { sort() `); expect(scale).toEqual([ - '256/243 black "A♭"', - '9/8 white "A"', - '32/27 black "B♭"', - '81/64 white "B"', - '4/3 white "C"', - '1024/729 black "D♭"', - '3/2 white "D"', - '128/81 black "E♭"', - '27/16 white "E"', - '16/9 white "F"', - '4096/2187 black "G♭"', - '2 white "G"', + '256/243 "A♭" black', + '9/8 "A" white', + '32/27 "B♭" black', + '81/64 "B" white', + '4/3 "C" white', + '1024/729 "D♭" black', + '3/2 "D" white', + '128/81 "E♭" black', + '27/16 "E" white', + '16/9 "F" white', + '4096/2187 "G♭" black', + '2 "G" white', ]); }); diff --git a/src/parser/__tests__/stdlib.spec.ts b/src/parser/__tests__/stdlib.spec.ts index f85e9595..50e6fddd 100644 --- a/src/parser/__tests__/stdlib.spec.ts +++ b/src/parser/__tests__/stdlib.spec.ts @@ -734,7 +734,7 @@ describe('SonicWeave standard library', () => { 2/1 "root" blue rotate(0) `); - expect(scale).toEqual(['4/3 red', '3/2 "fifth"', '7/4', '2/1 blue "root"']); + expect(scale).toEqual(['4/3 red', '3/2 "fifth"', '7/4', '2/1 "root" blue']); }); it('coalesces cents geometrically', () => { @@ -760,7 +760,7 @@ describe('SonicWeave standard library', () => { label(cs) label(ls) }`); - expect(scale).toEqual(['4/3 red "one"', '5/3 "two"', '6/3']); + expect(scale).toEqual(['4/3 "one" red', '5/3 "two"', '6/3']); }); it('generates Raga Kafi with everything simplified', () => { @@ -813,18 +813,18 @@ describe('SonicWeave standard library', () => { sort() `); expect(pythagoras).toEqual([ - '256/243 black "Ab"', - '9/8 white "A"', - '32/27 black "Bb"', - '81/64 white "B"', - '4/3 white "C"', - '1024/729 black "Db"', - '3/2 white "D"', - '128/81 black "Eb"', - '27/16 white "E"', - '16/9 white "F"', - '4096/2187 black "Gb"', - '2 white "G"', + '256/243 "Ab" black', + '9/8 "A" white', + '32/27 "Bb" black', + '81/64 "B" white', + '4/3 "C" white', + '1024/729 "Db" black', + '3/2 "D" white', + '128/81 "Eb" black', + '27/16 "E" white', + '16/9 "F" white', + '4096/2187 "Gb" black', + '2 "G" white', ]); }); @@ -923,9 +923,9 @@ describe('SonicWeave standard library', () => { it('can paint the whole scale', () => { const scale = expand('3::6;white;label("bob")'); expect(scale).toEqual([ - '4/3 white "bob"', - '5/3 white "bob"', - '6/3 white "bob"', + '4/3 "bob" white', + '5/3 "bob" white', + '6/3 "bob" white', ]); });