Skip to content

Commit

Permalink
Fix, document and test the subtension calculator
Browse files Browse the repository at this point in the history
Bump node version to 12 for Object.fromEntries.

ref #146
  • Loading branch information
frostburn committed Apr 10, 2024
1 parent 6e2e136 commit 2086c56
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 19 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,6 @@
"xen-dev-utils": "^0.2.9"
},
"engines": {
"node": ">=10.6.0"
"node": ">=12.0.0"
}
}
41 changes: 40 additions & 1 deletion src/__tests__/builtin.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {describe, it, expect} from 'vitest';
import {hasConstantStructure} from '../builtin';
import {hasConstantStructure, subtensions} from '../builtin';
import {TimeMonzo} from '../monzo';

describe('Constant structure checker', () => {
Expand Down Expand Up @@ -29,3 +29,42 @@ describe('Constant structure checker', () => {
).toBe(false);
});
});

describe('Subtension calculator', () => {
it('calculates the structure of Raga Bhairavi', () => {
const scale = ['16/15', '9/8', '6/5', '27/20', '3/2', '8/5', '9/5', '2'];
const subtenders = Object.fromEntries(
subtensions(scale.map(TimeMonzo.fromFraction)).map(
({monzo, subtensions}) => [
monzo.toFraction().toFraction(),
Array.from(subtensions).sort(),
]
)
);
expect(subtenders).toEqual({
'2': [8],
'16/15': [1],
'9/8': [1, 2],
'6/5': [2, 3],
'27/20': [4],
'3/2': [4, 5],
'8/5': [5, 6],
'9/5': [7],
'135/128': [1],
'81/64': [3],
'45/32': [4],
'27/16': [6],
'15/8': [7],
'4/3': [3, 4],
'64/45': [4],
'16/9': [6, 7],
'256/135': [7],
'5/4': [2, 3],
'5/3': [5, 6],
'10/9': [1],
'32/27': [2],
'40/27': [4],
'128/81': [5],
});
});
});
49 changes: 32 additions & 17 deletions src/builtin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
Fraction,
kCombinations as xduKCombinations,
mmod,
isPrime as xduIsPrime,
primes as xduPrimes,
approximateRadical,
Expand Down Expand Up @@ -1163,38 +1162,54 @@ lcm.__doc__ =
'Obtain the smallest (linear) interval that shares all intervals or the current scale as multiplicative factors.';
lcm.__node__ = builtinNode(lcm);

/**
* Result from {@link subtensions} consisting of a relative interval and all of the spans it subtends (a set of 0-indexed interval classes).
*/
export type Subtender = {
monzo: TimeMonzo;
subtensions: Set<number>;
};

/**
* Calculate all subtensions i.e 0-indexed interval classes associated with relative intervals.
* @param monzos Musical intervals given as relative monzos not including the implicit unison at the start, but including the interval of repetition at the end.
* @returns An array of subtensions associated with each interval found in the scale.
*/
export function subtensions(monzos: TimeMonzo[]): Subtender[] {
if (monzos.length < 1) {
const n = monzos.length;
if (!n) {
return [];
}
const numComponents = Math.max(...monzos.map(m => m.numberOfComponents));
monzos = monzos.map(m => m.clone());
const equave = monzos.pop()!;
monzos.unshift(equave.pow(0));
for (const monzo of monzos) {
const scale = monzos.map(m => m.clone());
for (const monzo of scale) {
monzo.numberOfComponents = numComponents;
}
const period = scale[n - 1];
for (const monzo of [...scale]) {
scale.push(period.mul(monzo));
}

const result: Subtender[] = [];

// Against 1/1
for (let i = 1; i < monzos.length; ++i) {
result.push({monzo: monzos[i], subtensions: new Set([i])});
for (let i = 0; i < n; ++i) {
for (const {monzo, subtensions} of result) {
if (monzo.strictEquals(scale[i])) {
subtensions.add(i + 1);
}
}
result.push({monzo: scale[i], subtensions: new Set([i + 1])});
}

// Against each other
for (let i = 1; i < monzos.length; ++i) {
for (let j = 1; j < monzos.length; ++j) {
let width = monzos[mmod(i + j, monzos.length)].div(monzos[i]);
if (i + j >= monzos.length) {
width = width.mul(equave);
}
for (let i = 0; i < n - 1; ++i) {
for (let j = 1; j < n; ++j) {
const width = scale[i + j].div(scale[i]);
let unique = true;
for (const subtender of result) {
if (width.strictEquals(subtender.monzo)) {
subtender.subtensions.add(j);
for (const {monzo, subtensions} of result) {
if (width.strictEquals(monzo)) {
subtensions.add(j);
unique = false;
}
}
Expand Down

0 comments on commit 2086c56

Please sign in to comment.