mini.mjs - <short description TODO> Copyright (C) 2022 Strudel contributors - see https://codeberg.org/uzu/strudel/src/branch/main/packages/mini/mini.mjs This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
11const randOffset = 0.0003; 12 13const applyOptions = (parent, enter) => (pat, i) => { 14 const ast = parent.source_[i]; 15 const options = ast.options_; 16 const ops = options?.ops; 17 const steps_source = pat.__steps_source; 18 if (ops) { 19 for (const op of ops) { 20 switch (op.type_) { 21 case 'stretch': { 22 const legalTypes = ['fast', 'slow']; 23 const { type, amount } = op.arguments_; 24 if (!legalTypes.includes(type)) { 25 throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`); 26 } 27 pat = strudel.reify(pat)[type](enter(amount)); 28 break; 29 } 30 case 'replicate': { 31 const { amount } = op.arguments_; 32 pat = strudel.reify(pat); 33 pat = pat._repeatCycles(amount)._fast(amount); 34 break; 35 } 36 case 'bjorklund': { 37 if (op.arguments_.rotation) { 38 pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation)); 39 } else { 40 pat = pat.euclid(enter(op.arguments_.pulse), enter(op.arguments_.step)); 41 } 42 break; 43 } 44 case 'degradeBy': { 45 pat = strudel 46 .reify(pat) 47 ._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5); 48 break; 49 } 50 case 'tail': { 51 const friend = enter(op.arguments_.element); 52 pat = pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend); 53 break; 54 } 55 case 'range': { 56 const friend = enter(op.arguments_.element); 57 pat = strudel.reify(pat); 58 const arrayRange = (start, stop, step = 1) => 59 Array.from({ length: Math.abs(stop - start) / step + 1 }, (value, index) => 60 start < stop ? start + index * step : start - index * step, 61 ); 62 let range = (apat, bpat) => apat.squeezeBind((a) => bpat.bind((b) => strudel.fastcat(...arrayRange(a, b)))); 63 pat = range(pat, friend); 64 break; 65 } 66 default: { 67 console.warn(`operator "${op.type_}" not implemented`); 68 } 69 } 70 } 71 } 72 pat.__steps_source = pat.__steps_source || steps_source; 73 return pat; 74};
expects ast from mini2ast + quoted mini string + optional callback when a node is entered
77export function patternifyAST(ast, code, onEnter, offset = 0) { 78 onEnter?.(ast); 79 const enter = (node) => patternifyAST(node, code, onEnter, offset); 80 switch (ast.type_) { 81 case 'pattern': { 82 // resolveReplications(ast); 83 const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter)); 84 const alignment = ast.arguments_.alignment; 85 const with_steps = children.filter((child) => child.__steps_source); 86 let pat; 87 switch (alignment) { 88 case 'stack': { 89 pat = strudel.stack(...children); 90 if (with_steps.length) { 91 pat._steps = lcm(...with_steps.map((x) => Fraction(x._steps))); 92 } 93 break; 94 } 95 case 'polymeter_slowcat': { 96 pat = strudel.stack(...children.map((child) => child._slow(child.__weight))); 97 if (with_steps.length) { 98 pat._steps = lcm(...with_steps.map((x) => Fraction(x._steps))); 99 } 100 break; 101 } 102 case 'polymeter': { 103 // polymeter 104 const stepsPerCycle = ast.arguments_.stepsPerCycle 105 ? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x)) 106 : strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1)); 107 108 const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight)))); 109 pat = strudel.stack(...aligned); 110 break; 111 } 112 case 'rand': { 113 pat = strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children); 114 if (with_steps.length) { 115 pat._steps = lcm(...with_steps.map((x) => Fraction(x._steps))); 116 } 117 break; 118 } 119 case 'feet': { 120 pat = strudel.fastcat(...children); 121 break; 122 } 123 default: { 124 const weightedChildren = ast.source_.some((child) => !!child.options_?.weight); 125 if (weightedChildren) { 126 const weightSum = ast.source_.reduce( 127 (sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)), 128 strudel.Fraction(0), 129 ); 130 pat = strudel.timeCat( 131 ...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]), 132 ); 133 pat.__weight = weightSum; // for polymeter 134 pat._steps = weightSum; 135 if (with_steps.length) { 136 pat._steps = pat._steps.mul(lcm(...with_steps.map((x) => Fraction(x._steps)))); 137 } 138 } else { 139 pat = strudel.sequence(...children); 140 pat._steps = children.length; 141 } 142 if (ast.arguments_._steps) { 143 pat.__steps_source = true; 144 } 145 } 146 } 147 if (with_steps.length) { 148 pat.__steps_source = true; 149 } 150 return pat; 151 } 152 case 'element': { 153 1; 154 return enter(ast.source_); 155 } 156 case 'atom': { 157 if (ast.source_ === '~' || ast.source_ === '-') { 158 return strudel.silence; 159 } 160 if (!ast.location_) { 161 console.warn('no location for', ast); 162 return ast.source_; 163 } 164 const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_; 165 if (offset === -1) { 166 // skip location handling (used when getting leaves to avoid confusion) 167 return strudel.pure(value); 168 } 169 const [from, to] = getLeafLocation(code, ast, offset); 170 return strudel.pure(value).withLoc(from, to); 171 } 172 case 'stretch': 173 return enter(ast.source_).slow(enter(ast.arguments_.amount)); 174 default: 175 console.warn(`node type "${ast.type_}" not implemented -> returning silence`); 176 return strudel.silence; 177 } 178}
takes quoted mini string + leaf node within, returns source location of node (whitespace corrected)
181export const getLeafLocation = (code, leaf, globalOffset = 0) => { 182 // value is expected without quotes! 183 const { start, end } = leaf.location_; 184 const actual = code?.split('').slice(start.offset, end.offset).join(''); 185 // make sure whitespaces are not part of the highlight 186 const [offsetStart = 0, offsetEnd = 0] = actual 187 ? actual.split(leaf.source_).map((p) => p.split('').filter((c) => c === ' ').length) 188 : []; 189 return [start.offset + offsetStart + globalOffset, end.offset - offsetEnd + globalOffset]; 190};
takes quoted mini string, returns ast
193export const mini2ast = (code, start = 0, userCode = code) => { 194 try { 195 return krill.parse(code); 196 } catch (error) { 197 const region = [error.location.start.offset + start, error.location.end.offset + start]; 198 const line = userCode.slice(0, region[0]).split('\n').length; 199 throw new Error(`[mini] parse error at line ${line}: ${error.message}`); 200 } 201};
takes quoted mini string, returns all nodes that are leaves
takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes
mini notation only (wraps in "")
turns str mini string (without quotes) into pattern offset is the position of the mini string in the JS code each leaf node will get .withLoc added this function is used by the transpiler for double quoted strings
includes haskell style (raw krill parsing)