total-serialism
Version:
A set of methods for the generation and transformation of number sequences useful in algorithmic composition
939 lines (867 loc) • 26.1 kB
JavaScript
//==============================================================================
// translate.js
// part of 'total-serialism' Package
// by Timo Hoogland (@t.mo / @tmhglnd), www.timohoogland.com
// MIT License
//
// Methods to translate between midi, note-names, intervals and more
//
// credits:
// - Using the amazing Tonal.js package by @danigb for various functions
//==============================================================================
// require API's
const { Note, Scale } = require('@tonaljs/tonal');
const { Chord } = require('@tonaljs/tonal');
const { Progression } = require('@tonaljs/tonal');
// require Scale Mappings
// const Scales = require('../data/scales.json');
const ToneSet = require('../data/tones.json');
const chromaSet = { c:0, d:2, e:4, f:5, g:7, a:9, b:11 };
const { unique } = require('./transform');
const { add, wrap, multiply, toArray } = require('./utility');
// create a mapping list of scales for 12-TET from Tonal
let Scales = {};
Scale.names().forEach((s) => {
let scl = Scale.get(s);
let name = scl.name.replace(/\s+/g, '_').replace(/[#'-]+/g, '');
let chroma = scl.chroma.split('').map(x => Number(x));
// rename aeolian to minor
name = (name === 'aeolian')? 'minor' : name;
let map = [];
for (let i=0; i<chroma.length; i++){
if (!chroma[i]){
map.push(map[map.length-1]);
continue;
}
map.push(i);
}
Scales[name] = map;
});
// global settings stored in object
var notation = {
"scale" : "chromatic",
"root" : "c",
"rootInt" : 0,
"map" : Scales["chromatic"],
"bpm" : 120,
"measureInMs" : 2000
}
// Return a dictionary with all the notational preferences:
// scale, root, map, bpm, measureInMs
//
// @return -> Dictionary object
//
function getSettings(){
return { ...notation };
}
exports.getSettings = getSettings;
// Set the tempo to use for translating between values, default = 100.
// Also calculates the length of a 4/4 measure in milliseconds
//
// @param {Number} -> the tempo in Beats/Minute (BPM)
// @return {Number} -> the tempo in Beats/Minute (BPM)
//
function setTempo(t=100){
if (Array.isArray(t)){
t = t[0];
}
notation.bpm = Math.max(1, Number(t));
notation.measureInMs = 60000.0 / notation.bpm * 4;
return getTempo();
}
exports.setTempo = setTempo;
exports.setBPM = setTempo;
// Get the current used tempo
//
// @return {Number} -> tempo in Beats/Minute (BPM)
//
function getTempo(){
return getSettings().bpm;
}
exports.getTempo = getTempo;
exports.getBPM = getTempo;
// Set the scale to use for mapping integer sequences to
//
// @param {String} -> scale name
// @param {Int/String} -> root of the scale (optional, default=c)
// @return {Object} -> the scale, root and rootInt
//
function setScale(s="chromatic", r){
if (Scales[s]){
notation.scale = s;
if (r !== undefined) { setRoot(r); }
notation.map = Scales[s];
}
return getScale();
}
exports.setScale = setScale;
// returns the scale and root as object
//
// @return {Object} -> the scale, root and rootInt
//
function getScale(){
return {
"scale" : getSettings().scale,
"root" : getSettings().root,
"rootInt" : getSettings().rootInt,
"mapping" : getSettings().map
};
}
exports.getScale = getScale;
// Set the root of a scale to use for mapping integer sequences
//
// @param {Int/String} -> root of the scale (optional, default=c)
// @return {Object} -> the scale, root and rootInt
//
function setRoot(r='c'){
if (!isNaN(Number(r))){
notation.rootInt = Math.floor(r);
notation.root = Note.pitchClass(Note.fromMidi(notation.rootInt));
}
// else if (r in ToneSet){
// notation.rootInt = chromaToRelative(r);
// // notation.rootInt = ToneSet[r];
// notation.root = r;
// } else {
// console.log('not a valid root');
// }
else {
notation.rootInt = chromaToRelative(r);
// notation.rootInt = ToneSet[r];
notation.root = r;
}
return getScale();
}
exports.setRoot = setRoot;
// returns the root of the scale as String and integer
//
// @return {Object} -> the scale and root
//
function getRoot(){
return { "root" : getSettings().root, "rootInt" : getSettings().rootInt };
}
exports.getRoot = getRoot;
/* WORK IN PROGRESS
// set a custom mapping for a non existing scale
//
// @params {Array} -> array of length 12 containing semitones
// @return {Void}
//
function setMapping(a){
if (!Array.isArray(a) || a.length < 12){
console.error("not an array or not long enough");
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
}
notation.map = a.slice(0, 12);
// a = (a !== undefined)? a : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
}
exports.setMapping = setMapping;*/
// returns an array of all available scale names
//
// @return {Array} -> scale names
//
function scaleNames(){
return Object.keys(Scales).sort();
}
exports.scaleNames = scaleNames;
exports.getScales = scaleNames;
/* WORK IN PROGRESS
// search scales based on an array of intervals
//
// @param {Array|String} -> array of intervals
// @return {Object} -> possible scales
//
function searchScales(iv){
iv = (Array.isArray(iv))? iv : [iv];
let names = scaleNames();
let scales = names.map(x => Scl.intervals(x));
let arr = [];
for (let n in names){
let includes = 0;
for (let i in iv){
includes += scales[n].includes(iv[i]);
}
if (includes == iv.length){
arr.push({ "scale" : names[n], "intervals" : scales[n]});
}
}
console.log(arr);
}
exports.searchScales = searchScales;*/
// Convert a midi value to a note name (60 => C4)
//
// @param {Number/Array} -> midi values to convert
// @return {String/Array} -> note name
//
function midiToNote(a=60){
if (!Array.isArray(a)){
return Note.fromMidi(a).toLowerCase();
}
return a.map(x => midiToNote(x));
}
exports.midiToNote = midiToNote;
exports.mton = midiToNote;
// Convert a midi value to a frequency (60 => 261.63 Hz)
// With default equal temperament tuning A4 = 440 Hz
// Adjust the tuning with optional second argument
// Adjust the amount of notes per octave (12-TET, 5-TET) with third argument
// Adjust the center c4 midi value with optional fourth argument
//
// @param {Number/Array} -> midi values to convert
// @param {Number} -> tuning
// @param {Number} -> octave division
// @return {Number/Array} -> frequency in Hz
//
function midiToFreq(a=48, t=440, n=12, c=69){
if (!Array.isArray(a)){
return Math.pow(2, (a - c) / n) * t;
}
return a.map(x => midiToFreq(x, t, n, c));
}
exports.midiToFreq = midiToFreq;
exports.mtof = midiToFreq;
// Convert a frequency to closest midi note (261.62 Hz => 60)
// With default equal temperament tuning A4 = 440 Hz
// Set the detune flag to return te exact floating point midi value
//
// @param {Number/Array} -> frequency value
// @param {Number/Array} -> detune precision value (default=false)
// @return {Number/Array} -> midi note
//
function freqToMidi(a=261, d=false){
if (!Array.isArray(a)){
let f = Math.log(a / 440) / Math.log(2) * 12 + 69;
if (!d) {
return Math.round(f);
}
return f;
}
return a.map(x => freqToMidi(x, d));
}
exports.freqToMidi = freqToMidi;
exports.ftom = freqToMidi;
// Convert a frequency to closest note name (261.62 Hz => 'c4')
// With default equal temperament tuning A4 = 440 Hz
//
// @param {Number/Array} -> frequency value
// @return {Number/Array} -> midi note
//
function freqToNote(a=261){
return midiToNote(freqToMidi(a));
}
exports.freqToNote = freqToNote;
exports.fton = freqToNote;
// Convert a pitch name to a midi value (C4 => 60)
//
// @param {String/Array} -> pitch name to convert
// @return {Number/Array} -> midi value
//
function noteToMidi(a='c4'){
if (!Array.isArray(a)){
return Note.midi(a);
}
return a.map(x => noteToMidi(x));
}
exports.noteToMidi = noteToMidi;
exports.ntom = noteToMidi;
// Convert a pitch name to a frequency (C4 => 261.63 Hz)
// With default equal temperament tuning A4 = 440 Hz
//
// @param {String/Array} -> pitch name to convert
// @return {Number/Array} -> frequency in Hz
//
function noteToFreq(a='c4'){
if (!Array.isArray(a)){
return Note.freq(a);
}
return a.map(x => noteToFreq(x));
}
exports.noteToFreq = noteToFreq;
exports.ntof = noteToFreq;
// Convert a chromagram pitch class to a relative note number
//
// @param {String/Array} -> pitchclass names to convert
// @return {Number/Array} -> midi note number
//
function chromaToRelative(c='c'){
if (!Array.isArray(c)){
let m = c.toLowerCase().match(/^[a-g]/);
let v = 0;
if (m){
v = chromaSet[m[0]];
} else {
console.log(`ctor(): '${c}' is not a valid chroma value`);
return 0;
}
let a = c.split('').slice(1);
a.forEach((a) => {
switch(a) {
case '#': v += 1; break;
case 'b': v -= 1; break;
case 'x': v += 2; break;
case '-': v -= 12; break;
case '+': v += 12; break;
}
});
return v;
}
return c.map(x => chromaToRelative(x));
}
exports.chromaToRelative = chromaToRelative;
exports.ctor = chromaToRelative;
// Convert a list of relative semitone intervals to midi
// provide octave offset with second argument. Octave offset
// follows midi octave convention where 3 is 48, 4 is 60 etc.
//
// @param {Number/Array} -> relative
// @param {Number/String} -> octave (optional, default=4)
// @return {Number/Array}
//
function relativeToMidi(a=0, o=4){
if (!Array.isArray(a)){
o = (typeof o === 'string')? Note.midi(o) : (o + 1) * 12;
return a + o;
}
return a.map(x => relativeToMidi(x, o));
}
exports.relativeToMidi = relativeToMidi;
exports.rtom = relativeToMidi;
// Convert a list of semitone intervals to frequency
// provide octave offset
//
// @param {Number/Array} -> semitones
// @param {Number} -> octave (optional, default=4)
// @return {Number/Array}
//
function relativeToFreq(a=0, o=4){
return midiToFreq(relativeToMidi(a, o));
}
exports.relativeToFreq = relativeToFreq;
exports.rtof = relativeToFreq;
// Map a list of relative semitone values to the selected
// scale set with setScale(). Preserves detuning when a
// midi floating point value is used.
// Also offsets the values with the root note selected
//
// @params {Array/Number} -> Array of relative semitones
// @params {String} -> Scale name (optional)
// @params {String/Int} -> Root offset
// @return {Array/Number} -> mapped to scale
//
function mapToScale(a=[0], s, r){
// get the global settings
let scale = getSettings().map;
let root = getSettings().rootInt;
// if a scale is provided and is not undefined
if (s && Scales[s]){
scale = Scales[s];
}
// if a root is provided
if (r) {
root = isNaN(Number(r)) ? chromaToRelative(r) : Math.floor(r);
}
// apply recursively through the entire array
return _mapToScale(a, scale, root);
}
exports.mapToScale = mapToScale;
exports.toScale = mapToScale;
// private function for mapToScale()
//
function _mapToScale(arr, scale, root){
if (!Array.isArray(arr)) {
// detuning float
let d = arr - Math.floor(arr);
// selected semitone
let s = Math.floor(((arr % 12) + 12) % 12);
// octave offset
let o = Math.floor(arr / 12) * 12;
// return notation.map[s] + o + d + notation.rootInt;
return scale[s] + o + d + root;
}
return arr.map(x => _mapToScale(x, scale, root));
}
// Map an array of relative semitone intervals to scale and
// output in specified octave as midi value
//
// @param {Array/Int} -> semitone intervals
// @param {Int/String} -> octave range
// @return {Array/Int} -> mapped midi values
//
function mapToMidi(a=[0], o=4){
return add(relativeToMidi(mapToScale(a), o), notation.rootInt);
}
exports.mapToMidi = mapToMidi;
exports.toMidi = mapToMidi;
// Map an array of relative semitone intervals to scale and
// output in frequency value
//
// @param {Array/Int} -> semitone intervals
// @param {Int/String} -> octave range
// @return {Array/Int} -> mapped midi values
//
function mapToFreq(a=[0], o=4){
// return mapToMidi(a, o);
return midiToFreq(mapToMidi(a, o));
}
exports.mapToFreq = mapToFreq;
exports.toFreq = mapToFreq;
// Convert a frequency ratio string to a corresponding cents value
// eq. ['2/1', '3/2'] => [1200, 701.95]
//
// @param {Number/String/Array} -> ratios to convert
// @return {Number/Array} -> cents output
//
function ratioToCent(a=['1/1']){
a = toArray(a);
return a.map(x => {
if (Array.isArray(x)){
return ratioToCent(x);
}
return Math.log(divRatio(x)) / Math.log(2) * 1200;
});
}
exports.ratioToCent = ratioToCent;
exports.rtoc = ratioToCent;
/* WORK IN PROGRESS
// Convert a midi value to semitone intervals
// provide octave offset
//
// @param {Number/Array} -> semitones
// @param {Number} -> octave (optional, default=4)
// @return {Number/Array}
//
function midiToSemi(a=0, o=4){
if (!Array.isArray(a)){
return a - o * 12;
}
return a.map(x => x - o * 12);
}
exports.midiToSemi = midiToSemi;
exports.mtos = midiToSemi;
*/
// Use a list of roman numerals to translate a chord progression
// The function returns a 2d-array of chords, where every chord is
// a separate array within the larger array. The chords are returned
// as semitones from 0-12. Optionally with a second argument you can
// offset the chords based on a note name or midi value
// eg. IIm with 'D' becomes [E, G, B] becomes => [4, 7, 11]
// Valid chord numerals: I, II, III, ..., VII
// Valid additions: m, M, 7, 9, sus2, sus4, maj7, m7, maj9, m9
//
// @param - {Array/String} -> roman numerals to convert to chords
// @param - {String/Number} -> root for chord progression
// @return - {2d-Array} -> array of chords
//
function chordsFromNumerals(a=['i'], n='c'){
// make array if not array and flatten
a = Array.isArray(a)? a.flat(Infinity) : [a];
// check if n is notename
n = isNaN(n)? n : midiToNote(wrap(n));
// generate progression of chord names
let p = Progression.fromRomanNumerals(n, a);
// translate chordnames to semitones
return chordsFromNames(p);
}
exports.chordsFromNumerals = chordsFromNumerals;
exports.chords = chordsFromNumerals;
// Use a list of chord names to generate a chord progression
// The function returns an array of chords and works on n-dimensional arrays
// where every chord is a separate array within the larger array.
// The chords are returned as semitones from 0-12.
// eg. Em becomes => [4, 7, 11]
// Valid note names: C, D, E ..., B
// Valid additions: m, M, 7, 9, sus2, sus4, maj7, m7, maj9, m9
//
// @param - {Array/String} -> chord names to convert to numbers
// @return - {2d-Array} -> array of chords
//
function chordsFromNames(a=['c']){
// if not an array, translate chordname to semitone array
if (!Array.isArray(a)){
let ch = Chord.get(a);
if (ch.empty){
console.log(`Invalid chord name generated from numeral: ${a}`);
return [0];
}
// return wrap(chromaToRelative(ch.notes));
return chromaToRelative(ch.notes);
}
return a.map(c => chordsFromNames(c));
}
exports.chordsFromNames = chordsFromNames;
// Convert a beat division value to milliseconds based on the global BPM
// eg. ['1/4', 1/8', '1/16'] => [500, 250, 125] @ BPM = 120
// Also works with ratio floating values
//
// @param {Number/String/Array} -> beat division or ratio array
// @param {Number} -> set the BPM (optional, default=globalBPM)
// @return {Number/Array}
//
function divisionToMs(a=['1'], bpm){
return ratioToMs(divisionToRatio(a), bpm);
}
exports.divisionToMs = divisionToMs;
exports.dtoms = divisionToMs;
// Convert a beat ratio value to milliseconds based on the global BPM
// eg. [0.25, 0.125, 0.0625] => [500, 250, 125] @ BPM = 120
//
// @param {Number/String/Array} -> beat ratio array
// @param {Number} -> set the BPM (optional, default=globalBPM)
// @return {Number/Array}
//
function ratioToMs(a=[1], bpm){
let measureMs = notation.measureInMs;
if (bpm){
measureMs = 60000 / Math.max(1, Number(bpm)) * 4;
}
return multiply(a, measureMs);
}
exports.ratioToMs = ratioToMs;
exports.rtoms = ratioToMs;
// Convert a beat ratio value to milliseconds based on the BPM
// eg. [0.25, 0.125, 0.0625] => [500, 250, 125] @ BPM = 120
//
// @param {Number/String/Array} -> beat ratio array
// @return {Number/Array}
//
function divisionToRatio(a=['1']){
a = toArray(a);
return a.map(x => {
if (Array.isArray(x)){
return divisionToRatio(x);
}
return divRatio(x);
});
}
exports.divisionToRatio = divisionToRatio;
exports.dtor = divisionToRatio;
// Evaluate a division string to a ratio
//
function divRatio(x){
// match all division symbols: eg. 1/4, 5/16
let d = /^\d+(\/\d+)?$/;
// output a floating point value
return (typeof x === 'string' && d.test(x))? eval(x) : x;
}
// Convert a division or ratio value to amount of ticks
// Used in software like Ableton, M4L and MaxMSP
//
// @param {Number/String/Array} -> division to convert
// @return {Array}
//
function divisionToTicks(a=['1']){
// 1 tick = 1/480th of a quarter note,
// 1 bar = 1920 ticks
return multiply(divisionToRatio(a), 1920);
}
exports.divisionToTicks = divisionToTicks;
exports.dtotk = divisionToTicks;
exports.ratioToTicks = divisionToTicks;
exports.rtotk = divisionToTicks;
// Convert timevalues to a ratio in floatingpoint
// eg. 4n, 8nt, 16nd, 2m etc.
//
// @param {String/Array} -> timevalues to convert
// @return {Array}
//
function timevalueToRatio(a=['1n']){
a = toArray(a);
return a.map(x => {
if (Array.isArray(x)){
return timevalueToRatio(x);
}
return timevalueRatio(x);
});
}
exports.timevalueToRatio = timevalueToRatio;
exports.ttor = timevalueToRatio;
// Convert timevalues to milliseconds
//
// @param {String/Array} -> timevalues to convert
// @param {Number} -> bpm (optional, default=globalBPM)
// @return {Array}
//
function timevalueToMs(a=['1n'], bpm){
return ratioToMs(timevalueToRatio(a), bpm);
}
exports.timevalueToMs = timevalueToMs;
exports.ttoms = timevalueToMs;
// Convert timevalues to ticks
//
// @param {String/Array} -> timevalues to convert
// @return {Array}
//
function timevalueToTicks(a=['1n']){
return multiply(timevalueToRatio(a), 1920);
}
exports.timevalueToTicks = timevalueToTicks;
exports.ttotk = timevalueToTicks;
function timevalueRatio(x){
let r = /^(\d+)([nm])([dt]?)$/;
let m = x.match(r);
let v = 1;
if (m){
let nm = { 'n' : 1, 'm' : m[1]*m[1] }
let dt = { 'd' : 3/2, 't' : 2/3, '' : 1 }
v = 1 / m[1] * nm[m[2]] * dt[m[3]];
} else {
console.log(`timevalueRatio(): ${x} is not a valid timevalue`);
}
return v;
}
// Convert toneJS time values
// function tonetimeRatio(x){
// }
// Convert a string or array of strings to the
// ASCII code values that belong to those characters
// ASCII is the American Standard Code for Information Interchange
//
// @param {String/Array} -> string to convert
// @return {Array} -> array of integers
//
function textToCode(a=[0]){
if (!Array.isArray(a)){
return String(a).split('').map(c => c.charCodeAt(0));
}
return a.map(x => textToCode(x));
}
exports.textToCode = textToCode;
exports.textCode = textToCode;
exports.ttoc = textToCode;
//=======================================================================
// Scala class
//
// Import a .scl file and convert to a JSON object. Use methods to
// translate numbers into frequencies according to the settings of
// tune, center and the scala cents
//=======================================================================
// const fs = require('fs');
// const path = require('path');
// const TL = require('./translate.js');
// scala database from json
// const db = require('../data/scldb-min.json');
class Scala {
constructor() {
// the converted file to dictionary
this.scl = {
'description' : 'Divide an octave into 12 equal steps',
'size' : 12,
'tune' : 440,
'center' : 69,
'range' : 1200,
'cents' : [ 0, 100, 200, 300, 400, 500,
600, 700, 800, 900, 1000, 1100 ]
};
}
// get the current loaded scala data
//
// @return {Object} -> Object with the loaded scala data
//
get data(){
return { ...this.scl };
}
// get the filenames from the database
//
// @return {Array} -> array with all scala filenames
//
get names(){
const db = require('../data/scldb.json');
return Object.keys(db);
}
// set the tuning in Hz for the center value
//
// @param {Number} -> tuning in Hz
// @return {Void}
//
tune(v){
if (isNaN(Number(v))){
error(v + ' is not a number \n');
} else {
this.scl['tune'] = v;
}
}
// set the center value corresponding with cent 0 and tuning frequency
//
// @param {Int} -> center value as integer
// @return {Void}
//
center(v){
if (isNaN(Number(v))){
error(v + ' is not a number \n');
} else {
this.scl['center'] = v;
}
}
// return the frequency from the scala corresponding to the input number
//
// @params {Number/Array} -> Number to convert
// @return {Number} -> Converted frequency
//
scalaToFreq(a=48){
let isArr = !Array.isArray(a);
let arr = (isArr)? [a] : a;
arr = arr.map((x) => {
let s = this.scl.size;
let n = x - this.scl.center;
let o = Math.floor(n / s) * this.scl.range;
let c = this.scl.cents[((n % s) + s) % s];
return Math.pow(2, (c + o) / 1200) * this.scl.tune;
});
return (isArr)? arr[0] : arr;
}
// shorthand for scalaToFreq()
stof(a=48){
return this.scalaToFreq(a);
}
// search the scala scale database with filter options
//
// @params {Object} -> filter options in the format:
// { size: <Number/Array>, range: <Number>,
// cents: <String/Array>, description: <String/Array> }
// @return {Object -> All scala files matching the filter
//
search(f){
const db = require('../data/scldb.json');
f = (typeof f !== 'undefined') ? f : {};
f.size = (typeof f.size !== 'undefined') ? f.size : null;
f.cents = (typeof f.cents !== 'undefined') ? f.cents : null;
f.description = (typeof f.description !== 'undefined') ? f.description : null;
f.decimals = (typeof f.decimals !== 'undefined') ? f.decimals : 3;
// console.log('search', f);
// let result = { ...db };
let result = JSON.parse(JSON.stringify(db));
Object.keys(f).forEach((k) => {
let tmpRes = {};
// only search the key if filter is added
if (f[k] !== null){
// allow arrays for multiple searches
let s = (!Array.isArray(f[k]))? [f[k]] : f[k];
// serach size with number match
if (k === 'size'){
Object.keys(result).forEach((scl) => {
s.forEach((v) => {
if (result[scl][k] === Number(v)){
tmpRes[scl] = result[scl];
}
});
});
result = tmpRes;
}
// search description with regular expression
if (k === 'description'){
Object.keys(result).forEach((scl) => {
s.forEach((v) => {
if (result[scl][k].match(String(v), 'i')){
tmpRes[scl] = result[scl];
}
});
});
result = tmpRes;
}
// search cents for number or ratio
if (k === 'cents'){
Object.keys(result).forEach((scl) => {
let match = 0;
// temporary cents array
let tmpCents = result[scl][k];
// append the octave ratio (or range)
tmpCents.push(result[scl]['range']);
// filter duplicates
tmpCents = unique(tmpCents).map(x => x.toFixed(f.decimals));
for (let i in s){
// for all entered cent/ratio values
let cent = (typeof s[i] === 'string')? ratioToCent(s[i])[0] : s[i];
// if equals cent from array increment match
for (let c=0; c<tmpCents.length; c++){
if (tmpCents[c] === cent.toFixed(f.decimals)){
match += 1;
}
}
}
// result if matches equals amount of searches
if (match === s.length) {
tmpRes[scl] = result[scl];
}
});
result = tmpRes;
}
}
});
return result;
}
// read and parse a filestring (best imported with fs.readFileSync for
// local usage or fetch() in the browser) to use in the scale
//
// @params {String} -> text as string loaded from .scl file
// @return {Void}
//
parse(f){
// read the file text in variable
// let file = fs.readFileSync(f, 'utf8');
// this.scl.name = path.parse(f).name;
// remove linebreaks and split into array of lines
let file = f.replace(/(\r\n|\n\r|\r|\n)/g, '\n').split('\n');
// empty cents array in dictionary
this.scl.cents = [ 0 ];
// init line number and note count
let l = 0, n = 0;
// iterate through lines
for (var i=0; i<file.length; i++){
let line = file[i];
if (line.match(/^!(.+)?/)) {
// ignore if comment
} else {
// console.log(line, l);
if (l === 0){
// first non-comment line is description
this.scl['description'] = line;
} else if (l === 1){
// second non-comment line is number of notes in scale
this.scl['size'] = Number(line);
} else {
// remove leading, trailing and multiple whitespace
// split line in array
line = line.trim().replace(/\s+/g, ' ').split(' ');
if (n < this.scl.size){
// if line is not a number then it's a ratio
if (isNaN(Number(line[0]))) {
line = ratioToCent(line[0])[0];
} else {
// if line is negative then make absolute
line = (Number(line[0]) < 0)? Math.abs(Number(line[0])) : Number(line[0]);
}
// push notes to object and increment notecount
this.scl.cents.push(line);
n++;
}
}
// increment linecount
l++;
}
}
// sort the cent values
this.scl['cents'] = this.scl.cents.sort((a, b) => {return a-b});
// last value is width of "octave" (usually an octave of 1200)
this.scl['range'] = this.scl.cents.pop();
}
// return an object with frequencies derived from the loaded scala
// mapped to a specific range of values
//
// @params {Int} -> high value for output range (optional, default=127)
// @params {Int} -> low value for output range (optional, default=0)
// @return {Object} -> Object with all values and corresponding frequency
//
chart(hi=127, lo=0){
// swap lo and hi range if hi is smaller than lo
if (hi < lo){ var t=hi, hi=lo, lo=t; }
let range = hi - lo;
// empty object for frequencies
let chart = {};
// calculate frequencies for values 0 to 127
for (var i=0; i<range+1; i++){
chart[i + lo] = this.scalaToFreq(i + lo);
}
return chart;
}
}
exports.Scala = Scala;