mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 06:19:33 -04:00
163 lines
4.7 KiB
TypeScript
163 lines
4.7 KiB
TypeScript
import * as krill from '../krill-parser';
|
|
import * as strudel from '../../strudel.mjs';
|
|
import { Scale, Note, Interval } from '@tonaljs/tonal';
|
|
import './tone';
|
|
import './midi';
|
|
import * as toneStuff from './tone';
|
|
import shapeshifter from './shapeshifter';
|
|
|
|
// even if some functions are not used, we need them to be available in eval
|
|
const {
|
|
pure,
|
|
stack,
|
|
slowcat,
|
|
fastcat,
|
|
cat,
|
|
sequence,
|
|
polymeter,
|
|
pm,
|
|
polyrhythm,
|
|
pr,
|
|
/* reify, */ silence,
|
|
Fraction,
|
|
timeCat,
|
|
} = strudel;
|
|
const { autofilter, filter, gain } = toneStuff;
|
|
|
|
function reify(thing: any) {
|
|
if (thing?.constructor?.name === 'Pattern') {
|
|
return thing;
|
|
}
|
|
return pure(thing);
|
|
}
|
|
|
|
function minify(thing: any) {
|
|
if (typeof thing === 'string') {
|
|
return mini(thing);
|
|
}
|
|
return reify(thing);
|
|
}
|
|
|
|
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().valueOf();
|
|
return reify(pat).fast(speed);
|
|
// 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;
|
|
};
|
|
|
|
export function patternifyAST(ast: any): any {
|
|
switch (ast.type_) {
|
|
case 'pattern':
|
|
const children = ast.source_.map(patternifyAST).map(applyOptions(ast));
|
|
if (ast.arguments_.alignment === 'v') {
|
|
return stack(...children);
|
|
}
|
|
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
|
if (weightedChildren) {
|
|
return timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
|
|
}
|
|
return sequence(...children);
|
|
case 'element':
|
|
if (ast.source_ === '~') {
|
|
return silence;
|
|
}
|
|
if (typeof ast.source_ !== 'object') {
|
|
return ast.source_;
|
|
}
|
|
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 pattern = sequence(
|
|
...strings.map((str) => {
|
|
const ast = krill.parse(`"${str}"`);
|
|
// console.log('ast', ast);
|
|
return patternifyAST(ast);
|
|
})
|
|
);
|
|
return pattern;
|
|
};
|
|
|
|
// shorthand for mini
|
|
const m = mini;
|
|
// shorthand for stack, automatically minifying strings
|
|
const s = (...strings) => {
|
|
const patternified = strings.map((s) => minify(s));
|
|
return stack(...patternified);
|
|
};
|
|
|
|
// includes haskell style (raw krill parsing)
|
|
export const h = (string: string) => {
|
|
const ast = krill.parse(string);
|
|
// console.log('ast', ast);
|
|
return patternifyAST(ast);
|
|
};
|
|
|
|
export const parse: any = (code: string) => {
|
|
let _pattern;
|
|
let mode;
|
|
try {
|
|
_pattern = h(code);
|
|
mode = 'pegjs';
|
|
} catch (err) {
|
|
// code is not haskell like
|
|
mode = 'javascript';
|
|
code = shapeshifter(code);
|
|
_pattern = eval(code);
|
|
if (_pattern?.constructor?.name !== 'Pattern') {
|
|
const message = `got "${typeof _pattern}" instead of pattern`;
|
|
throw new Error(message + (typeof _pattern === 'function' ? ', did you forget to call a function?' : '.'));
|
|
}
|
|
}
|
|
return { mode, pattern: _pattern };
|
|
};
|