mini.mjsannotatedmini.mjssource261 lines · 9.6 KB · raw
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}