@strudel/mini
Version: 
Mini notation for strudel
262 lines (247 loc) • 9.8 kB
JavaScript
/*
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/>.
*/
import * as krill from './krill-parser.js';
import * as strudel from '@strudel/core';
import Fraction, { lcm } from '@strudel/core/fraction.mjs';
const randOffset = 0.0003;
const applyOptions = (parent, enter) => (pat, i) => {
  const ast = parent.source_[i];
  const options = ast.options_;
  const ops = options?.ops;
  const steps_source = pat.__steps_source;
  if (ops) {
    for (const op of ops) {
      switch (op.type_) {
        case 'stretch': {
          const legalTypes = ['fast', 'slow'];
          const { type, amount } = op.arguments_;
          if (!legalTypes.includes(type)) {
            throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
          }
          pat = strudel.reify(pat)[type](enter(amount));
          break;
        }
        case 'replicate': {
          const { amount } = op.arguments_;
          pat = strudel.reify(pat);
          pat = pat._repeatCycles(amount)._fast(amount);
          break;
        }
        case 'bjorklund': {
          if (op.arguments_.rotation) {
            pat = pat.euclidRot(enter(op.arguments_.pulse), enter(op.arguments_.step), enter(op.arguments_.rotation));
          } else {
            pat = pat.euclid(enter(op.arguments_.pulse), enter(op.arguments_.step));
          }
          break;
        }
        case 'degradeBy': {
          pat = strudel
            .reify(pat)
            ._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5);
          break;
        }
        case 'tail': {
          const friend = enter(op.arguments_.element);
          pat = pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend);
          break;
        }
        case 'range': {
          const friend = enter(op.arguments_.element);
          pat = strudel.reify(pat);
          const arrayRange = (start, stop, step = 1) =>
            Array.from({ length: Math.abs(stop - start) / step + 1 }, (value, index) =>
              start < stop ? start + index * step : start - index * step,
            );
          let range = (apat, bpat) => apat.squeezeBind((a) => bpat.bind((b) => strudel.fastcat(...arrayRange(a, b))));
          pat = range(pat, friend);
          break;
        }
        default: {
          console.warn(`operator "${op.type_}" not implemented`);
        }
      }
    }
  }
  pat.__steps_source = pat.__steps_source || steps_source;
  return pat;
};
// expects ast from mini2ast + quoted mini string + optional callback when a node is entered
export function patternifyAST(ast, code, onEnter, offset = 0) {
  onEnter?.(ast);
  const enter = (node) => patternifyAST(node, code, onEnter, offset);
  switch (ast.type_) {
    case 'pattern': {
      // resolveReplications(ast);
      const children = ast.source_.map((child) => enter(child)).map(applyOptions(ast, enter));
      const alignment = ast.arguments_.alignment;
      const with_steps = children.filter((child) => child.__steps_source);
      let pat;
      switch (alignment) {
        case 'stack': {
          pat = strudel.stack(...children);
          if (with_steps.length) {
            pat._steps = lcm(...with_steps.map((x) => Fraction(x._steps)));
          }
          break;
        }
        case 'polymeter_slowcat': {
          pat = strudel.stack(...children.map((child) => child._slow(child.__weight)));
          if (with_steps.length) {
            pat._steps = lcm(...with_steps.map((x) => Fraction(x._steps)));
          }
          break;
        }
        case 'polymeter': {
          // polymeter
          const stepsPerCycle = ast.arguments_.stepsPerCycle
            ? enter(ast.arguments_.stepsPerCycle).fmap((x) => strudel.Fraction(x))
            : strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
          const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight))));
          pat = strudel.stack(...aligned);
          break;
        }
        case 'rand': {
          pat = strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
          if (with_steps.length) {
            pat._steps = lcm(...with_steps.map((x) => Fraction(x._steps)));
          }
          break;
        }
        case 'feet': {
          pat = strudel.fastcat(...children);
          break;
        }
        default: {
          const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
          if (weightedChildren) {
            const weightSum = ast.source_.reduce(
              (sum, child) => sum.add(child.options_?.weight || strudel.Fraction(1)),
              strudel.Fraction(0),
            );
            pat = strudel.timeCat(
              ...ast.source_.map((child, i) => [child.options_?.weight || strudel.Fraction(1), children[i]]),
            );
            pat.__weight = weightSum; // for polymeter
            pat._steps = weightSum;
            if (with_steps.length) {
              pat._steps = pat._steps.mul(lcm(...with_steps.map((x) => Fraction(x._steps))));
            }
          } else {
            pat = strudel.sequence(...children);
            pat._steps = children.length;
          }
          if (ast.arguments_._steps) {
            pat.__steps_source = true;
          }
        }
      }
      if (with_steps.length) {
        pat.__steps_source = true;
      }
      return pat;
    }
    case 'element': {
      1;
      return enter(ast.source_);
    }
    case 'atom': {
      if (ast.source_ === '~' || ast.source_ === '-') {
        return strudel.silence;
      }
      if (!ast.location_) {
        console.warn('no location for', ast);
        return ast.source_;
      }
      const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
      if (offset === -1) {
        // skip location handling (used when getting leaves to avoid confusion)
        return strudel.pure(value);
      }
      const [from, to] = getLeafLocation(code, ast, offset);
      return strudel.pure(value).withLoc(from, to);
    }
    case 'stretch':
      return enter(ast.source_).slow(enter(ast.arguments_.amount));
    default:
      console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
      return strudel.silence;
  }
}
// takes quoted mini string + leaf node within, returns source location of node (whitespace corrected)
export const getLeafLocation = (code, leaf, globalOffset = 0) => {
  // value is expected without quotes!
  const { start, end } = leaf.location_;
  const actual = code?.split('').slice(start.offset, end.offset).join('');
  // make sure whitespaces are not part of the highlight
  const [offsetStart = 0, offsetEnd = 0] = actual
    ? actual.split(leaf.source_).map((p) => p.split('').filter((c) => c === ' ').length)
    : [];
  return [start.offset + offsetStart + globalOffset, end.offset - offsetEnd + globalOffset];
};
// takes quoted mini string, returns ast
export const mini2ast = (code, start = 0, userCode = code) => {
  try {
    return krill.parse(code);
  } catch (error) {
    const region = [error.location.start.offset + start, error.location.end.offset + start];
    const line = userCode.slice(0, region[0]).split('\n').length;
    throw new Error(`[mini] parse error at line ${line}: ${error.message}`);
  }
};
// takes quoted mini string, returns all nodes that are leaves
export const getLeaves = (code, start, userCode) => {
  const ast = mini2ast(code, start, userCode);
  let leaves = [];
  patternifyAST(
    ast,
    code,
    (node) => {
      if (node.type_ === 'atom') {
        leaves.push(node);
      }
    },
    -1,
  );
  return leaves;
};
// takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes
export const getLeafLocations = (code, start = 0, userCode) => {
  return getLeaves(code, start, userCode).map((l) => getLeafLocation(code, l, start));
};
// mini notation only (wraps in "")
export const mini = (...strings) => {
  const pats = strings.map((str) => {
    const code = `"${str}"`;
    const ast = mini2ast(code);
    return patternifyAST(ast, code);
  });
  return strudel.sequence(...pats);
};
// 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
export const m = (str, offset) => {
  const code = `"${str}"`;
  const ast = mini2ast(code);
  return patternifyAST(ast, code, null, offset);
};
// includes haskell style (raw krill parsing)
export const h = (string) => {
  const ast = mini2ast(string);
  return patternifyAST(ast, string);
};
export function minify(thing) {
  if (typeof thing === 'string') {
    return mini(thing);
  }
  return strudel.reify(thing);
}
// calling this function will cause patterns to parse strings as mini notation by default
export function miniAllStrings() {
  strudel.setStringParser(mini);
}