moment-of-symmetry
Version:
Moment of Symmetry (MOS) musical scale generation and analysis for Javascript
245 lines • 8.91 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.scaleInfo = exports.mosPatterns = exports.toBrightGeneratorPerPeriod = exports.isBright = exports.mosSizes = exports.mosForms = void 0;
const xen_dev_utils_1 = require("xen-dev-utils");
const names_1 = require("./names");
const ONE = new xen_dev_utils_1.Fraction(1);
function wrapGeneratorPerPeriod(x) {
if (typeof x === 'number') {
return new xen_dev_utils_1.Fraction(x).simplify(1e-12).mmod(ONE);
}
return x.mmod(ONE);
}
/**
* An array of fractions that convey information about a MOS scale.
* @param generatorPerPeriod Generator divided by period.
* @param maxSize Maximum size of a MOS pattern.
* @param maxLength Maximum length of the result.
* @returns An array of MOS forms.
*/
function mosForms(generatorPerPeriod, maxSize, maxLength) {
if (maxLength !== undefined) {
maxLength += 2;
}
generatorPerPeriod = wrapGeneratorPerPeriod(generatorPerPeriod);
const convergents = (0, xen_dev_utils_1.getConvergents)(generatorPerPeriod, maxSize, maxLength, true, true);
// Get rid of the first two
convergents.shift();
convergents.shift();
return convergents;
}
exports.mosForms = mosForms;
/**
* An array of sizes of MOS patterns.
* @param generatorPerPeriod Generator divided by period.
* @param maxSize Maximum size of a MOS pattern.
* @param maxLength Maximum length of the result.
* @returns An array of MOS sizes.
*/
function mosSizes(generatorPerPeriod, maxSize, maxLength) {
return mosForms(generatorPerPeriod, maxSize, maxLength).map(convergent => convergent.d);
}
exports.mosSizes = mosSizes;
/**
* Determine if a generator / period ratio is bright.
* @param generatorPerPeriod Generator divided by period.
* @param size Size of the scale.
* @returns `true` if the generator creates large intervals when stacked.
*/
function isBright(generatorPerPeriod, size) {
const generator = wrapGeneratorPerPeriod(generatorPerPeriod);
const negativeGenerator = ONE.sub(generator);
const range = [...Array(size).keys()];
const positive = range.map(i => generator.mul(i).mmod(ONE));
positive.sort((a, b) => a.compare(b));
positive.push(ONE);
const negative = range.map(i => negativeGenerator.mul(i).mmod(ONE));
negative.sort((a, b) => a.compare(b));
negative.push(ONE);
// Check which scale has brighter intervals
for (let i = 1; i < positive.length; ++i) {
const positiveInterval = positive[i].sub(positive[0]);
const negativeInterval = negative[i].sub(negative[0]);
const cmp = positiveInterval.compare(negativeInterval);
if (cmp > 0) {
return true;
}
if (cmp < 0) {
return false;
}
}
// Ambiguous generator
return true;
}
exports.isBright = isBright;
function toBrightGeneratorPerPeriod(generatorPerPeriod, size) {
const generator = wrapGeneratorPerPeriod(generatorPerPeriod);
if (isBright(generatorPerPeriod, size)) {
if (typeof generatorPerPeriod === 'number') {
return (0, xen_dev_utils_1.mmod)(generatorPerPeriod, 1);
}
else {
return generator;
}
}
if (typeof generatorPerPeriod === 'number') {
return (0, xen_dev_utils_1.mmod)(-generatorPerPeriod, 1);
}
else {
return ONE.sub(generator);
}
}
exports.toBrightGeneratorPerPeriod = toBrightGeneratorPerPeriod;
/**
* An array of information about the MOS patterns generated from a generator / period ratio.
* @param generatorPerPeriod Generator divided by period.
* @param numberOfPeriods Number of periods per octave.
* @param maxSize Maximum size of a MOS pattern.
* @param maxLength Maximum length of the result.
* @returns An array of MOS information.
*/
function mosPatterns(generatorPerPeriod, numberOfPeriods = 1, maxSize, maxLength) {
if (maxLength !== undefined) {
maxLength += 1;
}
const forms = mosForms(generatorPerPeriod, undefined, maxLength);
const result = [];
let size;
for (let j = 0; j < forms.length; ++j) {
const form = forms[j];
if (size !== undefined) {
if (size * numberOfPeriods > maxSize) {
break;
}
const scale = [...Array(size).keys()].map(i => form.mul(i).mmod(ONE));
scale.push(new xen_dev_utils_1.Fraction(1));
scale.sort((a, b) => a.compare(b));
let s = scale[1];
for (let i = 0; i < size; ++i) {
const other = scale[i + 1].sub(scale[i]);
const cmp = other.compare(s);
if (cmp < 0) {
s = other;
break;
}
else if (cmp > 0) {
break;
}
}
let numberOfSmallSteps = 0;
let numberOfLargeSteps = 0;
for (let i = 0; i < size; ++i) {
if (scale[i].add(s).equals(scale[i + 1])) {
numberOfSmallSteps++;
}
else {
numberOfLargeSteps++;
}
}
size *= numberOfPeriods;
numberOfLargeSteps *= numberOfPeriods;
numberOfSmallSteps *= numberOfPeriods;
const mosPattern = `${numberOfLargeSteps}L ${numberOfSmallSteps}s`;
const info = {
size,
numberOfLargeSteps,
numberOfSmallSteps,
mosPattern,
};
Object.assign(info, (0, names_1.tamnamsInfo)(mosPattern));
result.push(info);
}
size = form.d;
}
return result;
}
exports.mosPatterns = mosPatterns;
function scalePattern(scale) {
const stepSizes = new xen_dev_utils_1.FractionSet();
for (let i = 1; i < scale.length; ++i) {
stepSizes.add(scale[i].sub(scale[i - 1]));
}
if (stepSizes.size === 1) {
return 'M'.repeat(scale.length);
}
if (stepSizes.size === 2) {
const sizes = [...stepSizes];
sizes.sort((a, b) => a.compare(b));
let pattern = '';
for (let i = 1; i < scale.length; ++i) {
if (scale[i].sub(scale[i - 1]).equals(sizes[0])) {
pattern += 's';
}
else {
pattern += 'L';
}
}
return pattern;
}
if (stepSizes.size === 3) {
const sizes = [...stepSizes];
sizes.sort((a, b) => a.compare(b));
let pattern = '';
for (let i = 1; i < scale.length; ++i) {
const interval = scale[i].sub(scale[i - 1]);
if (interval.equals(sizes[0])) {
pattern += 's';
}
else if (interval.equals(sizes[1])) {
pattern += 'M';
}
else {
pattern += 'L';
}
}
return pattern;
}
throw new Error(`Too many step sizes (${stepSizes.size})`);
}
/**
* Information about the scale generated from a generator / period ratio of a given size.
* @param generatorPerPeriod Generator divided by period.
* @param numberOfPeriods Number of periods per octave.
* @param size Size of the scale.
* @param generatorsDown How many generators to go downwards.
* @returns Information about the scale.
*/
function scaleInfo(generatorPerPeriod, size, generatorsDown, numberOfPeriods = 1) {
if (size % numberOfPeriods) {
throw new Error('Size must be divisible by the number of periods');
}
if (generatorsDown % numberOfPeriods) {
throw new Error('Number of generators must be divisible by the number of periods');
}
size /= numberOfPeriods;
generatorsDown /= numberOfPeriods;
generatorPerPeriod = wrapGeneratorPerPeriod(generatorPerPeriod);
const g = generatorPerPeriod.clone();
const scale = [...Array(size).keys()].map(i => g.mul(i - generatorsDown).mmod(ONE));
scale.push(new xen_dev_utils_1.Fraction(1));
scale.sort((a, b) => a.compare(b));
const pattern = scalePattern(scale);
const info = { stepPattern: pattern.repeat(numberOfPeriods) };
if (pattern.includes('M')) {
return info;
}
let numberOfSmallSteps = 0;
let numberOfLargeSteps = 0;
[...pattern].forEach(s => {
if (s === 's') {
numberOfSmallSteps++;
}
else {
numberOfLargeSteps++;
}
});
numberOfSmallSteps *= numberOfPeriods;
numberOfLargeSteps *= numberOfPeriods;
const mosPattern = `${numberOfLargeSteps}L ${numberOfSmallSteps}s`;
info.mosPattern = mosPattern;
info.modeName = (0, names_1.modeName)(pattern.repeat(numberOfPeriods));
Object.assign(info, (0, names_1.tamnamsInfo)(mosPattern));
return info;
}
exports.scaleInfo = scaleInfo;
//# sourceMappingURL=generator-ratio.js.map