UNPKG

total-serialism

Version:

A set of methods for the generation and transformation of number sequences useful in algorithmic composition

584 lines (544 loc) 18.1 kB
//============================================================================== // gen-complex.js // part of 'total-serialism' Package // by Timo Hoogland (@t.mo / @tmhglnd), www.timohoogland.com // MIT License // // Complex Algorithms and methods that generate number sequences as // startingpoint for composing melodies, rhythms and more // // credits: // - euclid() based on paper by Godfried Toussaint // http://cgm.cs.mcgill.ca/~godfried/publications/banff.pdf // and code from https://github.com/brianhouse/bjorklund // - hexBeat() inspired by Steven Yi's implementation in the csound // livecode environment from // https://github.com/kunstmusik/csound-live-code // and here https://kunstmusik.github.io/learn-hex-beats/ // - fibonacci(), nbonacci() and pisano() inspired by 'fibonacci motion' // used by composer Iannis Xenakis and 'symbolic music'. See further // reading in README.md. Also inspired by Numberphile videos on // pisano period on youtube. // - infinitySeries(), contributed by Stephen Meyer and based on // https://www.lawtonhall.com/blog/2019/9/9/per-nrgrds-infinity-series#:~:text=Coding%20the%20Infinity%20Series // //============================================================================== const { mod, size } = require('./utility'); const { rotate } = require('./transform'); const BigNumber = require('bignumber.js'); // configure the bignumber settings BigNumber.config({ DECIMAL_PLACES: 20, EXPONENTIAL_AT: [-7, 20] }); // A hexadecimal rhythm generator. Generates values of 0 and 1 // based on the input of a hexadecimal character string // Does not work with `0x` hexadecimal notation, for that use binary() // // @param {String/Number} -> hexadecimal characters (0 t/m f) // @return {Array} -> rhythm // function hexBeat(hex="8"){ // convert to string if a number if (!hex.isNaN){ hex = hex.toString(); } let a = []; // for every char in string get binary expansion for (let i=0; i<hex.length; i++){ let binary = parseInt("0x" + hex[i]).toString(2); binary = isNaN(binary)? '0000' : binary; // pad with leading 0's to ensure 4 values let padding = binary.padStart(4, '0'); a = a.concat(padding.split('').map(x => Number(x))); } return a; } exports.hexBeat = hexBeat; exports.hex = hexBeat; // A fast euclidean rhythm algorithm // Uses the downsampling of a line drawn between two points in a // 2-dimensional grid to divide the squares into an evenly distributed // amount of steps. Generates correct distribution, but the distribution // may differ a bit from the recursive euclidean distribution algorithm // for steps above 44. // // @param {Int} -> steps (optional, default=8) // @param {Int} -> beats (optional, default=4) // @param {Int} -> rotate (optional, default=0) // @return {Array} // function fastEuclid(s=8, h=4, r=0){ let arr = []; let d = -1; // steps/hits is minimum of 1 or array length s = size(s); h = size(h); for (let i=0; i<s; i++){ let v = Math.floor(i * (h / s)); arr[i] = Number(v !== d); d = v; } if (r){ return rotate(arr, r); } return arr; } exports.fastEuclidean = fastEuclid; exports.fastEuclid = fastEuclid; // The Euclidean rhythm generator // Generate a euclidean rhythm evenly spacing n-beats amongst n-steps. // Inspired by Godfried Toussaints famous paper "The Euclidean Algorithm // Generates Traditional Musical Rhythms". // // @param {Int} -> steps (optional, default=8) // @param {Int} -> beats (optional, default=4) // @param {Int} -> rotate (optional, default=0) // @return {Array} // let pattern, counts, remainders; function euclid(steps=8, beats=4, rot=0){ // steps/hits is minimum of 1 or array length steps = size(steps); beats = size(beats); pattern = []; counts = []; remainders = []; var level = 0; var divisor = steps - beats; remainders.push(beats); while (remainders[level] > 1){ counts.push(Math.floor(divisor / remainders[level])); remainders.push(divisor % remainders[level]); divisor = remainders[level]; level++; } counts.push(divisor); build(level); return rotate(pattern, rot - pattern.indexOf(1)); } exports.euclidean = euclid; exports.euclid = euclid; function build(l){ var level = l; if (level == -1){ pattern.push(0); } else if (level == -2){ pattern.push(1); } else { for (var i=0; i<counts[level]; i++){ build(level-1); } if (remainders[level] != 0){ build(level-2); } } } // Lindenmayer String expansion // a recursive fractal algorithm to generate botanic (and more) // Default rule is 1 -> 10, 0 -> 1, where 1=A and 0=B // Rules are specified as a JS object consisting of strings or arrays // // @param {String} -> the axiom (the start) // @param {Int} -> number of generations // @param {Object} -> production rules // @return {String/Array} -> axiom determins string or array output // function linden(axiom=[1], iteration=3, rules={1: [1, 0], 0: [1]}){ axiom = (typeof axiom === 'number')? [axiom] : axiom; let asString = typeof axiom === 'string'; let res; // return axiom of iterations is < 1 if (iteration < 1){ return axiom }; for(let n=0; n<iteration; n++){ res = (asString)? "" : []; for(let ch in axiom){ let char = axiom[ch]; let rule = rules[char]; if(rule){ res = (asString)? res + rule : res.concat(rule); }else{ res = (asString)? res + char : res.concat(char); } } axiom = res; } return res; } exports.linden = linden; // Generate a single sequence of the Collatz Conjecture given // a starting value greater than 1 // The conjecture states that any giving positive integer will // eventually reach zero after iteratively applying the following rules // if the number is even, divide by 2 // if the number is odd, multiply by 3 and add 1 // // @param {Int+} -> starting number // @return {Array} -> the sequence (inverted, so starting at 1) // function collatz(n=12){ n = Math.max(2, n); let sequence = []; while (n != 1){ if (n % 2){ n = n * 3 + 1; } else { n = n / 2; } sequence.push(n); } return sequence.reverse(); } exports.collatz = collatz; // Return the modulus of a collatz conjecture sequence // Set the modulo // // @param {Int+} -> starting number // @param {Int+} -> modulus // function collatzMod(n=12, m=2){ return mod(collatz(n), Math.min(m, Math.floor(m))); } exports.collatzMod = collatzMod; // The collatz conjecture with BigNumber library // function bigCollatz(n=12){ let num = new BigNumber(n); let sequence = []; while (num.gt(1)){ if (num.mod(2).eq(1)){ num = num.times(3); num = num.plus(1); } else { num = num.div(2); } sequence.push(num.toFixed()); } return sequence.reverse(); } exports.bigCollatz = bigCollatz; // Return the modulus of a collatz conjecture sequence // Set the modulo // function bigCollatzMod(n=12, m=2){ let arr = bigCollatz(n); for (let i in arr){ arr[i] = new BigNumber(arr[i]); arr[i] = arr[i].mod(m).toNumber(); } return arr; } exports.bigCollatzMod = bigCollatzMod; // Generate any n-bonacci sequence as an array of BigNumber objects // F(n) = t * F(n-1) + F(n-2). This possibly generatres various // integer sequences: fibonacci, pell, tribonacci // // @param {Int} -> output length of array // @param {Int} -> start value 1 // @param {Int} -> start value 2 // @param {Int} -> multiplier t // @return {Array} -> array of BigNumber objects // function numBonacci(len=1, s1=0, s2=1, t=1){ var n1 = new BigNumber(s2); //startvalue n-1 var n2 = new BigNumber(s1); //startvalue n-2 var cur = 0, arr = [n2, n1]; if (len < 3) { // return arr; return arr.slice(0, len); } else { len = Math.max(1, len-2); for (var i=0; i<len; i++){ // general method for nbonacci sequences // Fn = t * Fn-1 + Fn-2 cur = n1.times(t).plus(n2); n2 = n1; // store n-1 as n-2 n1 = cur; // store current number as n-1 arr.push(cur); // store BigNumber in array } return arr; } } // Generate any n-bonacci sequence as an array of BigNumber objects // for export fuction. F(n) = t * F(n-1) + F(n-2) // // @param {Int} -> output length of array // @param {Int} -> start value 1 (optional, default=0) // @param {Int} -> start value 2 (optional, default=1) // @param {Int} -> multiplier (optional, default=1) // @return {String-Array} -> array of bignumbers as strings // function nbonacci(len=1, s1=0, s2=1, t=1, toString=false){ return numBonacci(len, s1, s2, t).map(x => { return (toString)? x.toFixed() : x.toNumber() }); } exports.nbonacci = nbonacci; // Generate the Fibonacci sequence as an array of BigNumber objects // F(n) = F(n-1) + F(n-2). The ratio between consecutive numbers in // the fibonacci sequence tends towards the Golden Ratio (1+√5)/2 // OEIS: A000045 (Online Encyclopedia of Integer Sequences) // When working with larger fibonacci-numbers then possible in 64-bit // Set the toString to true // // @param {Int} -> output length of array // @param {Int} -> offset in sequence (optional, default=0) // @param {Bool} -> numbers as strings (optional, default=false) // @return {String-Array} -> array of bignumbers as strings // function fibonacci(len=1, offset=0, toString=false){ var f = numBonacci(len+offset, 0, 1, 1).map(x => { return (toString)? x.toFixed() : x.toNumber() }); if (offset > 0){ return f.slice(offset, offset+len); } return f; } exports.fibonacci = fibonacci; // Generate the Pisano period sequence as an array of BigNumber objects // Returns array of [0] if no period is found within the default length // of fibonacci numbers (256). Mod value is a minimum of 2 // // F(n) = (F(n-1) + F(n-2)) mod a. // // @param {Int} -> output length of array // @param {Int} -> modulus for pisano period // @return {Int-Array} -> array of integers // function pisano(mod=12, len=-1){ if (mod < 2){ return [0]; } if (len < 1){ return pisanoPeriod(mod); } else { return numBonacci(len, 0, 1, 1).map(x => x.mod(mod).toNumber()); } } exports.pisanoPeriod = pisano; exports.pisano = pisano; function pisanoPeriod(mod=2, length=32){ // console.log('pisano', '@mod', mod, '@length', length); var seq = numBonacci(length, 0, 1, 1).map(x => x.mod(mod).toNumber()); var p = [], l = 0; for (var i=0; i<seq.length; i++){ // console.log(i, seq[i]); p.push(seq[i]); if (p.length > 2){ var c = [0, 1, 1]; var equals = 0; // compare last 3 values with [0, 1, 1] for (let k=0; k<p.length; k++){ equals += p[k] === c[k]; // console.log('>>', equals); } // if equals slice the sequence and return if (equals === 3 && l > 3){ // console.log('true'); return seq.slice(0, l); } p = p.slice(1, 3); l++; } } // console.log('no period, next iteration'); return pisanoPeriod(mod, length*2); } // Generate the Pell numbers as an array of BigNumber objects // F(n) = 2 * F(n-1) + F(n-2). The ratio between consecutive numbers // in the pell sequence tends towards the Silver Ratio 1 + √2. // OEIS: A006190 (Online Encyclopedia of Integer Sequences) // // @param {Int} -> output length of array // @param {Int} -> offset in sequence (optional, default=0) // @param {Bool} -> numbers as strings (optional, default=false) // @return {String-Array} -> array of bignumbers as strings // function pell(len=1, offset=0, toString=false){ var f = numBonacci(len+offset, 0, 1, 2).map(x => { return (toString)? x.toFixed() : x.toNumber() }); if (offset > 0){ return f.slice(offset, offset+len); } return f; } exports.pell = pell; // Generate the Tribonacci numbers as an array of BigNumber objects // F(n) = 2 * F(n-1) + F(n-2). The ratio between consecutive numbers in // the 3-bonacci sequence tends towards the Bronze Ratio (3 + √13) / 2. // OEIS: A000129 (Online Encyclopedia of Integer Sequences) // // @param {Int} -> output length of array // @param {Int} -> offset in sequence (optional, default=0) // @param {Bool} -> numbers as strings (optional, default=false) // @return {String-Array} -> array of bignumbers as strings // function threeFibonacci(len=1, offset=0, toString=false){ let f = numBonacci(len+offset, 0, 1, 3).map(x => { return (toString)? x.toFixed() : x.toNumber() }); if (offset > 0){ return f.slice(offset, offset+len); } return f; } exports.threeFibonacci = threeFibonacci; // Generate the Lucas numbers as an array of BigNumber objects // F(n) = F(n-1) + F(n-2), with F0=2 and F1=1. // OEIS: A000032 (Online Encyclopedia of Integer Sequences) // // @param {Int} -> output length of array // @param {Int} -> offset in sequence (optional, default=0) // @param {Bool} -> numbers as strings (optional, default=false) // @return {String-Array} -> array of bignumbers as strings // function lucas(len=1, offset=0, toString=false){ let f = numBonacci(len+offset, 2, 1, 1).map(x => { return (toString)? x.toFixed() : x.toNumber() }); if (offset > 0){ return f.slice(offset, offset+len); } return f; } exports.lucas = lucas; // Generate the Nørgård infinity series sequence. // // @param {Int+} -> size the length of the resulting Meldoy's steps (default=16) // @param {Array} -> seed the sequence's first two steps (defaults = [0, 1]) // @param {Int} -> offset from which the sequence starts // @return {Array} -> an Array with the infinity series as its steps // function infinitySeries(len=16, seed=[0,1], offset=0){ len = size(len); let root = seed[0]; let step1 = seed[1]; let seedInterval = step1 - root; let steps = Array.from(new Array(len), (n, i) => i + offset).map(step => { return root + (norgardInteger(step) * seedInterval); }); return steps; } exports.infinitySeries = infinitySeries; exports.infSeries = infinitySeries; // Returns the value for any index of the base infinity series sequence // (0, 1 seed). This function enables an efficient way to compute any // arbitrary section of the infinity series without needing to compute // the entire sequence up to that point. // // This is the Infinity Series binary trick. Steps: // 1. Convert the integer n to binary string // 2. Split the string and map as an Array of 1s and 0s // 3. Loop thru the digits, summing the 1s digits, and changing the // negative/positve polarity **at each step** when a 0 is encounterd // // @param {Int} -> index the 0-based index of the infinity series // @return -> the value in the infinity series at the given index. // function norgardInteger(index) { var binaryDigits = index.toString(2).split("").map(bit => parseInt(bit)); return binaryDigits.reduce((integer, digit) => { return (digit === 1)? integer+=1 : integer*= -1; }, 0); } // Generate an Elementary Cellular Automaton class // This is an one dimensional array (collection of cells) with states // that are either dead or alive (0/1). By following a set of rules the // next generation is calculated for every cell based on its neighbouring // cells. Invoke the next() method to iterate the generations. Set the first // generation with the feed() method (usually random values work quite well) // Change the rule() based on a decimal number or an array of digits // // Some interesting rules to try: // 3 5 9 18 22 26 30 41 45 54 60 73 90 105 // 106 110 120 122 126 146 150 154 181 // // @constructor {length, rule} -> generate the CA // @get state -> return the current generations as array // @get table -> return the table of rules // @method rule() -> set the rule based on decimal number or array // @method feed() -> feed the initial generation with an array // @method next() -> generate the next generation and return // class Automaton { constructor(l=8, r=110){ // the size of the population for each generation this._length = Math.max(3, l); // the state of the current generation this._state = new Array(this._length).fill(0); // the rule (will be converted to binary representation) this._rule = this.ruleToBinary(r).split(''); // the rule table for lookup this._table = this.binaryToTable(this._rule); } get state(){ // return the current state of the Automaton return this._state; } get table(){ // return the object of rules return this._table; } rule(a){ // set the rule for the automaton if (Array.isArray(a)){ // when the argument is an array of 1's and 0's convert to table if (a.length != 8){ console.log('Warning: rule() must have length 8 to correctly represent all possible states'); } let r = a.slice(0, 8).join('').padStart(8, '0'); // this._rule = parseInt(r, 2); this._table = this.binaryToTable(r); } else if (typeof a === 'object'){ // when the argument is an object store it directly in table if (Object.keys(a).length != 8){ console.log('Warning: rule() must have 8 keys to correctly represent all possible states') } this._rule = undefined; this._table = { ...a }; } else { if (isNaN(Number(a))){ console.error('Error: rule() expected a number but received:', a); } else { // when the argument is a number let b = this.ruleToBinary(Number(a)); this._rule = a; this._table = this.binaryToTable(b); } } } feed(a){ // feed the automaton with an initial array if (!Array.isArray(a) || a.length < 3){ console.log('Warning: feed() expected array of at least length 3 but received:', typeof a, 'with length:', (Array.isArray(a)?a:[a]).length); } else { this._state = a; this._length = a.length; } } next(){ // calculate the next generation from the rules let n = []; let l = this._length; // for every cell in the current state, check the neighbors for (let i = 0; i < l; i++){ let left = this._state[((i-1 % l) + l) % l]; let right = this._state[((i+1 % l) + l) % l]; // join 3 cells to string and lookup next value from table n[i] = this._table[[left, this._state[i], right].join('')]; } // store in state and return result as array return this._state = n; } ruleToBinary(r){ // convert a rule number to binary sequence return r.toString(2).padStart(8, '0'); } binaryToTable(r){ // store binary sequence in lookup table let c = {}; for (let i = 0; i < 8; i++){ c[(7-i).toString(2).padStart(3, '0')] = Number(r[i]); } return c; } } exports.Automaton = Automaton;