total-serialism
Version:
A set of methods for the generation and transformation of number sequences useful in algorithmic composition
612 lines (560 loc) • 16.2 kB
JavaScript
//=======================================================================
// transform.js
// part of 'total-serialism' Package
// by Timo Hoogland (@t.mo / @tmhglnd), www.timohoogland.com
// MIT License
//
// Methods that transform number sequences
// These are called the "transformers"
// A transformer always takes an input list as the first argument
// A transformer never destructively changes the input list
// The output of the transformer is the modified input list(s)
//
// TODO:
// - make invert() work with note-values 'c' etc.
//
// credits:
// - Many functions are based on Laurie Spiegel's suggestion to
// "extract a basic "library" consisting of the most elemental
// transformations which have consistently been successfully used on
// musical patterns, a basic group of "tried-and-true" musical
// manipulations.", in Manipulation of Musical Patterns (1981)
//=======================================================================
// require the Utility methods
// const Rand = require('./gen-stochastic');
const { sort } = require('./statistic');
const { flat, add, max, min, lerp, toArray, size, unique, arrayCombinations } = require('./utility');
// Duplicate an array multiple times,
// optionaly add an offset to every value when duplicating
// Also works with 2-dimensonal arrays
// If string the values will be concatenated
//
// @param {Array} -> array to clone
// @param {Int, Int2, ... Int-n} -> amount of clones with integer offset
// -> or string concatenation
//
function clone(a=[0], ...c){
a = toArray(a);
if (!c.length) {
// return input if no clone arguments
return a;
} else {
// flatten clone array if multi-dimensional
c = flat(c);
}
let arr = [];
for (let i=0; i<c.length; i++){
arr = arr.concat(a.map(v => add(v, c[i])));
}
return arr;
}
exports.clone = clone;
// combine arrays into one array
// multiple arrays as arguments possible
//
// @params {Array0, Array1, ..., Array-n} -> Arrays to join
// @return {Array}
//
function combine(...arrs){
if (!arrs.length){ return [0]; }
let arr = [];
for (let i=0; i<arrs.length; i++){
arr = arr.concat(arrs[i]);
}
return arr;
}
exports.combine = combine;
exports.join = combine;
// duplicate an array a certain amount of times
//
// @param {Array} -> array to duplicate
// @param {Int} -> amount of output duplicates (optional, default=2)
// @return {Array}
//
function duplicate(a=[0], d=2){
let arr = [];
for (let i=0; i<Math.max(1,d); i++){
arr = arr.concat(a);
}
return arr;
}
exports.duplicate = duplicate;
exports.copy = duplicate;
exports.dup = duplicate;
// pad an array with zeroes (or other values)
// the division determines the amount of values per bar
// total length = bars * div
//
// param {Array} -> Array to use every n-bars
// param {Int} -> amount of bars (optional, default=1)
// param {Int} -> amount of values per bar (optional, default=16)
// param {Value} -> padding argument (optional, default=0)
// param {Number} -> shift the output by n-divs (optional, default=0)
// return {Array}
//
function every(a=[0], bars=1, div=16, pad=0, shift=0){
let len = Math.floor(bars * div);
let sft = Math.floor(shift * div);
return padding(a, len, pad, sft);
}
exports.every = every;
// Import from the Util.flatten
// flatten a multidimensional array. Optionally set the depth
// for the flattening
//
exports.flatten = flat;
exports.flat = flat;
// similar to every(), but instead of specifying bars/divisions
// this method allows you to specify the exact length of the array
// and the shift is not a ratio but in whole integer steps
//
// param {Array} -> Array to use every n-bars
// param {Int} -> Array length output
// param {Number} -> shift the output by n-divs (optional, default=0)
// param {Value} -> padding argument (optional, default=0)
// return {Array}
//
function padding(a=[0], length=16, pad=0, shift=0){
a = toArray(a);
length = size(length);
let len = length - a.length;
if (len < 1) {
return a.slice(0, length);
}
let arr = new Array(len).fill(pad);
return rotate(a.concat(arr), shift);
}
exports.padding = padding;
exports.pad = padding;
// filter one or multiple values from an array
//
// @param {Array} -> array to filter
// @param {Number/String/Array} -> values to filter
// @return (Array} -> filtered array
//
function filter(a=[0], f){
let arr = (Array.isArray(a))? a.slice() : [a];
f = toArray(f);
for (var i=0; i<f.length; i++){
let index = arr.indexOf(f[i]);
while (index >= 0){
arr.splice(index, 1);
index = arr.indexOf(f[i]);
}
}
return arr;
}
exports.filter = filter;
// filter one or multiple datatypes from an array
// In this case the input type is the type that is output
//
// @param {Array} -> array to filter
// @param {String/Array} -> types to filter (default = number)
// @return (Array} -> filtered array
//
function filterType(a=[0], t='number'){
a = (Array.isArray(a))? a.slice() : [a];
t = toArray(t);
let types = a.map(x => typeof x);
let arr = [];
for (let i in t){
let index = types.indexOf(t[i]);
while (index >= 0){
arr.push(a[index]);
a.splice(index, 1);
types.splice(index, 1);
index = types.indexOf(t[i]);
}
}
return arr;
}
exports.filterType = filterType;
exports.tFilter = filterType;
// invert a list of values by mapping the lowest value
// to the highest value and vice versa, flipping everything
// in between.
// Second optional argument sets the center to flip values against.
// Third optional argument sets a range to flip values against.
//
// @param {Array} -> array to invert
// @param {Int} -> invert center / low range (optional)
// @param {Int} -> high range (optional)
// @return {Array}
//
function invert(a=[0], lo, hi){
a = toArray(a);
if (lo === undefined){
// if no center value set lo/hi based on min/max
hi = max(a);
lo = min(a);
} else if (hi === undefined){
// if no hi defined set hi to be same as lo
hi = lo;
}
return a.slice().map(v => {
// apply the algorithm recursively for all items
if (Array.isArray(v)){
return invert(v, lo, hi);
}
return hi - v + lo;
});
}
exports.invert = invert;
// interleave two or more arrays
//
// @param {Array0, Array1, ..., Array-n} -> arrays to interleave
// @return {Array}
//
function lace(...arrs){
if (!arrs.length){ return [0]; }
// get the length of longest list
var l = 0;
for (let i=0; i<arrs.length; i++){
arrs[i] = toArray(arrs[i]);
l = Math.max(arrs[i].length, l);
}
// for the max length push all values of the various lists
var arr = [];
for (var i=0; i<l; i++){
for (var k=0; k<arrs.length; k++){
let v = arrs[k][i];
if (v !== undefined){ arr.push(v); }
}
}
return arr;
}
exports.lace = lace;
exports.zip = lace;
// Build an array of items based on another array of indeces
// The values are wrapped within the length of the lookup array
// Works with n-dimensional arrays by applying a recursive lookup
//
// @param {Array} -> Array with indeces to lookup
// @param {Array} -> Array with values returned from lookup
// @return {Array} -> Looked up values
//
function lookup(idx=[0], arr=[0]){
idx = toArray(idx);
arr = toArray(arr);
let a = [];
let len = arr.length;
for (let i=0; i<idx.length; i++){
// recursively lookup values for multidimensional arrays
if (Array.isArray(idx[i])){
a.push(lookup(idx[i], arr));
} else {
if (!isNaN(idx[i])){
let look = (Math.floor(idx[i]) % len + len) % len;
a.push(arr[look]);
}
}
}
return a;
}
exports.lookup = lookup;
// merge all values of two arrays on the same index
// into a 2D array. preserves length of longest list
// flattens multidimensional arrays to 2 dimensions on merge
//
// @params {Array0, Array1, ..., Array-n} -> Arrays to merge
// @return {Array}
//
function merge(...arrs){
if (!arrs.length){ return [0]; }
let l = 0;
for (let i=0; i<arrs.length; i++){
arrs[i] = toArray(arrs[i]);
l = Math.max(arrs[i].length, l);
}
let arr = [];
for (let i=0; i<l; i++){
let a = [];
for (let k=0; k<arrs.length; k++){
let v = arrs[k][i];
if (v !== undefined){
if (Array.isArray(v)) a.push(...v);
else a.push(v);
}
}
arr[i] = a;
}
return arr;
}
exports.merge = merge;
// reverse an array and concatenate to the input
// creating a palindrome of the array
//
// @param {Array} -> array to make palindrome of
// @param {Bool} -> no-double flag (optional, default=false)
// @return {Array}
//
function palindrome(arr, noDouble=false){
if (arr === undefined){ return [0] };
if (!Array.isArray(arr)){ return [arr] };
let rev = arr.slice().reverse();
if (noDouble){
rev = rev.slice(1, rev.length-1);
}
return arr.concat(rev);
}
exports.palindrome = palindrome;
exports.palin = palindrome;
exports.mirror = palindrome;
// The thumbUp technique takes an array and outputs
// a transformed array where the first value alternates
// between every other value in a left to right order.
// This is based on the Ableton arpeggiator algorithms.
// For example [0 3 7 12 19] results in [0 3 0 7 0 12 0 19]
//
// @param {Array} -> array to transform
// @return {Array}
//
function thumbUp(arr=[0]){
if (arr === undefined){ return [0] };
arr = toArray(arr);
let thumb = arr.shift();
if (arr.length < 1){ return [thumb] };
let out = [];
for (let i=0; i<arr.length; i++){
out.push(thumb);
out.push(arr[i]);
}
return out;
}
exports.thumbUp = thumbUp;
exports.thumb = thumbUp;
// Similar to thumb-up, but in reverse order
// For instance [0 3 7 12 19] results in [0 19 0 12 0 7 0 3]
//
function thumbDown(arr=[0]){
if (arr === undefined){ return [0] };
arr = toArray(arr);
return thumbUp(combine(arr.shift(), reverse(arr)));
}
exports.thumbDown = thumbDown;
// Similar to thumbUp and thumbDown, but a combination of both
// Creates a palindrome of the notes
// For instance [0 3 7 12 19] results in [0 3 0 7 0 12 0 19 0 12 0 7]
//
function thumbUpDown(arr=[0]){
if (arr === undefined){ return [0] };
arr = toArray(arr);
return thumbUp(combine(arr.shift(), palindrome(arr, true)));
}
exports.thumbUpDown = thumbUpDown;
// The pinkyUp technique takes an array and outputs
// a transformed array where the last value alternates
// between every other previous value in a left to right order.
// This is inspired by the Ableton arpeggiator algorithms.
// For example [0 3 7 12 19] results in [0 19 3 19 7 19 12 19]
//
// @param {Array} -> array to transform
// @return {Array}
//
function pinkyUp(arr=[0]){
if (arr === undefined){ return [0] };
arr = toArray(arr);
let pinky = arr.pop();
if (arr.length < 1){ return [pinky] };
let out = [];
for (let i=0; i<arr.length; i++){
out.push(arr[i]);
out.push(pinky);
}
return out;
}
exports.pinkyUp = pinkyUp;
exports.pinky = pinkyUp;
// PinkyDown is similar to pinkyUp, but in reverse order.
// For instance `[0 3 7 12 19]` results in `[12 19 7 19 3 19 0 19]`
//
function pinkyDown(arr=[0]){
if (arr === undefined){ return [0] };
arr = toArray(arr);
let pinky = arr.pop();
return pinkyUp(combine(reverse(arr), pinky));
}
exports.pinkyDown = pinkyDown;
// PinkyUpDown is similar to pinkyUp and pinkyDown and is basically
// a combination of both. For instance `[0 3 7 12 19]` results
// in `[0 19 3 19 7 19 12 19 7 19 3 19]`.
//
function pinkyUpDown(arr=[0]){
if (arr === undefined){ return [0] };
arr = toArray(arr);
let pinky = arr.pop();
return pinkyUp(combine(palindrome(arr, true), pinky));
}
exports.pinkyUpDown = pinkyUpDown;
// repeat the values of an array n-times
// Using a second array for repeat times iterates over that array
//
// @param {Array} -> array with values to repeat
// @param {Int/Array} -> array or number of repetitions per value
// @return {Array}
//
function repeat(arr=[0], rep=1){
arr = toArray(arr);
rep = toArray(rep);
let a = [];
for (let i=0; i<arr.length; i++){
let r = rep[i % rep.length];
r = (isNaN(r) || r < 0)? 0 : r;
for (let k=0; k<r; k++){
a.push(arr[i]);
}
}
return a;
}
exports.repeat = repeat;
// reverse the order of items in an Array
//
// @param {Array} -> array to reverse
// @return {Array}
//
function reverse(a=[0]){
if (!Array.isArray(a)){ return [a]; }
return a.slice().reverse();
}
exports.reverse = reverse;
// rotate the position of items in an array
// 1 = direction right, -1 = direction left
//
// @param {Array} -> array to rotate
// @param {Int} -> steps to rotate (optional, default=0)
// @return {Array}
//
function rotate(a=[0], r=0){
if (!Array.isArray(a)){ return [a]; }
var l = a.length;
var arr = [];
for (var i=0; i<l; i++){
// arr[i] = a[Util.mod((i - r), l)];
arr[i] = a[((i - r) % l + l) % l];
}
return arr;
}
exports.rotate = rotate;
// placeholder for the sort() method found in
// statistic.js
//
exports.sort = sort;
// slice an array in one or multiple parts
// slice lengths are determined by the second argument array
// outputs an array of arrays of the result
//
// @params {Array} -> array to slice
// @params {Number|Array} -> slice points
// @return {Array}
//
function slice(a=[0], s=[0], r=true){
a = toArray(a);
s = toArray(s);
let arr = [];
let _s = 0;
for (let i=0; i<s.length; i++){
if (s[i] > 0){
let _t = _s + s[i];
arr.push(a.slice(_s, _t));
_s = _t;
}
}
if (r){
let rest = a.slice(_s, a.length);
// attach the rest if not an empty array and r=true
if (rest.length > 0){ arr.push(rest); }
}
return arr;
}
exports.slice = slice;
// Similar to slice in that it also splits an array
// excepts slice recursively splits until the array is
// completely empty
//
// @params {Array} -> array to split
// @params {Number/Array} -> split sizes to iterate over
// @return {Array} -> 2D array of splitted values
//
function split(a=[0], s=[1]){
a = toArray(a);
s = toArray(s);
return _split(a, s);
}
exports.split = split;
function _split(a, s){
if (s[0] > 0){
let arr = a.slice(0, s[0]);
let res = a.slice(s[0], a.length);
if (res.length < 1){ return [arr]; }
return [arr, ...split(res, rotate(s, -1))];
}
return [...split(a, rotate(s, -1))];
}
// spray the values of one array on the
// places of values of another array if
// the value is greater than 0
//
// param {Array} -> array to spread
// param {Array} -> positions to spread to
// return {Array}
//
function spray(values=[0], beats=[0]){
values = toArray(values);
beats = toArray(beats);
var arr = beats.slice();
var c = 0;
for (let i in beats){
if (beats[i] > 0){
arr[i] = values[c++ % values.length];
}
}
return arr;
}
exports.spray = spray;
// Alternate through 2 or multiple lists consecutively
// Gives a similar result as lace except the output
// length is the lowest common denominator of the input lists
// so that every combination of consecutive values is included
//
// @param {Array0, Array1, ..., Array-n} -> arrays to interleave
// @return {Array} -> array of results 1 dimension less
//
function step(...arrs){
if (!arrs.length){ return [ 0 ] }
return flat(arrayCombinations(...arrs), 1);
}
exports.step = step;
// stretch (or shrink) an array of numbers to a specified length
// interpolating the values to fill in the gaps.
// TO-DO: Interpolations options are: none, linear, cosine, cubic
//
// param {Array} -> array to stretch
// param {Array} -> outputlength of array
// param {String/Int} -> interpolation function (optional, default=linear)
//
function stretch(a=[0], len=1, mode='linear'){
a = toArray(a);
if (len < 2){ return a; }
len = size(len);
let arr = [];
let l = a.length;
for (let i=0; i<len; i++){
// construct a lookup interpolation position for new array
let val = i / (len - 1) * (l - 1);
// lookup nearest neighbour left/right
let a0 = a[Math.max(Math.trunc(val), 0)];
let a1 = a[Math.min(Math.trunc(val)+1, l-1) % a.length];
if (mode === 'none' || mode === null || mode === false){
arr.push(a0);
} else {
// interpolate between the values according to decimal place
arr.push(lerp(a0, a1, val % 1));
}
}
return arr;
}
exports.stretch = stretch;
// placeholder for unique from Utils.js
// filter duplicate items from an array
// does not account for 2-dimensional arrays in the array
exports.unique = unique;