Skip to content

Commit

Permalink
Finetune .swi format
Browse files Browse the repository at this point in the history
ref #236
  • Loading branch information
frostburn committed May 13, 2024
1 parent 3f972af commit a237cf5
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 7 deletions.
88 changes: 87 additions & 1 deletion src/__tests__/interval.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {describe, it, expect} from 'vitest';
import {TimeMonzo} from '../monzo';
import {TimeMonzo, TimeReal} from '../monzo';
import {Interval, intervalValueAs} from '../interval';
import {FractionLiteral, NedjiLiteral} from '../expression';
import {sw} from '../parser';
Expand Down Expand Up @@ -44,6 +44,92 @@ describe('Idempontent formatting', () => {
});
});

describe('Interchange format', () => {
it('uses plain monzos up to 23-limit', () => {
const interval = new Interval(TimeMonzo.fromFraction('23/16'), 'linear');
interval.node = interval.asMonzoLiteral(true);
expect(interval.toString()).toBe('[-4 0 0 0 0 0 0 0 1>');
});

it('uses plain monzos up to 23-limit (poor internal value)', () => {
const interval = new Interval(TimeMonzo.fromFraction('5/4', 2), 'linear');
interval.node = interval.asMonzoLiteral(true);
expect(interval.toString()).toBe('[-2 0 1>');
});

it('switches to subgroup monzos for 29-limit', () => {
const interval = new Interval(
TimeMonzo.fromFraction('29/16', 20),
'logarithmic'
);
interval.node = interval.asMonzoLiteral(true);
expect(interval.toString()).toBe('[-4 1>@2.29');
});

it('uses explicit basis with the absolute echelon', () => {
const interval = new Interval(
TimeMonzo.fromFractionalFrequency(440),
'linear'
);
interval.node = interval.asMonzoLiteral(true);
expect(interval.toString()).toBe('[1 3 1 1>@Hz.2.5.11');
});

it('uses explicit basis with steps', () => {
const interval = new Interval(
TimeMonzo.fromFraction('5/3'),
'logarithmic',
-3
);
interval.node = interval.asMonzoLiteral(true);
expect(interval.toString()).toBe('[-3 -1 1>@1°.3.5');
});

it('uses increasing non-fractional basis', () => {
const interval = new Interval(TimeMonzo.fromFraction('103/101'), 'linear');
interval.node = interval.asMonzoLiteral(true);
expect(interval.toString()).toBe('[-1 1>@101.103');
});

it('has an expression for rational unity', () => {
const interval = new Interval(TimeMonzo.fromFraction(1), 'linear');
interval.node = interval.asMonzoLiteral(true);
expect(interval.toString()).toBe('[>');
});

it('has an expression for rational zero', () => {
const interval = new Interval(TimeMonzo.fromFraction(0), 'linear');
interval.node = interval.asMonzoLiteral(true);
expect(interval.toString()).toBe('[1>@0');
});

it('has an expression for rational -2', () => {
const interval = new Interval(TimeMonzo.fromFraction(-2), 'linear');
interval.node = interval.asMonzoLiteral(true);
expect(interval.toString()).toBe('[1 1>@-1.2');
});

it('has an expression for real unity', () => {
const interval = new Interval(TimeReal.fromValue(1), 'linear');
interval.node = interval.asMonzoLiteral(true);
expect(interval.toString()).toBe('[0.>@rc');
});

// Real zero skipped

it('has an expression for real -2', () => {
const interval = new Interval(TimeReal.fromValue(-2), 'linear');
interval.node = interval.asMonzoLiteral(true);
expect(interval.toString()).toBe('[1 1200.>@-1.rc');
});

it('has an expression for real 256Hz', () => {
const interval = new Interval(TimeReal.fromFrequency(256), 'linear');
interval.node = interval.asMonzoLiteral(true);
expect(interval.toString()).toBe('[1. 9600.>@Hz.rc');
});
});

