import * as krill from '../krill-parser'; import * as strudel from '../../strudel.mjs'; import { Scale, Note, Interval } from '@tonaljs/tonal'; import { addMiniLocations } from './shapeshifter'; const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence } = strudel; const applyOptions = (parent: any) => (pat: any, i: number) => { const ast = parent.source_[i]; const options = ast.options_; const operator = options?.operator; if (operator) { switch (operator.type_) { case 'stretch': const speed = new Fraction(operator.arguments_.amount).inverse(); return reify(pat).fast(speed); case 'bjorklund': return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation); // TODO: case 'fixed-step': "%" } console.warn(`operator "${operator.type_}" not implemented`); } if (options?.weight) { // weight is handled by parent return pat; } // TODO: bjorklund e.g. "c3(5,8)" const unimplemented = Object.keys(options || {}).filter((key) => key !== 'operator'); if (unimplemented.length) { console.warn( `option${unimplemented.length > 1 ? 's' : ''} ${unimplemented.map((o) => `"${o}"`).join(', ')} not implemented` ); } return pat; }; function resolveReplications(ast) { // the general idea here: x!3 = [x*3]@3 // could this be made easier?! ast.source_ = ast.source_.map((child) => { const { replicate, ...options } = child.options_ || {}; if (replicate) { return { ...child, options_: { ...options, weight: replicate }, source_: { type_: 'pattern', arguments_: { alignment: 'h', }, source_: [ { type_: 'element', source_: child.source_, location_: child.location_, options_: { operator: { type_: 'stretch', // valueOf should be ok here, as we turn it into a string // fraction.js doc: "If you want to keep the number as it is, convert it to a string, as the string parser will not perform any further observations" arguments_: { amount: String(new Fraction(replicate).inverse().valueOf()) }, }, }, }, ], }, }; } return child; }); } export function patternifyAST(ast: any): any { switch (ast.type_) { case 'pattern': resolveReplications(ast); const children = ast.source_.map(patternifyAST).map(applyOptions(ast)); const alignment = ast.arguments_.alignment; if (alignment === 'v') { return stack(...children); } const weightedChildren = ast.source_.some((child) => !!child.options_?.weight); if (!weightedChildren && alignment === 't') { return slowcat(...children); } if (weightedChildren) { const pat = timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]])); if (alignment === 't') { const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0); return pat._slow(weightSum); // timecat + slow } return pat; } return sequence(...children); case 'element': if (ast.source_ === '~') { return silence; } if (typeof ast.source_ !== 'object') { if (!addMiniLocations) { return ast.source_; } if (!ast.location_) { console.warn('no location for', ast); return ast.source_; } const { start, end } = ast.location_; // should we really parse as number here? strings wont be fareyed... const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_; // return ast.source_; // the following line expects the shapeshifter to wrap this in withLocationOffset // because location_ is only relative to the mini string, but we need it relative to whole code return pure(value).withLocation({ start, end }); } return patternifyAST(ast.source_); case 'stretch': return patternifyAST(ast.source_).slow(ast.arguments_.amount); case 'scale': let [tonic, scale] = Scale.tokenize(ast.arguments_.scale); const intervals = Scale.get(scale).intervals; const pattern = patternifyAST(ast.source_); tonic = tonic || 'C4'; // console.log('scale', ast, pattern, tonic, scale); console.log('tonic', tonic); return pattern.fmap((step: any) => { step = Number(step); if (isNaN(step)) { console.warn(`scale step "${step}" not a number`); return step; } const octaves = Math.floor(step / intervals.length); const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m); const index = mod(step, intervals.length); // % with negative numbers. e.g. -1 % 3 = 2 const interval = Interval.add(intervals[index], Interval.fromSemitones(octaves * 12)); return Note.transpose(tonic, interval || '1P'); }); /* case 'struct': // TODO: return silence; */ default: console.warn(`node type "${ast.type_}" not implemented -> returning silence`); return silence; } } // mini notation only (wraps in "") export const mini = (...strings: string[]) => { const pats = strings.map((str) => { const ast = krill.parse(`"${str}"`); return patternifyAST(ast); }); return sequence(...pats); }; // includes haskell style (raw krill parsing) export const h = (string: string) => { const ast = krill.parse(string); // console.log('ast', ast); return patternifyAST(ast); }; // shorthand for mini Pattern.prototype.define('mini', mini, { composable: true }); Pattern.prototype.define('m', mini, { composable: true }); Pattern.prototype.define('h', h, { composable: true }); // TODO: move this to strudel? export function reify(thing: any) { if (thing?.constructor?.name === 'Pattern') { return thing; } return pure(thing); } export function minify(thing: any) { if (typeof thing === 'string') { return mini(thing); } return reify(thing); }