1/* 2mini.mjs - <short description TODO> 3Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/mini/mini.mjs> 4This 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/>. 5*/ 6 7import * as krill from './krill-parser.js'; 8import * as strudel from '@strudel/core'; 9import Fraction, { lcm } from '@strudel/core/fraction.mjs'; 10 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}; 75 76// 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} 179 180// 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}; 191 192// 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}; 202 203// takes quoted mini string, returns all nodes that are leaves 204export const getLeaves = (code, start, userCode) => { 205 const ast = mini2ast(code, start, userCode); 206 let leaves = []; 207 patternifyAST( 208 ast, 209 code, 210 (node) => { 211 if (node.type_ === 'atom') { 212 leaves.push(node); 213 } 214 }, 215 -1, 216 ); 217 return leaves; 218}; 219 220// takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes 221export const getLeafLocations = (code, start = 0, userCode) => { 222 return getLeaves(code, start, userCode).map((l) => getLeafLocation(code, l, start)); 223}; 224 225// mini notation only (wraps in "") 226export const mini = (...strings) => { 227 const pats = strings.map((str) => { 228 const code = `"${str}"`; 229 const ast = mini2ast(code); 230 return patternifyAST(ast, code); 231 }); 232 return strudel.sequence(...pats); 233}; 234 235// turns str mini string (without quotes) into pattern 236// offset is the position of the mini string in the JS code 237// each leaf node will get .withLoc added 238// this function is used by the transpiler for double quoted strings 239export const m = (str, offset) => { 240 const code = `"${str}"`; 241 const ast = mini2ast(code); 242 return patternifyAST(ast, code, null, offset); 243}; 244 245// includes haskell style (raw krill parsing) 246export const h = (string) => { 247 const ast = mini2ast(string); 248 return patternifyAST(ast, string); 249}; 250 251export function minify(thing) { 252 if (typeof thing === 'string') { 253 return mini(thing); 254 } 255 return strudel.reify(thing); 256} 257 258// calling this function will cause patterns to parse strings as mini notation by default 259export function miniAllStrings() { 260 strudel.setStringParser(mini); 261}