const SERIALIZED =
'["hello",{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":2,"d":1},{"n":1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"linear","steps":0,"label":"","node":{"type":"IntegerLiteral","value":"12"},"trackingIds":[]},12,{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":1,"d":1},{"n":1,"d":1},{"n":-1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"linear","steps":0,"label":"","node":{"type":"DecimalLiteral","sign":"","whole":"1","fractional":"2","exponent":null,"flavor":"e"},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":0,"d":1},{"n":-1,"d":1},{"n":1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"linear","steps":0,"label":"","node":{"type":"FractionLiteral","numerator":"5","denominator":"3"},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":1,"d":3},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"linear","steps":0,"label":"","node":{"type":"RadicalLiteral","argument":{"n":2,"d":1},"exponent":{"n":1,"d":3}},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":46797,"d":80000}],"residual":{"n":1,"d":1}},"domain":"logarithmic","steps":0,"label":"","node":{"type":"CentsLiteral","sign":"","whole":"701","fractional":"955","exponent":null,"real":false},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":-4,"d":1},{"n":4,"d":1},{"n":-1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"logarithmic","steps":0,"label":"","node":{"type":"SquareSuperparticular","start":"9","end":null},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[],"residual":{"n":1,"d":1}},"domain":"logarithmic","steps":3,"label":"","node":{"type":"StepLiteral","count":3},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":0,"d":1},{"n":6,"d":13},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"logarithmic","steps":0,"label":"","node":{"type":"NedjiLiteral","numerator":6,"denominator":13,"equaveNumerator":3,"equaveDenominator":null},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":-2,"d":1},{"n":0,"d":1},{"n":1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"logarithmic","steps":0,"label":"","node":{"ups":0,"lifts":0,"type":"FJS","pythagorean":{"type":"Pythagorean","quality":{"fraction":"","quality":"M"},"degree":{"negative":false,"base":3,"octaves":0,"imperfect":true}},"superscripts":[[5,""]],"subscripts":[]},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":1,"d":1},{"n":-2,"d":1},{"n":0,"d":1},{"n":1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"logarithmic","steps":0,"label":"","node":{"ups":0,"lifts":0,"type":"AbsoluteFJS","pitch":{"type":"AbsolutePitch","nominal":"A","accidentals":[{"fraction":"","accidental":"b"}],"octave":4},"superscripts":[[7,""]],"subscripts":[]},"trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":-1,"d":1},"primeExponents":[{"n":0,"d":1},{"n":1,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":37,"d":1}},"domain":"linear","steps":0,"label":"","trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":1,"d":1},"primeExponents":[{"n":-3,"d":1},{"n":1,"d":1},{"n":-3,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1},{"n":0,"d":1}],"residual":{"n":1,"d":1}},"domain":"linear","steps":0,"label":"","trackingIds":[]},{"type":"Interval","value":{"type":"TimeMonzo","timeExponent":{"n":0,"d":1},"primeExponents":[{"n":-3,"d":1},{"n":1,"d":1},{"n":1,"d":1}],"residual":{"n":1,"d":1}},"domain":"logarithmic","steps":0,"label":"","node":{"ups":0,"lifts":0,"type":"MonzoLiteral","components":[{"sign":"-","left":3,"separator":"","right":"","exponent":null},{"sign":"","left":1,"separator":"","right":"","exponent":null},{"sign":"","left":1,"separator":"","right":"","exponent":null}],"basis":[]},"trackingIds":[]}]';

