mini.mjsannotatedmini.mjssource261 lines · 9.6 KB · raw

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/.

7import * as krill from './krill-parser.js';
8import * as strudel from '@strudel/core';
9import Fraction, { lcm } from '@strudel/core/fraction.mjs';
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

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};

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};

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};

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

239export const m = (str, offset) => {
240  const code = `"${str}"`;
241  const ast = mini2ast(code);
242  return patternifyAST(ast, code, null, offset);
243};

includes haskell style (raw krill parsing)

246export const h = (string) => {
247  const ast = mini2ast(string);
248  return patternifyAST(ast, string);
249};
251export function minify(thing) {
252  if (typeof thing === 'string') {
253    return mini(thing);
254  }
255  return strudel.reify(thing);
256}

calling this function will cause patterns to parse strings as mini notation by default

259export function miniAllStrings() {
260  strudel.setStringParser(mini);
261}