UNPKG

edo.js

Version:

A set of functions for manipulating musical pitches within a given EDO

1,226 lines (1,083 loc) 395 kB
const environment = (typeof window === 'undefined') ? "server" : "browser" // import { createRequire } from "module"; // const require = createRequire(import.meta.url); let fs, parseXML, midiParser if (environment == 'server') { fs = require('fs') parseXML = require('xml2js').parseString; midiParser = require('midi-parser-js'); } let save_file if (environment == 'server') { /** * @ignore*/ save_file = function (name, dir, contents, _unused) { fs.writeFile(dir + name, contents, function (err) { if (err) { return console.log(err); } }); } } else { /** * Handles file saving when run client-side * @ignore * */ save_file = function (name, dir, contents, mime_type = "text/plain") { const blob = new Blob([contents], {type: mime_type}); const dlink = document.createElement('a'); dlink.download = name; dlink.href = window.URL.createObjectURL(blob); dlink.onclick = function (e) { // revokeObjectURL needs a delay to work properly setTimeout(() => { window.URL.revokeObjectURL(this.href); }, 1500); }; dlink.click(); dlink.remove(); } } let load_file if (environment == 'server') { /** * Handles file loading when run server-side * @ignore * */ /** * @ignore*/ load_file = function (file) { return fs.readFileSync(file, // {encoding:'utf8', flag:'r'} ); } } else { /** * Handles file saving when run client-side * @ignore * */ load_file = function (name, dir, contents) { var fileSelector = document.createElement('input'); fileSelector.setAttribute('type', 'file'); var selectDialogueLink = document.createElement('a'); selectDialogueLink.setAttribute('href', ''); selectDialogueLink.innerText = "Select File"; selectDialogueLink.onclick = function () { fileSelector.click(); return false; } selectDialogueLink.click() } } class FixedContentNecklace { constructor(number_list,method="fast") { /* Class FixedContentNecklace Init Method :param number_list: A list of integers */ // Force negative numbers to zero for (let i = 0; i < number_list.length; i++) { if (number_list[i] < 0) number_list[i] = 0 } this.n_init = number_list this.N = number_list.reduce((t, n) => n + t) this.k = number_list.length this.initialize(method) } initialize(method) { /* Determines what method algorithm to use in the generation :param method: The name of the method/algorithm to use */ this.occurrence = [...this.n_init] this.word = Array(this.N).fill(0) this.alphabet = Array(this.k).fill(0) this.alphabet = this.alphabet.map((el, i, arr) => i) this.run = Array(this.N).fill(0) this.first_letter = 0 this.last_letter = this.k - 1 this.__set_letter_bounds(method) if (method != 'simple') { this.word = [this.word[0]].concat(Array(this.N - 1).fill(this.last_letter)) } } __set_letter_bounds(method) { /* Assign the first letter with nonzero occurrence to word[0], short-circuiting the search to the letter to put there during the algorithm, and finds the last nonzero letter :param method: The name of the method/algorithm to use */ let found_first_nonzero = false for (let letter = 0; letter < this.k; letter++) { if (!found_first_nonzero && this.occurrence[letter] > 0) { found_first_nonzero = true this.occurrence[letter] -= 1 this.word[0] = letter this.first_letter = letter } // remove any letters with zero occurrence from the alphabet so that // we automatically skip them if (method != 'simple') { if (this.occurrence[letter] == 0) { this.__remove_letter(letter) } } } this.last_letter = (!this.alphabet) ? 0 : Math.max.apply(Math, this.alphabet) } * execute(method = "simple") { /* Runs the algorithm that's passed to `method` :param method: The method/algorithm to execute */ this.initialize(method) if (method == 'simple') { yield* this._simple_fixed_content(2, 1) } else if (method == 'fast') { yield* this._fast_fixed_content(2, 1, 2) } } * _simple_fixed_content(t, p) { /* The simple algorithm :param t: ? :param p: ? */ if (t > this.N) { // if the prenecklace is complete if (this.N % p == 0) { // if the prenecklace word is a necklace yield [...this.word] } } else { for (let letter = this.word[t - p - 1]; letter < this.k; letter++) { if (this.occurrence[letter] > 0) { this.word[t - 1] = letter this.occurrence[letter] -= 1 if (letter == this.word[t - p - 1]) { yield* this._simple_fixed_content(t + 1, p) } else { yield* this._simple_fixed_content(t + 1, t) } this.occurrence[letter] += 1 } } } } * _fast_fixed_content(t, p, s) { let i_removed /* The fast algorithm */ if (this.occurrence[this.last_letter] == this.N - t + 1) { if (this.occurrence[this.last_letter] == this.run[t - p - 1]) { if (this.N % p == 0) { yield [...this.word] } } else if (this.occurrence[this.last_letter] > this.run[t - p - 1]) { yield [...this.word] } } else if (this.occurrence[this.first_letter] != this.N - t + 1) { let letter = Math.max.apply(Math, this.alphabet) let i = this.alphabet.length - 1 let s_current = s while (letter >= this.word[t - p - 1]) { this.run[parseInt(s - 1)] = parseInt(t - s) this.word[t - 1] = letter this.occurrence[letter] -= 1 if (!this.occurrence[letter]) { i_removed = this.__remove_letter(letter) } if (letter != this.last_letter) { s_current = t + 1 } if (letter == this.word[t - p - 1]) { yield* this._fast_fixed_content(t + 1, p, s_current) } else { yield* this._fast_fixed_content(t + 1, t, s_current) } if (!this.occurrence[letter]) { this.__add_letter(i_removed, letter) } this.occurrence[letter] += 1 i -= 1 letter = this.__get_letter(i) } this.word[t - 1] = this.last_letter } } __remove_letter(letter) { let index = this.alphabet.indexOf(letter) this.alphabet.splice(index, 1) return index } __add_letter(index, letter) { this.alphabet.splice(index, 0, letter) } __get_letter(index) { return (index < 0) ? -1 : this.alphabet[index] } } const combinations = (set, k) => { if (k > set.length || k <= 0) { return [] } if (k == set.length) { return [set] } if (k == 1) { return set.reduce((acc, cur) => [...acc, [cur]], []) } let combs = [], tail_combs = [] for (let i = 0; i <= set.length - k + 1; i++) { tail_combs = combinations(set.slice(i + 1), k - 1) for (let j = 0; j < tail_combs.length; j++) { combs.push([set[i], ...tail_combs[j]]) } } return combs } const unique_in_array = (base) => [...new Set(base)] const rescale = (num, in_min, in_max, out_min, out_max) => { return (num - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } const GCD = (...n) => n.length === 2 ? n[1] ? GCD(n[1], n[0] % n[1]) : n[0] : n.reduce((a, c) => a = GCD(a, c)); /** Class representing some EDO tuning system.*/ class EDO { /** * <p>Creates a tuning context and system that exposes powerful functions for manipulating, analyzing, and generating music.</p> * <p>This is the main class of the project. At its center stand 7 collections (see "Namespaces" below) of functions.</p> * <ul> * <li> [EDO.convert]{@link EDO#convert} is a set of functions used to change between equivalent representations within the tuning context.</li> * <li> [EDO.count]{@link EDO#count} is a set of functions used to count stuff.</li> * <li> [EDO.get]{@link EDO#get} is a set of functions used to manipulate and generate stuff.</li> * <li> [EDO.is]{@link EDO#is} is a set of functions used for boolean truth statements.</li> * <li> [EDO.show]{@link EDO#show} is a set of functions used for visualization.</li> * <li> [EDO.midi]{@link EDO#midi} is a set of functions used for importing and processing midi files.</li> * <li> [EDO.xml]{@link EDO#xml} is a set of functions used for- importing and processing musicXML files.</li> * <li> [EDO.export]{@link EDO#export} is a set of functions used for exporting the output to various formats.</li> * </ul> * @param {number} edo - The number of equal divisions of the octave. * @example * //Basic usage: * let edo = new EDO(12) //create a new EDO context with 12 divisions. * * //once the object has been created, you can access its functions. * edo.get.inversion([0,2,4,5,7,9,11]) //inverts the pitches * //returns [0, 2, 4, 6, 7, 9, 11] * * edo.convert.ratio_to_interval(3/2) * //returns [7] * * edo.count.pitches([0, 3, 3, 2, 4, 3, 4]) * //returns [[3,3],[4,2], [2,1], [0,1]] (3 appears 3 times, 4 appears 2 times, etc.) * * edo.is.subset([2,4],[1,2,3,4,5]) * //returns true (the set [2,4] IS a subset of [1,2,3,4,5]) */ constructor(edo = 12) { this.edo = edo this.cents_per_step = (12 / edo) * 100 this.M3s = this.convert.ratio_to_interval(5 / 4, 20) this.m3s = this.convert.ratio_to_interval(6 / 5, 20) this.P5s = this.convert.ratio_to_interval(3 / 2, 5) this.edo_divisors = this.get.divisors(edo) this.catalog = {} } /** * <p>Returns a new Scale Object with given pitches</p> * <p>Remark: "pitch classes" conform to the current tuning system used. 0-11 in 12EDO, 0-16 in 17EDO, etc.</p> * @param {Array<Number>} pitches - a collection of pitch classes * @return {Scale} */ scale(pitches,cache = this.cache) { return new Scale(pitches, this, cache) } make_DOM_svg(container_id, width, height, clean = false) { let div = document.createElement('div') div.style.width = width + "px"; div.style.height = height + "px"; div.style.display = "inline" let div_id = div.setAttribute("id", "paper_" + Date.now()); let container = document.getElementById(container_id) if (clean) container.innerHTML = "" container.appendChild(div) const paper = new Raphael(div, width, height); let background = paper.rect(0, 0, width, height).attr('fill', '#000000') return { div_id: div_id, div: div, container_id: container_id, container: container, paper: paper, background: background, width: width, height: height, cleaned: clean } } shuffle_array(arr_in, in_place = true) { let arr if (in_place) arr = arr_in else arr = [...arr_in] for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * i) const temp = arr[i] arr[i] = arr[j] arr[j] = temp } return arr } sort_scales = (scales) => { scales = scales.sort((a, b) => { let run = Math.min(a.pitches.length, b.pitches.length) for (let i = 0; i < run; i++) { if (a.pitches[i] != b.pitches[i]) return a.pitches[i] - b.pitches[i] else if (a.pitches[i] == b.pitches[i] && i == run - 1) return a.pitches.length - b.pitches.length } }) return scales } float_to_rat(x,tolerance = 1.0E-3) { let h1=1, h2=0, k1=0, k2=1; let b = x; do { let a = Math.floor(b); let aux = h1; h1 = a*h1+h2; h2 = aux; aux = k1; k1 = a*k1+k2; k2 = aux; b = 1/(b-a); } while (Math.abs(x-h1/k1) > x*tolerance); return h1+"/"+k1; } /**A collection of functions that convert an input into other equivalent representations * @namespace EDO#convert*/ convert = { /** Expresses cents as intervallic unit (in given EDO) * * @param {Number} interval - cents * @param {Boolean} [round=true] - whether to round the decimals in case not a round number * @returns {Number} An equivilant value represented in intervallic units * @memberOf EDO#convert * @example * let edo = new EDO(24) // define a tuning with 24 divisions of the octave * edo.convert.cents_to_interval(6) * //returns 2*/ cents_to_interval: (cents, round=true) => { let result = cents / this.cents_per_step if(round) result = Math.round(result) return result }, /** Returns a ratio as a decimal number from an interval represented in cents * * @param {Number} cents - an interval in cents * @returns {Number} a ratio * @memberOf EDO#convert * @example * let edo = new EDO(12) // define a tuning system with 12 divisions of the octave * edo.convert.cents_to_ratio(700) * // returns 1.4983070768766815*/ cents_to_ratio: (cents) => { if(Array.isArray(cents)) { return cents.map(e=>this.convert.cents_to_ratio(e)) } return Math.pow(2, cents / 1200) }, cents_to_simple_ratio: (cents,limit=17) => { if(Array.isArray(cents)) return cents.map(c=>this.convert.cents_to_simple_ratio(c,limit)) cents = this.mod(cents,1200) if(cents==0) { return { cents: 0, cents_in_octave: 0, value: 1, diff_in_octave: 0, ratio: '1/1', original: 0 } } let SR = this.get.simple_ratios(limit,true) let min for (let key of Object.keys(SR)) { if(min) { let diff_in_octave = Math.abs(SR[key].cents_in_octave-cents) let diff_min = Math.abs(SR[min].cents_in_octave-cents) if(diff_in_octave<diff_min) min = key } else min = key } SR[min].diff_in_octave = cents-SR[min].cents_in_octave SR[min].ratio = min SR[min].original = cents return SR[min] }, /** Returns the midi_note and cents offset for a given pitch frequency in hertz * * @param {Number} hz - Some frequency of a pitch * @returns {Object} {midi: the midi-note number, cents: fine-tuning of note in cents} * @memberOf EDO#convert * @example * let edo = new EDO() * edo.convert.freq_to_midi(445) * //returns * { midi: 69, cents: 20 } * */ freq_to_midi: (hz) => { let result = (12*Math.log2(hz/440))+69 let midi_note = Math.floor(result) let dec = result-midi_note let cents = Math.round(dec*100) if(cents>50) { midi_note = midi_note+1 cents = (100-cents)*-1 } return {midi:midi_note, cents:cents} }, /** Returns a value in cents from a given interval * * @param {Number} interval - Some interval * @returns {Number} the interval represented in cents * @memberOf EDO#convert * @example * let edo = new EDO(17) // define a tuning with 17 divisions of the octave * edo.convert.interval_to_cents(6) * //returns 423.5294117647059*/ interval_to_cents: (interval) => { return this.cents_per_step * interval }, /** Returns a ratio as a decimal number from a given interval * * @param {Number|Array<Number>} interval - Some interval * @returns {Number} a ratio * @memberOf EDO#convert * @example * let edo = new EDO(12) // define a tuning system with 12 divisions of the octave * edo.convert.interval_to_ratio(7) * // returns 1.4983070768766815*/ interval_to_ratio: (interval) => { if(Array.isArray(interval)) return interval.map(i=>this.convert.interval_to_ratio(i)) return Math.pow(2, interval / this.edo) }, /** Given a list of intervals (or list of lists), returns pitches made with the intervals * starting from starting_pitch * @param {Array<Number>|Array<Array<Number>>} intervals - a list of intervals * @param {Number} [starting_pitch=0] * @param {Boolean} [modulo] if modulo is provided, the pitches will conform to it * @returns {Array<Number>|Array<Array<Number>>} The input as pitches * @memberOf EDO#convert * @example * let edo = new EDO(12) // define a tuning system with 12 divisions of the octave * edo.convert.intervals_to_pitches([2,3]) * //returns [ 0, 2, 5 ]*/ intervals_to_pitches: (intervals, starting_pitch = 0, modulo = undefined) => { let pitches if (modulo) pitches = [mod(starting_pitch, modulo)] else pitches = [starting_pitch] for (let interval of intervals) { if (Array.isArray(interval)) { starting_pitch = pitches.flat()[pitches.flat().length - 1] let result = this.convert.intervals_to_pitches(interval, starting_pitch) result = result.slice(1) pitches.push(result) } else { if (modulo) pitches.push(mod(parseInt(pitches[pitches.length - 1]) + parseInt(interval)), modulo) else pitches.push(parseInt(pitches[pitches.length - 1]) + parseInt(interval)) } } return pitches }, /** <p>Gets a series of intervallic units . Returns a scale as list of pitch classes</p> *<p>Remark: "pitch classes" conform to the current tuning system used. 0-11 in 12EDO, 0-16 in 17EDO, etc.</p> * @param {Array<Number>} intervals - A list of intervals * @example * let edo = new EDO(12) // define a tuning system with 12 divisions of the octave * edo.convert.intervals_to_scale([2, 2, 1, 2, 2, 2, 1]) * // returns [0,2,4,5,7,9,11] * @returns {Number} A scale made up by adding the intervals in order * @memberOf EDO#convert*/ intervals_to_scale: (intervals) => { let pcs = [0] intervals.forEach((interval) => { pcs.push((interval + pcs[pcs.length - 1])) }) return this.scale(pcs,false).pitches }, /** Given a list of midi notes, returns a list of intervals * @param {Array<Number>} midi - a list of midi pitches * @returns {Array<Number>} The input as intervals * @memberOf EDO#convert * @example * let edo = new EDO(12) // define a tuning system with 12 divisions of the octave * edo.convert.midi_to_intervals([60,64,57,61]) * //returns [ 4, -7, 4 ]*/ midi_to_intervals: (midi) => { let intervals = [] for (let i = 0; i < midi.length - 1; i++) { intervals.push(midi[i + 1] - midi[i]) } return intervals }, /** Returns the name of the note (including octave) from a midi value * @param {Array<Number>|Number} note_number - a midi note number or an array of midi note numbers * @param {Number} offset - an amount by which to shift note_number * @example * let edo = new EDO(12) // define a tuning system with 12 divisions of the octave * edo.convert.midi_to_name([60,62]) * //returns ["C4","D4"] * @returns {Array<String>|String} The input as note name(s) * @memberOf EDO#convert*/ midi_to_name: (note_number, offset = 0) => { /*Given a midi note code as an integer, returns its note name and octave disposition (e.g C4 for 60).*/ //only supports 12 edo, so it returns the input if in other edo if (this.edo != 12) return note_number //If it's an array of notes if (Array.isArray(note_number)) { return note_number.map((a) => this.convert.midi_to_name(a, offset)) } else { note_number = note_number + offset let octave = Math.floor(note_number / 12) - 1 let note_name = this.convert.pc_to_name(this.mod(note_number, 12)) return note_name.trim() + octave } }, /** Returns the frequency of the midi note * @param {Array<Number>|Number} note_number - a midi note number or an array of midi note numbers * @param {Number} [offset=0] - By how much to offset every given number * @param {Number} [A=440] - What is the tuning of A * @example * let edo = new EDO(12) // define a tuning system with 12 divisions of the octave * edo.convert.midi_to_freq(69) //returns 440 * edo.convert.midi_to_freq([69,70]) //returns [ 440, 466.1637615180899 ] * @returns {Array<Number>|Number} the frequency of the midi note * @memberOf EDO#convert*/ midi_to_freq: (midi,offset=0,A=440) => { if(Array.isArray(midi)) return midi.map(n=>this.convert.midi_to_freq(n,offset,A)) else return Math.pow(2,((midi+offset)-69)/12)*A }, /** Gets a scale's name, and returns it as a Scale object * * @param {String} name - a scale's name (based on this API's naming formula) * @returns {Scale} a scale object * @memberOf EDO#convert * @example * let edo = new EDO(12) // define a tuning system * edo.convert.name_to_scale('12-1387') * //returns Scale object corresponding to the diatonic scale*/ name_to_scale: (name) => { name = name.split('-') let edo = name[0] name = name[1] if (edo != this.edo) return "Wrong edo" let vector = [] for (let i = edo; i > 0; i--) { let nw = Math.pow(2, i) if (nw > name) continue vector.push(i) name -= nw } vector.push(0) vector.reverse() return this.scale(vector, false) }, /** Returns the name of a note from a given pitch class (supports only 12-edo) * @param {Number | Array<Number>} pc - a pitch class * @returns {String} The input as a note name * @memberOf EDO#convert * @example * let edo = new EDO(12) // define a tuning system with 12 divisions of the octave * edo.convert.pc_to_name(4) * //returns "E" * */ pc_to_name: (pc) => { let PC = { 0: 'C ', 1: 'C#', 2: 'D ', 3: 'Eb', 4: 'E ', 5: 'F ', 6: 'F#', 7: 'G ', 8: 'Ab', 9: 'A ', 10: 'Bb', 11: 'B ', '*': '**' } if (this.edo != 12) return pc if(Array.isArray(pc)) return pc.map(p=>this.convert.pc_to_name(p)) return PC[pc].trim() }, /** Returns the frequency of the given notes * @param {Array<Number>|Number} note_number - a set of pitches * @param {Number} [freq_0=440] - The frequency of note 0 * @returns {Array<Number>|Number} the frequency of the notes * @memberOf EDO#convert*/ pitches_to_freq: (pitches,freq_0=440) => { let edo = this.edo let freqs = pitches.map(p=>Math.pow(2,p/edo)*freq_0) return freqs }, /** <p>Normalizes any input to include pitch-classes only (to ignore octave displacement)</p> * <p>Remark: "pitch classes" conform to the current tuning system used. 0-11 in 12EDO, 0-16 in 17EDO, etc.</p> * @param {Array<Number>} pitches - any collection of pitches (e.g. a melody) * @returns {Array<Number>} the input as pitch classes * @memberOf EDO#convert * @example * let edo = new EDO(12) // define a tuning system with 12 divisions of the octave * edo.convert.pitches_to_PCs([0,2,12,-2,7]) * //returns [0,2,0,10,7] * */ pitches_to_PCs: (pitches) => { return pitches.map((pitch) => this.mod(pitch, this.edo)) }, /** Returns a value in cents to a given input ratio * * @param {Number} ratio - A harmonic ratio * @returns {Number} The ratio represented in cents * @memberOf EDO#convert * @example * let edo = new EDO(12) // define a tuning system * edo.convert.ratio_to_cents(5/4) * //returns 386.3137138648348*/ ratio_to_cents: (ratio) => { return 1200 * Math.log2(ratio) }, /** <p>Returns all of the intervallic units in the EDO that equal to a given ratio (with a given tolerance in cents)</p> *<p>Remark: "intervallic units" conform to the current tuning system used. E.g., 0-11 occupy 1 octave in 12EDO, 0-16 in 17EDO, etc.</p> * @param {Number} ratio - A harmonic ratio * @param {Number} [tolerance=10] - a tolerance (allowed error) in cents * @returns {Array<Number>} Intervals that fit that ratio * @memberOf EDO#convert * @example * let edo = new EDO(12) // define a tuning system * edo.convert.ratio_to_interval(3/2) * //[7] * @example * let edo = new EDO(12) // define a tuning system * edo.convert.ratio_to_interval(5/4,20) //increased the default tolerance (default 10 won't accept IC 4) * //returns [4] * */ ratio_to_interval: (ratio, tolerance = 10) => { let intervals = [] let cents = this.convert.ratio_to_cents(ratio) for (let i = 0; i < this.edo; i++) { let interval = this.convert.interval_to_cents(i) if (Math.abs(interval - cents) <= tolerance) intervals.push(i) else if (intervals.length > 0) break } return intervals }, /** Gets a melody as pitches and returns the melody as intervals * * @param {Array<number>} lst - a collection of pitches * @param {Boolean} [cache=false] - when true the result is cached for faster future retrieval * @returns {Array<Number>} an array of intervals * @memberOf EDO#convert * @example * let edo = new EDO(12) // define a tuning system * edo.convert.to_steps([0,2,4,5,7,9,11]) * //returns [ 2, 2, 1, 2, 2, 2 ]*/ to_steps: (lst, cache = true) => { if(lst.length<=1) return [] if(this.cat_getset(["to_steps",String(lst)])) return this.cat_getset(["to_steps",String(lst)]) let intervals = [] for (let i = 0; i < lst.length - 1; i++) { intervals.push(lst[i + 1] - lst[i]) } if (cache) this.cat_getset(["to_steps",String(lst)],intervals) return intervals } } /**A collection of functions that return an amount * @namespace EDO#count*/ count = { /** * Returns the number of commons tones between two collections of pitches * <p>Remark: "pitches" conform to the current tuning system used. E.g., 0-11 occupy 1 octave in 12EDO, 0-16 in 17EDO, etc.</p> * @param {Array<Number>} list1 - a collection of pitches (not necessarily pitch classes) * @param {Array<Number>} list2 - a collection of pitches (not necessarily pitch classes) * @return {Number} The number of common tones between the two lists * @memberOf EDO#count * @example * let edo = new EDO(12) // define a tuning system * edo.count.common_tones([1,2,4],[2,3,4,5]) * //returns 2 (because 2 and 4 are in both lists) */ common_tones: (list1, list2) => { return list1.reduce((ag,e)=>ag+ list2.includes(e),0) }, /** * From a list of arrays passed to the function, returns the number of differences between each array and its following neighbor. * @param {Array<Number>} ...args - As many arrays as needed. * @return {Number} The number of differences between neighboring arrays * @memberOf EDO#count * @example * let edo = new EDO() * edo.count.differences([0,2,3],[0,1,2],[0,2,4],[0,2,1,1,1]) * // returns [2,2,3] (2 differences between the 1st and 2nd arrays, 2 diffs between the 2nd and 3rd, and 3 diffs between the 3rd and 4th.) */ differences: (...arrays) => { let args = arrays.map((el,i,arr)=>{ if(i!=arr.length-1) { let lena = arr[i].length let lenb = arr[i+1].length let minlen = Math.min(lena,lenb) let maxlen = Math.max(lena,lenb) let diff = maxlen-minlen for (let j = 0; j < minlen; j++) { if(arr[i][j]!=arr[i+1][j]) diff++ } return diff } }) return args.slice(0,args.length-1) }, /** * Returns the pitch and the number of its occurrences as a tuple for every unique value in pitches * @param {Array<Number>} pitches - a collection of pitches (not necessarily pitch classes) * @example * let edo = new EDO(12) // define a tuning system * edo.count.pitches([0, 3, 3, 2, 4, 3, 4]) * // returns [[3,3],[4,2], [2,1], [0,1]] (3 appears 3 times, 4 appears 2 times, etc.) * @return {Array<Number>} A pitch, and how many times it appears * @memberOf EDO#count */ pitches: (pitches) => { let counts = [] let unique = new Set(pitches) for (let pitch of unique) { let count = pitches.reduce((t, e) => { if (e == pitch) return t + 1 else return t }, 0) counts.push([pitch, count]) } counts.sort((a, b) => b[1] - a[1]) return counts }, } /**A collection of functions that exports various file formats * @namespace*/ export = { /** * <p>Downloads / saves a png file with the contents of a container</p> * <p>Note: all of the graphics made with this library create SVG elements, so just pass the same ID that you used to create the graphic in the first place</p> * * @param {String} container_id - The ID of a container that has one or more SVG elements in it. * @memberOf EDO#export * @example * <script src="edo.js"></script> * <script src="raphael.min.js"></script> * <div id="container" style="width:900px;height:600px; margin:0 auto;"></div> * <script> * let edo = new EDO() * //Create a necklace graphic * edo.show.necklace('container', [0,2,4,5,7,9,11]) * * //Save the graphic * edo.export.png('container') //downloads the necklace * </script> */ png: (container_id) => { if (environment == "server") return console.log("This is only support when run on client-side") const triggerDownload = function (imgURI) { let evt = new MouseEvent('click', { view: window, bubbles: false, cancelable: true }); let a = document.createElement('a'); a.setAttribute('download', container_id + '.png'); a.setAttribute('href', imgURI); a.setAttribute('target', '_blank'); a.dispatchEvent(evt); } let el = document.getElementById(container_id) let svgs = el.getElementsByTagName('svg') for (let svg of svgs) { let bBox = svg.getBBox(); let width = bBox.width let height = bBox.height let canvas = document.createElement('canvas'); canvas.width = width canvas.height = height let ctx = canvas.getContext('2d'); let data = (new XMLSerializer()).serializeToString(svg); let DOMURL = window.URL || window.webkitURL || window; let img = new Image(); let mime_type = 'image/svg+xml;charset=utf-8' let svgBlob = new Blob([data], {type: mime_type}); let url = DOMURL.createObjectURL(svgBlob); img.onload = function () { ctx.drawImage(img, 0, 0); DOMURL.revokeObjectURL(url); var imgURI = canvas .toDataURL('image/png') .replace('image/png', 'image/octet-stream'); triggerDownload(imgURI); }; img.src = url; } }, /** * <p>Downloads / saves an SVG file with the contents of a container</p> * <p>Note: all of the graphics made with this library create SVG elements, so just pass the same ID that you used to create the graphic in the first place</p> * * @param {String} container_id - The ID of a container that has one or more SVG elements in it. * @memberOf EDO#export * @example * let edo = new EDO() * //Create a necklace graphic * edo.show.necklace('container', [0,2,4,5,7,9,11]) * * //Save the graphic * edo.export.svg('container') //downloads the necklace */ svg: (container_id) => { if (environment == "server") return console.log("This is only support when run on client-side") let el = document.getElementById(container_id) let svgs = el.getElementsByTagName('svg') for (let svg of svgs) { let svgString = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" + svg.outerHTML let a = document.createElement('a'); a.download = container_id + '.svg'; a.type = 'image/svg+xml'; let blob = new Blob([svgString], {"type": "image/svg+xml"}); a.href = (window.URL || webkitURL).createObjectURL(blob); a.click(); } } } /**A collection of functions manipulating an input * @namespace EDO#get*/ get = { /** <p>Returns the angle created on the necklace for a given trichord.</p> * * <p>If <code>a</code>, <code>b</code>, and <code>c</code>, are vertices of a triangle (trichord) on a necklace. This function returns the angle <code>abc</code>. That is, the angle node b has with a and c.</p> * @param {Array<Number>} triplet - a triplet/trichord of 3 numbers (intervallic units) * @returns {Number} the angle in degrees * @memberOf EDO#get * @example * let edo = new EDO(12) // define a tuning system * edo.get.angle([0,3,6]) //returns 90 */ angle: (triplet) => { let diff1=Math.abs(triplet[0]-triplet[1]) diff1=(diff1>Math.ceil(this.edo/2))?this.edo-diff1:diff1 let diff2=Math.abs(triplet[1]-triplet[2]) diff2=(diff2>Math.ceil(this.edo/2))?this.edo-diff2:diff2 return ((180-diff1/12*360)/2) + ((180-diff2/12*360)/2) }, /** <p>Given an array of scale degrees in cents, returns a Scale Object in the edo that best describes the pitches.</p> * * <p>If <code>a</code>, <code>b</code>, and <code>c</code>, are vertices of a triangle (trichord) on a necklace. This function returns the angle <code>abc</code>. That is, the angle node b has with a and c.</p> * @param {Array<Number>} scale_in_cents - The scale in question represented in cents * @param {Number} begin_edo - The smallest EDO to consider * @param {Number} end_edo - The largest EDO to consider * @returns {Scale} A scale in the best fitting EDO * @memberOf EDO#get * @example * let edo = new EDO() // define a tuning system * edo.get.best_edo_from_cents([0,200,350,500,700,900,1100]) * //returns the Scale Object [0,4,7,10,14,18,22] in a 24EDO context */ best_edo_from_cents: (scale_in_cents,begin_edo=scale_in_cents.length+2,end_edo=24) => { let cents = scale_in_cents let diff = Infinity let min_edo = Infinity for (let i = begin_edo; i <= end_edo; i++) { let ed = new EDO(i) let edo_app = ed.get.notes_from_cents(cents) let edo_diff = edo_app.reduce((ag,e)=>ag+Math.abs(e.diff),0) if (edo_diff<diff) { diff = edo_diff min_edo = i } } let win_edo = new EDO(min_edo) return win_edo.scale(win_edo.get.notes_from_cents(cents).map(e=>e.note)) }, /** Returns the [x,y] coordinates of the nodes of the given pitches. * pitch * @param {Array<Number> | Number} pitch - A pitch, or an array of pitches * @param {Array<Number>} [circle_center=[0,0]] - The center of the circle * @param {Number} [r=0.56418958354776] - The radius of the circle. By default the radius is of a circle with area=1 * @returns {Array<Array<Number,Number>>} An array with tuples each corresponding to the x,y position of every pitch * @memberOf EDO#get * @see Scale#get.coordinates * @example * let edo = new EDO(12) //define context * edo.get.coordinates([0,3,7]) //minor triad * //returns * [ * [0,0.56418958354776], * [0.56418958354776,3.454664838020213e-17], * [-0.2820947917738801,-0.48860251190292314] * ] * */ coordinates: (pitch,circle_center = [0,0],r=0.56418958354776 ) => { if(Array.isArray(pitch)) return pitch.map(p=>this.get.coordinates(p,circle_center,r)) const angle_mult = 360/this.edo pitch = this.mod(pitch,this.edo) const angle = (pitch*angle_mult)*(Math.PI/180) const x = (r*Math.sin(angle))+circle_center[0] const y = (r*Math.cos(angle))+circle_center[1] return [x,y] }, /** <p>Returns a vector describing the contour of the given pitches.</p> * * <p>If local is set to true, every cell in the vector will be * either 1 if note n is higher than n-1, 0 if note n is the same as n-1, and -1 if note n is lower than n-1 * For instance <code>[0,0,4,7,4,7,4,0]</code> will in local mode will return <code>[0,1,1,-1,1,-1,-1]</code></p> * * <p>If local is set to false (default), the contour of the line is expressed such that the actual pitch-class of the * note is removed but its relative position in regards to the entire line is kept. * <code>[0,4,7,12,16,7,12,16]</code> (Bach prelude in C) has 5 distinct note heights, so it will return * <code>[0,1,2,3, 4, 2,3, 4]</code> indicating the relative height of each note in the entire phrase</p> * @param {Array<Number>} pitches - a given array of pitches * @param {Boolean} [local=false] - if set to false, function will only return subsets that have consecutive members * @returns {Array<Number>} * @memberOf EDO#get * @example * let edo = new EDO(12) // define a tuning system * edo.get.contour([0,4,7,12,16,7,12,16]) * //returns [0, 1, 2, 3, 4, 2, 3, 4] * @example * let edo = new EDO(12) // define a tuning system * edo.get.contour([0,4,7,12,16,7,12,16],true) * //returns [1, 1, 1, 1,-1, 1, 1] */ contour: (pitches, local = false) => { if (local) { let vector = [] for (let i = 1; i < pitches.length; i++) { if (pitches[i] > pitches[i - 1]) vector.push(1) else if (pitches[i] == pitches[i - 1]) vector.push(0) else vector.push(-1) } return vector } else { let catalog = {} let unique_pitches = this.get.unique_elements(pitches) unique_pitches = unique_pitches.sort((a, b) => a - b) for (let i = 0; i < unique_pitches.length; i++) { catalog[unique_pitches[i]] = i } let vector = pitches.map((pitch) => catalog[pitch]) return vector } }, /** * <p>Extracts every possible contour motive from a given melody. </p> * <p>The function extracts every contour subset appearing in the given melody. * The function also keeps track of the number of times each motive appeared.</p> * @param {Array<Number>} melody - a collection of pitches to find (in order) * @param {Boolean} [allow_skips=false] - if false, the search will only be done on consecutive items * @param {Number} [maximal_length=8] - Do not look for motives longer than this value. * @return {Array<motives>} * @memberOf EDO#get * @function * @example * let edo = new EDO(12) // define a tuning system * edo.get.contour_motives([7,6,7,6,7,2,5,3,0]).slice(0,4) //get first 3 motives * //returns * [ * { motive: [ -1, 1 ], incidence: 2 }, //going a half-step down, then up appears twice * { motive: [ -1 ], incidence: 2 }, //going a half-step down appears twice * { motive: [ 1 ], incidence: 2 } //going a half-step up appears twice * ] */ contour_motives: (melody, allow_skips = false,maximal_length=8) => { let motives = [] let all_subsets = this.get.subsets(melody, allow_skips).map((subset) => this.get.contour(subset)).filter((contour) => contour.length > 1) all_subsets = all_subsets.filter(sub=>sub.length<=maximal_length) let unique_subsets = this.get.unique_elements(all_subsets) motives = unique_subsets.map((subset) => { let count = 0 for (let i = 0; i < all_subsets.length; i++) { if (this.is.same(subset, all_subsets[i])) count++ } return {motive: subset, incidence: count} }) motives = motives.sort((a, b) => b.incidence - a.incidence || b.motive.length - a.motive.length) return motives }, /** * <p>From a given set of pitches, returns every combination (order specific) of size k</p> * <p>(For a similar function where the order doesn't matter use [EDO.get.n_choose_k()]{@link EDO#get.n_choose_k}) * @param {Array<Number>} set - The array from which to extract combinations * @param {Number} k - The number of elements per set returned * @return {Array<Array<Number>>} * @memberOf EDO#get * @example * edo.get.combinations([1,3,5,7],2) * //returns * [[1,3],[3,1],[1,5],[5,1],[1,7],[7,1],[3,5],[5,3],[3,7],[7,3],[5,7],[7,5]] */ combinations: (set, k) => { let combs = this.get.n_choose_k(set,k) combs = combs.map(e=>this.get.permutations(e)) combs = combs.flat() combs = this.get.unique_elements(combs) return combs }, /** <p>Returns the complementary interval (needed to complete the octave) for a given an interval class.</p> * @param {Number} interval - Some interval class * @returns {Number} * @memberOf EDO#get * @example * let edo = new EDO(12) // define a tuning system * edo.get.complementary_interval(3) //returns 9 * */ complementary_interval: (interval) => { return this.edo - interval }, /** <p>Returns all the pitch-classes of the EDO that the scale does not use.</p> * <p>Remark: "pitch-classes" conform to the current tuning system used. E.g., 0-11 in 12EDO, 0-16 in 17EDO, etc.</p> * @param {boolean} [from_0=false] - when true, the output will be normalized to 0. * @returns {Array<Number>} * @memberOf EDO#get * @example * let edo = new EDO(12) // define a tuning system * edo.get.complementary_set([0,2,4,5,7,9,11]) * //returns [1, 3, 6, 8, 10] * * edo.get.complementary_set([0,2,4,5,7,9,11],true) * //returns [0, 2, 5, 7, 9] */ complementary_set: (pitches, from_0) => { let PCs = Array.from(Array(this.edo).keys()) pitches.forEach((PC) => { (PCs.indexOf(PC) != -1) ? PCs.splice(PCs.indexOf(PC), 1) : true }) if (from_0) PCs = this.scale(PCs, false).pitches return PCs }, /** <p>Returns N step constituents such that they minimize the size between the smallest and largest constituents</p> * @param {Number} n - The numbers of desired steps * @returns {Array<Number>} * @memberOf EDO#get * @example * let edo = new EDO(12) // define a tuning system * edo.get.evenly_split(5) //returns [ 2, 2, 2, 3, 3 ] * edo.get.evenly_split(6) //returns [ 2, 2, 2, 2, 2, 2 ] * edo.get.evenly_split(7) //returns [ 1, 1, 2, 2, 2, 2, 2 ] * edo.get.evenly_split(8) //returns [ 1, 1, 1, 1, 2, 2, 2, 2 ] */ evenly_split: (n) => { let intervals = [] let edo = this.edo if (edo < n) return intervals else if (edo % n == 0) { intervals = Array.from(Array(n).fill(edo/n)) } else { let zp = n - (edo % n); let pp = Math.floor(edo / n); for (let i = 0; i < n; i++) { if (i >= zp) intervals.push((pp + 1)) else intervals.push(pp) } } return intervals }, /** <p>Returns a chord progression of length <code>num_of_chords</code> using only <code>allowed_qualities</code>, with at least <code>common_notes_min</code> notes in