Expand Down
2 changes: 1 addition & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function toSonicWeaveInterchange(source: string) {
}
for (const interval of visitor.currentScale) {
const universal = interval.shallowClone();
universal.node = universal.asMonzoLiteral();
universal.node = universal.asMonzoLiteral(true);
let line = universal.toString(context);
if (line.startsWith('(') && line.endsWith(')')) {
line = line.slice(1, -1);
Expand Down
40 changes: 37 additions & 3 deletions src/interval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ import {
import {TimeMonzo, TimeReal} from './monzo';
import {asAbsoluteFJS, asFJS} from './fjs';
import {type RootContext} from './context';
import {ONE, ZERO, countUpsAndLifts, setUnion} from './utils';
import {
NUM_INTERCHANGE_COMPONENTS,
ONE,
ZERO,
countUpsAndLifts,
setUnion,
} from './utils';
import {Fraction, FractionValue} from 'xen-dev-utils';

/**
Expand Down Expand Up @@ -992,6 +998,10 @@ export class Interval {
);
}

/**
* Return `true` if this interval is only composed of abstract edosteps.
* @returns `true` if the interval is a unit scalar, possibly with edosteps, `false` otherwise.
*/
isPureSteps() {
return this.value.isScalar() && this.value.isUnity();
}
Expand Down Expand Up @@ -1063,8 +1073,32 @@ export class Interval {
return this.node;
}

asMonzoLiteral(): MonzoLiteral {
const node = this.value.asMonzoLiteral();
/**
* Convert the interval to a virtual AST node representing the universal type.
* @param interchange Boolean flag to format everything explicitly.
* @returns A virtual monzo literal.
*/
asMonzoLiteral(interchange = false): MonzoLiteral {
let node: MonzoLiteral;
if (
interchange &&
this.value instanceof TimeMonzo &&
!this.value.residual.isUnity()
) {
const clone = this.value.clone();
clone.numberOfComponents = NUM_INTERCHANGE_COMPONENTS;
node = clone.asMonzoLiteral();
} else {
node = this.value.asMonzoLiteral();
}
if (
interchange &&
(node.basis.length ||
node.components.length > NUM_INTERCHANGE_COMPONENTS ||
this.steps)
) {
node = this.value.asInterchangeLiteral();
}
if (this.steps) {
if (!node.basis.length && node.components.length) {
node.basis.push({numerator: 2, denominator: null, radical: false});
Expand Down
101 changes: 99 additions & 2 deletions src/monzo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
FRACTION_PRIMES,
HALF,
NEGATIVE_ONE,
NUM_INTERCHANGE_COMPONENTS,
ONE,
TWO,
ZERO,
Expand Down Expand Up @@ -59,7 +60,7 @@ export type EqualTemperament = {
equave: Fraction;
};

let NUMBER_OF_COMPONENTS = 9; // Primes 2, 3, 5, 7, 11, 13, 17, 19 and 23
let NUMBER_OF_COMPONENTS = NUM_INTERCHANGE_COMPONENTS; // Primes 2, 3, 5, 7, 11, 13, 17, 19 and 23

/**
* Set the default number of components in the vector part of time monzos.
Expand Down Expand Up @@ -741,6 +742,49 @@ export class TimeReal {
return {type: 'MonzoLiteral', components, ups: 0, lifts: 0, basis};
}

/**
* Obtain an AST node representing the time monzo as a monzo literal suitable for interchange between programs.
* @returns Monzo literal.
*/
asInterchangeLiteral(): MonzoLiteral {
const components: VectorComponent[] = [];
const basis: BasisElement[] = [];
if (this.timeExponent) {
basis.push('Hz');
const {sign, whole, fractional, exponent} = numberToDecimalLiteral(
-this.timeExponent,
'r'
);
components.push({
sign,
left: Number(whole),
separator: '.',
right: fractional,
exponent,
});
}
if (this.value < 0) {
basis.push({numerator: -1, denominator: null, radical: false});
components.push({sign: '', left: 1, right: '', exponent: null});
} else if (this.value === 0) {
basis.push({numerator: 0, denominator: null, radical: false});
components.push({sign: '', left: 1, right: '', exponent: null});
}
basis.push('rc');
const {sign, whole, fractional, exponent} = numberToDecimalLiteral(
this.totalCents(true),
'r'
);
components.push({
sign,
left: Number(whole),
separator: '.',
right: fractional,
exponent,
});
return {type: 'MonzoLiteral', components, ups: 0, lifts: 0, basis};
}

/**
* Faithful string representation of the time real.
* @param domain Domain of representation.
Expand Down Expand Up @@ -2612,8 +2656,61 @@ export class TimeMonzo {
return {type: 'MonzoLiteral', components, ups: 0, lifts: 0, basis};
}

/**
* Obtain an AST node representing the time monzo as a monzo literal suitable for interchange between programs.
* @returns Monzo literal.
*/
asInterchangeLiteral(): MonzoLiteral {
const components: VectorComponent[] = [];
const basis: BasisElement[] = [];
if (this.timeExponent.n) {
basis.push('Hz');
components.push(
fractionToVectorComponent(this.timeExponent.inverse().neg())
);
}
if (this.residual.s < 0) {
basis.push({numerator: -1, denominator: null, radical: false});
components.push({sign: '', left: 1, right: '', exponent: null});
} else if (!this.residual.s) {
basis.push({numerator: 0, denominator: null, radical: false});
components.push({sign: '', left: 1, right: '', exponent: null});
}
for (let i = 0; i < this.primeExponents.length; ++i) {
const component = this.primeExponents[i];
if (component.n) {
basis.push({numerator: PRIMES[i], denominator: null, radical: false});
components.push(fractionToVectorComponent(component));
}
}
if (this.residual.n > 1) {
basis.push({
numerator: this.residual.n,
denominator: null,
radical: false,
});
components.push({sign: '', left: 1, right: '', exponent: null});
}
if (this.residual.d > 1) {
basis.push({
numerator: this.residual.d,
denominator: null,
radical: false,
});
components.push({sign: '-', left: 1, right: '', exponent: null});
if (this.residual.n > this.residual.d) {
basis.push(basis.pop()!, basis.pop()!);
components.push(components.pop()!, components.pop()!);
}
}
return {type: 'MonzoLiteral', components, ups: 0, lifts: 0, basis};
}

/**
* Obtain an AST node representing the time monzo as a val literal.
* @returns Val literal.
*/
asValLiteral(): ValLiteral {
// TODO: Check that the basis is legal.
return {...this.asMonzoLiteral(false), type: 'ValLiteral'} as ValLiteral;
}

Expand Down
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import {Fraction, PRIMES} from 'xen-dev-utils';

export const NUM_INTERCHANGE_COMPONENTS = 9;

export function F(n: number, d?: number) {
return Object.freeze(new Fraction(n, d));
}
Expand Down

0 comments on commit a237cf5

Please sign in to comment.