mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 21:58:31 +00:00
Merge branch 'main' into stateful-events
This commit is contained in:
commit
becc73e58a
@ -1936,6 +1936,7 @@ function peg$parse(input, options) {
|
||||
this.type_ = "element";
|
||||
this.source_ = source;
|
||||
this.options_ = options;
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var CommandStub = function(name, options)
|
||||
|
||||
@ -175,6 +175,13 @@ class Pattern {
|
||||
_withEvents(func) {
|
||||
return new Pattern((span) => func(this.query(span)));
|
||||
}
|
||||
withLocation(location) {
|
||||
return this.fmap((value) => {
|
||||
value = typeof value === "object" && !Array.isArray(value) ? value : {value};
|
||||
const locations = (value.locations || []).concat([location]);
|
||||
return {...value, locations};
|
||||
});
|
||||
}
|
||||
withValue(func) {
|
||||
return new Pattern((span) => this.query(span).map((hap) => hap.withValue(func)));
|
||||
}
|
||||
@ -312,6 +319,7 @@ class Pattern {
|
||||
_patternify(func) {
|
||||
const pat = this;
|
||||
const patterned = function(...args) {
|
||||
args = args.map((arg) => arg.constructor?.name === "Pattern" ? arg.fmap((value) => value.value || value) : arg);
|
||||
const pat_arg = sequence(...args);
|
||||
return pat_arg.fmap((arg) => func.call(pat, arg)).outerJoin();
|
||||
};
|
||||
@ -596,6 +604,28 @@ Pattern.prototype.bootstrap = () => {
|
||||
}));
|
||||
return bootstrapped;
|
||||
};
|
||||
function withLocationOffset(pat, offset) {
|
||||
return pat.fmap((value) => {
|
||||
value = typeof value === "object" && !Array.isArray(value) ? value : {value};
|
||||
let locations = value.locations || [];
|
||||
locations = locations.map(({start, end}) => {
|
||||
const colOffset = start.line === 1 ? offset.start.column : 0;
|
||||
return {
|
||||
start: {
|
||||
...start,
|
||||
line: start.line - 1 + (offset.start.line - 1) + 1,
|
||||
column: start.column - 1 + colOffset
|
||||
},
|
||||
end: {
|
||||
...end,
|
||||
line: end.line - 1 + (offset.start.line - 1) + 1,
|
||||
column: end.column - 1 + colOffset
|
||||
}
|
||||
};
|
||||
});
|
||||
return {...value, locations};
|
||||
});
|
||||
}
|
||||
export {
|
||||
Fraction,
|
||||
TimeSpan,
|
||||
@ -633,5 +663,6 @@ export {
|
||||
struct,
|
||||
mask,
|
||||
invert,
|
||||
inv
|
||||
inv,
|
||||
withLocationOffset
|
||||
};
|
||||
|
||||
20
docs/dist/App.js
vendored
20
docs/dist/App.js
vendored
@ -1,4 +1,4 @@
|
||||
import React, {useCallback, useLayoutEffect, useRef} from "../_snowpack/pkg/react.js";
|
||||
import React, {useCallback, useLayoutEffect, useMemo, useRef, useState} from "../_snowpack/pkg/react.js";
|
||||
import * as Tone from "../_snowpack/pkg/tone.js";
|
||||
import CodeMirror from "./CodeMirror.js";
|
||||
import cx from "./cx.js";
|
||||
@ -28,9 +28,21 @@ function getRandomTune() {
|
||||
}
|
||||
const randomTune = getRandomTune();
|
||||
function App() {
|
||||
const [editor, setEditor] = useState();
|
||||
const doc = useMemo(() => editor?.getDoc(), [editor]);
|
||||
const {setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog} = useRepl({
|
||||
tune: decoded || randomTune,
|
||||
defaultSynth
|
||||
defaultSynth,
|
||||
onEvent: useCallback((event) => {
|
||||
const locs = event.value.locations;
|
||||
if (!locs) {
|
||||
return;
|
||||
}
|
||||
const marks = locs.map(({start, end}) => doc.markText({line: start.line - 1, ch: start.column}, {line: end.line - 1, ch: end.column}, {css: "background-color: gray;"}));
|
||||
setTimeout(() => {
|
||||
marks.forEach((mark) => mark.clear());
|
||||
}, event.duration * 0.9 * 1e3);
|
||||
}, [doc])
|
||||
});
|
||||
const logBox = useRef();
|
||||
useLayoutEffect(() => {
|
||||
@ -95,10 +107,12 @@ function App() {
|
||||
className: cx("h-full bg-[#2A3236]", error ? "focus:ring-red-500" : "focus:ring-slate-800")
|
||||
}, /* @__PURE__ */ React.createElement(CodeMirror, {
|
||||
value: code,
|
||||
editorDidMount: setEditor,
|
||||
options: {
|
||||
mode: "javascript",
|
||||
theme: "material",
|
||||
lineNumbers: true
|
||||
lineNumbers: true,
|
||||
styleSelectedText: true
|
||||
},
|
||||
onChange: (_2, __, value) => setCode(value)
|
||||
}), /* @__PURE__ */ React.createElement("span", {
|
||||
|
||||
8
docs/dist/CodeMirror.js
vendored
8
docs/dist/CodeMirror.js
vendored
@ -4,15 +4,17 @@ import "../_snowpack/pkg/codemirror/mode/javascript/javascript.js";
|
||||
import "../_snowpack/pkg/codemirror/mode/pegjs/pegjs.js";
|
||||
import "../_snowpack/pkg/codemirror/theme/material.css.proxy.js";
|
||||
import "../_snowpack/pkg/codemirror/lib/codemirror.css.proxy.js";
|
||||
export default function CodeMirror({value, onChange, options}) {
|
||||
export default function CodeMirror({value, onChange, options, editorDidMount}) {
|
||||
options = options || {
|
||||
mode: "javascript",
|
||||
theme: "material",
|
||||
lineNumbers: true
|
||||
lineNumbers: true,
|
||||
styleSelectedText: true
|
||||
};
|
||||
return /* @__PURE__ */ React.createElement(CodeMirror2, {
|
||||
value,
|
||||
options,
|
||||
onBeforeChange: onChange
|
||||
onBeforeChange: onChange,
|
||||
editorDidMount
|
||||
});
|
||||
}
|
||||
|
||||
12
docs/dist/parse.js
vendored
12
docs/dist/parse.js
vendored
@ -1,6 +1,7 @@
|
||||
import * as krill from "../_snowpack/link/repl/krill-parser.js";
|
||||
import * as strudel from "../_snowpack/link/strudel.js";
|
||||
import {Scale, Note, Interval} from "../_snowpack/pkg/@tonaljs/tonal.js";
|
||||
import {addMiniLocations} from "./shapeshifter.js";
|
||||
const {pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence} = strudel;
|
||||
const applyOptions = (parent) => (pat, i) => {
|
||||
const ast = parent.source_[i];
|
||||
@ -39,6 +40,7 @@ function resolveReplications(ast) {
|
||||
{
|
||||
type_: "element",
|
||||
source_: child.source_,
|
||||
location_: child.location_,
|
||||
options_: {
|
||||
operator: {
|
||||
type_: "stretch",
|
||||
@ -80,7 +82,15 @@ export function patternifyAST(ast) {
|
||||
return silence;
|
||||
}
|
||||
if (typeof ast.source_ !== "object") {
|
||||
return ast.source_;
|
||||
if (!addMiniLocations) {
|
||||
return ast.source_;
|
||||
}
|
||||
if (!ast.location_) {
|
||||
console.warn("no location for", ast);
|
||||
return ast.source_;
|
||||
}
|
||||
const {start, end} = ast.location_;
|
||||
return pure(ast.source_).withLocation({start, end});
|
||||
}
|
||||
return patternifyAST(ast.source_);
|
||||
case "stretch":
|
||||
|
||||
248
docs/dist/shapeshifter.js
vendored
248
docs/dist/shapeshifter.js
vendored
@ -1,30 +1,266 @@
|
||||
import { parseScript } from './shift-parser/index.js'; // npm module does not work in the browser
|
||||
import { parseScriptWithLocation } from './shift-parser/index.js'; // npm module does not work in the browser
|
||||
import traverser from './shift-traverser/index.js'; // npm module does not work in the browser
|
||||
const { replace } = traverser;
|
||||
import { LiteralStringExpression, IdentifierExpression } from '../_snowpack/pkg/shift-ast.js';
|
||||
import { LiteralStringExpression, IdentifierExpression, CallExpression, StaticMemberExpression } from '../_snowpack/pkg/shift-ast.js';
|
||||
import codegen from '../_snowpack/pkg/shift-codegen.js';
|
||||
import * as strudel from '../_snowpack/link/strudel.js';
|
||||
|
||||
const { Pattern } = strudel;
|
||||
|
||||
const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
|
||||
|
||||
const addLocations = true;
|
||||
export const addMiniLocations = true;
|
||||
|
||||
export default (code) => {
|
||||
const ast = parseScript(code);
|
||||
const shifted = replace(ast, {
|
||||
const ast = parseScriptWithLocation(code);
|
||||
const nodesWithLocation = [];
|
||||
const parents = [];
|
||||
const shifted = replace(ast.tree, {
|
||||
enter(node, parent) {
|
||||
// replace identifiers that are a note with a note string
|
||||
parents.push(parent);
|
||||
const isSynthetic = parents.some((p) => nodesWithLocation.includes(p));
|
||||
if (isSynthetic) {
|
||||
return node;
|
||||
}
|
||||
const grandparent = parents[parents.length - 2];
|
||||
const isTimeCat = parent?.type === 'ArrayExpression' && isPatternFactory(grandparent);
|
||||
const isMarkable = isPatternFactory(parent) || isTimeCat;
|
||||
// operator overloading => still not done
|
||||
const operators = {
|
||||
'*': 'fast',
|
||||
'/': 'slow',
|
||||
'&': 'stack',
|
||||
'&&': 'append',
|
||||
};
|
||||
if (
|
||||
node.type === 'BinaryExpression' &&
|
||||
operators[node.operator] &&
|
||||
['LiteralNumericExpression', 'LiteralStringExpression', 'IdentifierExpression'].includes(node.right?.type) &&
|
||||
canBeOverloaded(node.left)
|
||||
) {
|
||||
let arg = node.left;
|
||||
if (node.left.type === 'IdentifierExpression') {
|
||||
arg = wrapReify(node.left);
|
||||
}
|
||||
return new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
property: operators[node.operator],
|
||||
object: wrapReify(arg),
|
||||
}),
|
||||
arguments: [node.right],
|
||||
});
|
||||
}
|
||||
// replace pseudo note variables
|
||||
if (node.type === 'IdentifierExpression') {
|
||||
if (isNote(node.name)) {
|
||||
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
|
||||
if (addLocations && isMarkable) {
|
||||
return reifyWithLocation(value, node, ast.locations, nodesWithLocation);
|
||||
}
|
||||
return new LiteralStringExpression({ value });
|
||||
}
|
||||
if (node.name === 'r') {
|
||||
return new IdentifierExpression({ name: 'silence' });
|
||||
}
|
||||
}
|
||||
if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) {
|
||||
// console.log('add', node);
|
||||
return reifyWithLocation(node.value, node, ast.locations, nodesWithLocation);
|
||||
}
|
||||
if (!addMiniLocations) {
|
||||
return node;
|
||||
}
|
||||
// mini notation location handling
|
||||
const miniFunctions = ['mini', 'm'];
|
||||
const isAlreadyWrapped = parent?.type === 'CallExpression' && parent.callee.name === 'withLocationOffset';
|
||||
if (node.type === 'CallExpression' && miniFunctions.includes(node.callee.name) && !isAlreadyWrapped) {
|
||||
// mini('c3')
|
||||
if (node.arguments.length > 1) {
|
||||
// TODO: transform mini(...args) to cat(...args.map(mini)) ?
|
||||
console.warn('multi arg mini locations not supported yet...');
|
||||
return node;
|
||||
}
|
||||
return wrapLocationOffset(node, node.arguments, ast.locations, nodesWithLocation);
|
||||
}
|
||||
if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property) && !isAlreadyWrapped) {
|
||||
// 'c3'.mini or 'c3'.m
|
||||
return wrapLocationOffset(node, node.object, ast.locations, nodesWithLocation);
|
||||
}
|
||||
return node;
|
||||
},
|
||||
leave() {
|
||||
parents.pop();
|
||||
},
|
||||
});
|
||||
return codegen(shifted);
|
||||
};
|
||||
|
||||
function wrapReify(node) {
|
||||
return new CallExpression({
|
||||
callee: new IdentifierExpression({
|
||||
name: 'reify',
|
||||
}),
|
||||
arguments: [node],
|
||||
});
|
||||
}
|
||||
|
||||
function isPatternFactory(node) {
|
||||
return node?.type === 'CallExpression' && Object.keys(Pattern.prototype.factories).includes(node.callee.name);
|
||||
}
|
||||
|
||||
function canBeOverloaded(node) {
|
||||
return (node.type === 'IdentifierExpression' && isNote(node.name)) || isPatternFactory(node);
|
||||
// TODO: support sequence(c3).transpose(3).x.y.z
|
||||
}
|
||||
|
||||
// turn node into withLocationOffset(node, location)
|
||||
function wrapLocationOffset(node, stringNode, locations, nodesWithLocation) {
|
||||
// console.log('wrapppp', stringNode);
|
||||
const expression = {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'IdentifierExpression',
|
||||
name: 'withLocationOffset',
|
||||
},
|
||||
arguments: [node, getLocationObject(stringNode, locations)],
|
||||
};
|
||||
nodesWithLocation.push(expression);
|
||||
// console.log('wrapped', codegen(expression));
|
||||
return expression;
|
||||
}
|
||||
|
||||
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
|
||||
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
|
||||
function reifyWithLocation(value, node, locations, nodesWithLocation) {
|
||||
// console.log('reifyWithLocation', value, node);
|
||||
const withLocation = new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
object: new CallExpression({
|
||||
callee: new IdentifierExpression({
|
||||
name: 'reify',
|
||||
}),
|
||||
arguments: [new LiteralStringExpression({ value })],
|
||||
}),
|
||||
property: 'withLocation',
|
||||
}),
|
||||
arguments: [getLocationObject(node, locations)],
|
||||
});
|
||||
nodesWithLocation.push(withLocation);
|
||||
return withLocation;
|
||||
}
|
||||
|
||||
// returns ast for source location object
|
||||
function getLocationObject(node, locations) {
|
||||
/*const locationAST = parseScript(
|
||||
"x=" + JSON.stringify(ast.locations.get(node))
|
||||
).statements[0].expression.expression;
|
||||
|
||||
console.log("locationAST", locationAST);*/
|
||||
|
||||
/*const callAST = parseScript(
|
||||
`reify(${node.name}).withLocation(${JSON.stringify(
|
||||
ast.locations.get(node)
|
||||
)})`
|
||||
).statements[0].expression;*/
|
||||
const loc = locations.get(node);
|
||||
return {
|
||||
type: 'ObjectExpression',
|
||||
properties: [
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'start',
|
||||
},
|
||||
expression: {
|
||||
type: 'ObjectExpression',
|
||||
properties: [
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'line',
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.start.line,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'column',
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.start.column,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'offset',
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.start.offset,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'end',
|
||||
},
|
||||
expression: {
|
||||
type: 'ObjectExpression',
|
||||
properties: [
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'line',
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.end.line,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'column',
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.end.column,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'offset',
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.end.offset,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: turn x.groove['[~ x]*2'] into x.groove('[~ x]*2'.m)
|
||||
// and ['c1*2'].xx into 'c1*2'.m.xx ??
|
||||
// or just all templated strings?? x.groove(`[~ x]*2`)
|
||||
// or just all templated strings?? x.groove(`[~ x]*2`)
|
||||
|
||||
11
docs/dist/tonal.js
vendored
11
docs/dist/tonal.js
vendored
@ -2,7 +2,7 @@ import {Note, Interval, Scale} from "../_snowpack/pkg/@tonaljs/tonal.js";
|
||||
import {Pattern as _Pattern} from "../_snowpack/link/strudel.js";
|
||||
const Pattern = _Pattern;
|
||||
function toNoteEvent(event) {
|
||||
if (typeof event === "string") {
|
||||
if (typeof event === "string" || typeof event === "number") {
|
||||
return {value: event};
|
||||
}
|
||||
if (event.value) {
|
||||
@ -53,13 +53,20 @@ Pattern.prototype._mapNotes = function(func) {
|
||||
Pattern.prototype._transpose = function(intervalOrSemitones) {
|
||||
return this._mapNotes(({value, scale}) => {
|
||||
const interval = !isNaN(Number(intervalOrSemitones)) ? Interval.fromSemitones(intervalOrSemitones) : String(intervalOrSemitones);
|
||||
if (typeof value === "number") {
|
||||
const semitones = typeof interval === "string" ? Interval.semitones(interval) || 0 : interval;
|
||||
return {value: value + semitones};
|
||||
}
|
||||
return {value: Note.transpose(value, interval), scale};
|
||||
});
|
||||
};
|
||||
Pattern.prototype._scaleTranspose = function(offset) {
|
||||
return this._mapNotes(({value, scale}) => {
|
||||
if (!scale) {
|
||||
throw new Error("can only use scaleOffset after .scale");
|
||||
throw new Error("can only use scaleTranspose after .scale");
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
throw new Error("can only use scaleTranspose with notes");
|
||||
}
|
||||
return {value: scaleTranspose(scale, Number(offset), value), scale};
|
||||
});
|
||||
|
||||
73
docs/dist/tunes.js
vendored
73
docs/dist/tunes.js
vendored
@ -4,11 +4,11 @@ export const timeCatMini = `stack(
|
||||
'[eb4@5 [f4 eb4 d4]@3] [eb4 c4]/2'.mini.slow(8)
|
||||
)`;
|
||||
export const timeCat = `stack(
|
||||
timeCat([3, c3], [1, stack(eb3, g3, m(c4, d4).slow(2))]),
|
||||
m(c2, g2),
|
||||
timeCat([3, c3], [1, stack(eb3, g3, cat(c4, d4).slow(2))]),
|
||||
cat(c2, g2),
|
||||
sequence(
|
||||
timeCat([5, eb4], [3, m(f4, eb4, d4)]),
|
||||
m(eb4, c4).slow(2)
|
||||
timeCat([5, eb4], [3, cat(f4, eb4, d4)]),
|
||||
cat(eb4, c4).slow(2)
|
||||
).slow(4)
|
||||
)`;
|
||||
export const shapeShifted = `stack(
|
||||
@ -120,11 +120,11 @@ export const spanish = `slowcat(
|
||||
stack(ab3,c4,eb4),
|
||||
stack(g3,b3,d4)
|
||||
)`;
|
||||
export const whirlyStrudel = `mini("[e4 [b2 b3] c4]")
|
||||
export const whirlyStrudel = `sequence(e4, [b2, b3], c4)
|
||||
.every(4, fast(2))
|
||||
.every(3, slow(1.5))
|
||||
.fast(slowcat(1.25, 1, 1.5))
|
||||
.every(2, _ => mini("e4 ~ e3 d4 ~"))`;
|
||||
.every(2, _ => sequence(e4, r, e3, d4, r))`;
|
||||
export const swimming = `stack(
|
||||
mini(
|
||||
'~',
|
||||
@ -357,64 +357,3 @@ export const caverave = `() => {
|
||||
synths
|
||||
).slow(2);
|
||||
}`;
|
||||
export const caveravefuture = `() => {
|
||||
const delay = new FeedbackDelay(1/8, .4).chain(vol(0.5), out);
|
||||
const kick = new MembraneSynth().chain(vol(.8), out);
|
||||
const snare = new NoiseSynth().chain(vol(.8), out);
|
||||
const hihat = new MetalSynth().set(adsr(0, .08, 0, .1)).chain(vol(.3).connect(delay),out);
|
||||
const bass = new Synth().set({ ...osc('sawtooth'), ...adsr(0, .1, .4) }).chain(lowpass(900), vol(.5), out);
|
||||
const keys = new PolySynth().set({ ...osc('sawtooth'), ...adsr(0, .5, .2, .7) }).chain(lowpass(1200), vol(.5), out);
|
||||
|
||||
const drums = stack(
|
||||
\`c1*2\`.tone(kick).bypass(\`<0@7 1>/8\`),
|
||||
\`~ <x!7 [x@3 x]>\`.tone(snare).bypass(\`<0@7 1>/4\`),
|
||||
\`[~ c4]*2\`.tone(hihat)
|
||||
);
|
||||
|
||||
const thru = (x) => x.transpose(\`<0 1>/8\`).transpose(-1);
|
||||
const synths = stack(
|
||||
\`<eb4 d4 c4 b3>/2\`.scale(timeCat([3,'C minor'],[1,'C melodic minor']).slow(8)).groove(\`[~ x]*2\`)
|
||||
.edit(
|
||||
scaleTranspose(0).early(0),
|
||||
scaleTranspose(2).early(1/8),
|
||||
scaleTranspose(7).early(1/4),
|
||||
scaleTranspose(8).early(3/8)
|
||||
).edit(thru).tone(keys).bypass(\`<1 0>/16\`),
|
||||
\`<C2 Bb1 Ab1 [G1 [G2 G1]]>/2\`.groove(\`x [~ x] <[~ [~ x]]!3 [x x]>@2\`).edit(thru).tone(bass),
|
||||
\`<Cm7 Bb7 Fm7 G7b13>/2\`.groove(\`~ [x@0.5 ~]\`.fast(2)).voicings().edit(thru).every(2, early(1/8)).tone(keys).bypass(\`<0@7 1>/8\`.early(1/4)),
|
||||
)
|
||||
return stack(
|
||||
drums.fast(2),
|
||||
synths
|
||||
).slow(2);
|
||||
}`;
|
||||
export const caveravefuture2 = `const delay = new FeedbackDelay(1/8, .4).chain(vol(0.5), out);
|
||||
const kick = new MembraneSynth().chain(vol(.8), out);
|
||||
const snare = new NoiseSynth().chain(vol(.8), out);
|
||||
const hihat = new MetalSynth().set(adsr(0, .08, 0, .1)).chain(vol(.3).connect(delay),out);
|
||||
const bass = new Synth().set({ ...osc('sawtooth'), ...adsr(0, .1, .4) }).chain(lowpass(900), vol(.5), out);
|
||||
const keys = new PolySynth().set({ ...osc('sawtooth'), ...adsr(0, .5, .2, .7) }).chain(lowpass(1200), vol(.5), out);
|
||||
|
||||
const drums = stack(
|
||||
"c1*2".tone(kick).bypass("<0@7 1>/8"),
|
||||
"~ <x!7 [x@3 x]>".tone(snare).bypass("<0@7 1>/4"),
|
||||
"[~ c4]*2".tone(hihat)
|
||||
);
|
||||
|
||||
const thru = (x) => x.transpose("<0 1>/8").transpose(-1);
|
||||
const synths = stack(
|
||||
"<eb4 d4 c4 b3>/2".scale(timeCat([3, 'C minor'], [1, 'C melodic minor']).slow(8)).groove("[~ x]*2")
|
||||
.edit(
|
||||
scaleTranspose(0).early(0),
|
||||
scaleTranspose(2).early(1/8),
|
||||
scaleTranspose(7).early(1/4),
|
||||
scaleTranspose(8).early(3/8)
|
||||
).edit(thru).tone(keys).bypass("<1 0>/16"),
|
||||
"<C2 Bb1 Ab1 [G1 [G2 G1]]>/2".groove("x [~ x] <[~ [~ x]]!3 [x x]>@2").edit(thru).tone(bass),
|
||||
"<Cm7 Bb7 Fm7 G7b13>/2".groove("~ [x@0.5 ~]".fast(2)).voicings().edit(thru).every(2, early(1/8)).tone(keys).bypass("<0@7 1>/8".early(1/4)),
|
||||
)
|
||||
$: stack(
|
||||
drums.fast(2),
|
||||
synths
|
||||
).slow(2);
|
||||
`;
|
||||
|
||||
8
docs/dist/useRepl.js
vendored
8
docs/dist/useRepl.js
vendored
@ -6,7 +6,7 @@ import usePostMessage from "./usePostMessage.js";
|
||||
let s4 = () => {
|
||||
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
|
||||
};
|
||||
function useRepl({tune, defaultSynth, autolink = true}) {
|
||||
function useRepl({tune, defaultSynth, autolink = true, onEvent}) {
|
||||
const id = useMemo(() => s4(), []);
|
||||
const [code, setCode] = useState(tune);
|
||||
const [activeCode, setActiveCode] = useState();
|
||||
@ -30,6 +30,8 @@ function useRepl({tune, defaultSynth, autolink = true}) {
|
||||
setError(void 0);
|
||||
setActiveCode(_code);
|
||||
} catch (err) {
|
||||
err.message = "evaluation error: " + err.message;
|
||||
console.warn(err);
|
||||
setError(err);
|
||||
}
|
||||
};
|
||||
@ -43,6 +45,7 @@ function useRepl({tune, defaultSynth, autolink = true}) {
|
||||
const cycle = useCycle({
|
||||
onEvent: useCallback((time, event) => {
|
||||
try {
|
||||
onEvent?.(event);
|
||||
if (!event.value?.onTrigger) {
|
||||
const note = event.value?.value || event.value;
|
||||
if (!isNote(note)) {
|
||||
@ -62,11 +65,12 @@ function useRepl({tune, defaultSynth, autolink = true}) {
|
||||
err.message = "unplayable event: " + err?.message;
|
||||
pushLog(err.message);
|
||||
}
|
||||
}, []),
|
||||
}, [onEvent]),
|
||||
onQuery: useCallback((span) => {
|
||||
try {
|
||||
return pattern?.query(span) || [];
|
||||
} catch (err) {
|
||||
err.message = "query error: " + err.message;
|
||||
setError(err);
|
||||
return [];
|
||||
}
|
||||
|
||||
2
docs/dist/voicings.js
vendored
2
docs/dist/voicings.js
vendored
@ -18,7 +18,7 @@ Pattern.prototype.voicings = function(range) {
|
||||
range = ["F3", "A4"];
|
||||
}
|
||||
return this.fmapNested((event) => {
|
||||
lastVoicing = getVoicing(event.value, lastVoicing, range);
|
||||
lastVoicing = getVoicing(event.value?.value || event.value, lastVoicing, range);
|
||||
return stack(...lastVoicing);
|
||||
});
|
||||
};
|
||||
|
||||
@ -41619,7 +41619,7 @@ var _usePostMessageDefault = parcelHelpers.interopDefault(_usePostMessage);
|
||||
let s4 = ()=>{
|
||||
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
|
||||
};
|
||||
function useRepl({ tune , defaultSynth , autolink =true }) {
|
||||
function useRepl({ tune , defaultSynth , autolink =true , onEvent }) {
|
||||
const id = _react.useMemo(()=>s4()
|
||||
, []);
|
||||
const [code, setCode] = _react.useState(tune);
|
||||
@ -41646,6 +41646,8 @@ function useRepl({ tune , defaultSynth , autolink =true }) {
|
||||
setError(undefined);
|
||||
setActiveCode(_code);
|
||||
} catch (err) {
|
||||
err.message = 'evaluation error: ' + err.message;
|
||||
console.warn(err);
|
||||
setError(err);
|
||||
}
|
||||
};
|
||||
@ -41661,6 +41663,7 @@ function useRepl({ tune , defaultSynth , autolink =true }) {
|
||||
const cycle1 = _useCycleDefault.default({
|
||||
onEvent: _react.useCallback((time, event)=>{
|
||||
try {
|
||||
onEvent?.(event);
|
||||
if (!event.value?.onTrigger) {
|
||||
const note = event.value?.value || event.value;
|
||||
if (!_tone.isNote(note)) throw new Error('not a note: ' + note);
|
||||
@ -41676,11 +41679,14 @@ function useRepl({ tune , defaultSynth , autolink =true }) {
|
||||
err.message = 'unplayable event: ' + err?.message;
|
||||
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
|
||||
}
|
||||
}, []),
|
||||
}, [
|
||||
onEvent
|
||||
]),
|
||||
onQuery: _react.useCallback((span)=>{
|
||||
try {
|
||||
return pattern?.query(span) || [];
|
||||
} catch (err) {
|
||||
err.message = 'query error: ' + err.message;
|
||||
setError(err);
|
||||
return [];
|
||||
}
|
||||
@ -41791,6 +41797,7 @@ hackLiteral(String, [
|
||||
Object.assign(globalThis, bootstrapped, _tone1, _tone);
|
||||
const evaluate = (code)=>{
|
||||
const shapeshifted = _shapeshifterDefault.default(code); // transform syntactically correct js code to semantically usable code
|
||||
// console.log('shapeshifted', shapeshifted);
|
||||
let evaluated = eval(shapeshifted);
|
||||
if (typeof evaluated === 'function') evaluated = evaluated();
|
||||
const pattern = _parse.minify(evaluated); // eval and minify (if user entered a string)
|
||||
@ -41887,6 +41894,8 @@ parcelHelpers.export(exports, "invert", ()=>invert
|
||||
);
|
||||
parcelHelpers.export(exports, "inv", ()=>inv
|
||||
);
|
||||
parcelHelpers.export(exports, "withLocationOffset", ()=>withLocationOffset
|
||||
);
|
||||
var _fractionJs = require("fraction.js");
|
||||
var _fractionJsDefault = parcelHelpers.interopDefault(_fractionJs);
|
||||
var _ramda = require("ramda"); // will remove this as soon as compose is implemented here
|
||||
@ -42107,6 +42116,20 @@ class Pattern {
|
||||
return new Pattern((span)=>func(this.query(span))
|
||||
);
|
||||
}
|
||||
withLocation(location) {
|
||||
return this.fmap((value)=>{
|
||||
value = typeof value === 'object' && !Array.isArray(value) ? value : {
|
||||
value
|
||||
};
|
||||
const locations = (value.locations || []).concat([
|
||||
location
|
||||
]);
|
||||
return {
|
||||
...value,
|
||||
locations
|
||||
};
|
||||
});
|
||||
}
|
||||
withValue(func) {
|
||||
// Returns a new pattern, with the function applied to the value of
|
||||
// each event. It has the alias 'fmap'.
|
||||
@ -42273,7 +42296,14 @@ class Pattern {
|
||||
_patternify(func) {
|
||||
const pat = this;
|
||||
const patterned = function(...args) {
|
||||
// the problem here: args could a pattern that has been turned into an object to add location
|
||||
// to avoid object checking for every pattern method, we can remove it here...
|
||||
// in the future, patternified args should be marked as well + some better object handling
|
||||
args = args.map((arg)=>arg.constructor?.name === 'Pattern' ? arg.fmap((value)=>value.value || value
|
||||
) : arg
|
||||
);
|
||||
const pat_arg = sequence(...args);
|
||||
// arg.locations has to go somewhere..
|
||||
return pat_arg.fmap((arg)=>func.call(pat, arg)
|
||||
).outerJoin();
|
||||
};
|
||||
@ -42688,6 +42718,34 @@ Pattern.prototype.bootstrap = ()=>{
|
||||
}));
|
||||
return bootstrapped;
|
||||
};
|
||||
// this is wrapped around mini patterns to offset krill parser location into the global js code space
|
||||
function withLocationOffset(pat, offset) {
|
||||
return pat.fmap((value)=>{
|
||||
value = typeof value === 'object' && !Array.isArray(value) ? value : {
|
||||
value
|
||||
};
|
||||
let locations = value.locations || [];
|
||||
locations = locations.map(({ start , end })=>{
|
||||
const colOffset = start.line === 1 ? offset.start.column : 0;
|
||||
return {
|
||||
start: {
|
||||
...start,
|
||||
line: start.line - 1 + (offset.start.line - 1) + 1,
|
||||
column: start.column - 1 + colOffset
|
||||
},
|
||||
end: {
|
||||
...end,
|
||||
line: end.line - 1 + (offset.start.line - 1) + 1,
|
||||
column: end.column - 1 + colOffset
|
||||
}
|
||||
};
|
||||
});
|
||||
return {
|
||||
...value,
|
||||
locations
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
},{"fraction.js":"1Q5M2","ramda":"10uzi","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"1Q5M2":[function(require,module,exports) {
|
||||
/**
|
||||
@ -57782,7 +57840,7 @@ Pattern.prototype.voicings = function(range) {
|
||||
'A4'
|
||||
];
|
||||
return this.fmapNested((event)=>{
|
||||
lastVoicing = getVoicing(event.value, lastVoicing, range);
|
||||
lastVoicing = getVoicing(event.value?.value || event.value, lastVoicing, range);
|
||||
return _strudelMjs.stack(...lastVoicing);
|
||||
});
|
||||
};
|
||||
@ -62414,7 +62472,7 @@ var _tonal = require("@tonaljs/tonal");
|
||||
var _strudelMjs = require("../../strudel.mjs");
|
||||
const Pattern = _strudelMjs.Pattern;
|
||||
function toNoteEvent(event) {
|
||||
if (typeof event === 'string') return {
|
||||
if (typeof event === 'string' || typeof event === 'number') return {
|
||||
value: event
|
||||
};
|
||||
if (event.value) return event;
|
||||
@ -62464,6 +62522,12 @@ Pattern.prototype._mapNotes = function(func) {
|
||||
Pattern.prototype._transpose = function(intervalOrSemitones) {
|
||||
return this._mapNotes(({ value , scale })=>{
|
||||
const interval = !isNaN(Number(intervalOrSemitones)) ? _tonal.Interval.fromSemitones(intervalOrSemitones) : String(intervalOrSemitones);
|
||||
if (typeof value === 'number') {
|
||||
const semitones = typeof interval === 'string' ? _tonal.Interval.semitones(interval) || 0 : interval;
|
||||
return {
|
||||
value: value + semitones
|
||||
};
|
||||
}
|
||||
return {
|
||||
value: _tonal.Note.transpose(value, interval),
|
||||
scale
|
||||
@ -62477,7 +62541,8 @@ Pattern.prototype._transpose = function(intervalOrSemitones) {
|
||||
// or even `stack(c3).superimpose(transpose.slowcat(7, 5))` or
|
||||
Pattern.prototype._scaleTranspose = function(offset) {
|
||||
return this._mapNotes(({ value , scale })=>{
|
||||
if (!scale) throw new Error('can only use scaleOffset after .scale');
|
||||
if (!scale) throw new Error('can only use scaleTranspose after .scale');
|
||||
if (typeof value !== 'string') throw new Error('can only use scaleTranspose with notes');
|
||||
return {
|
||||
value: scaleTranspose(scale, Number(offset), value),
|
||||
scale
|
||||
@ -62532,23 +62597,63 @@ Pattern.prototype.define('groove', (groove, pat)=>pat.groove(groove)
|
||||
},{"../../strudel.mjs":"ggZqJ"}],"67UCx":[function(require,module,exports) {
|
||||
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
||||
parcelHelpers.defineInteropFlag(exports);
|
||||
parcelHelpers.export(exports, "addMiniLocations", ()=>addMiniLocations
|
||||
);
|
||||
var _indexJs = require("./shift-parser/index.js"); // npm module does not work in the browser
|
||||
var _shiftTraverser = require("./shift-traverser"); // npm module does not work in the browser
|
||||
var _shiftTraverserDefault = parcelHelpers.interopDefault(_shiftTraverser);
|
||||
var _shiftAst = require("shift-ast");
|
||||
var _shiftCodegen = require("shift-codegen");
|
||||
var _shiftCodegenDefault = parcelHelpers.interopDefault(_shiftCodegen);
|
||||
var _strudelMjs = require("../../strudel.mjs");
|
||||
const { replace } = _shiftTraverserDefault.default;
|
||||
const { Pattern } = _strudelMjs;
|
||||
const isNote = (name)=>/^[a-gC-G][bs]?[0-9]$/.test(name)
|
||||
;
|
||||
const addLocations = true;
|
||||
const addMiniLocations = true;
|
||||
exports.default = (code)=>{
|
||||
const ast = _indexJs.parseScript(code);
|
||||
const shifted = replace(ast, {
|
||||
const ast = _indexJs.parseScriptWithLocation(code);
|
||||
const nodesWithLocation = [];
|
||||
const parents = [];
|
||||
const shifted = replace(ast.tree, {
|
||||
enter (node, parent) {
|
||||
// replace identifiers that are a note with a note string
|
||||
parents.push(parent);
|
||||
const isSynthetic = parents.some((p)=>nodesWithLocation.includes(p)
|
||||
);
|
||||
if (isSynthetic) return node;
|
||||
const grandparent = parents[parents.length - 2];
|
||||
const isTimeCat = parent?.type === 'ArrayExpression' && isPatternFactory(grandparent);
|
||||
const isMarkable = isPatternFactory(parent) || isTimeCat;
|
||||
// operator overloading => still not done
|
||||
const operators = {
|
||||
'*': 'fast',
|
||||
'/': 'slow',
|
||||
'&': 'stack',
|
||||
'&&': 'append'
|
||||
};
|
||||
if (node.type === 'BinaryExpression' && operators[node.operator] && [
|
||||
'LiteralNumericExpression',
|
||||
'LiteralStringExpression',
|
||||
'IdentifierExpression'
|
||||
].includes(node.right?.type) && canBeOverloaded(node.left)) {
|
||||
let arg = node.left;
|
||||
if (node.left.type === 'IdentifierExpression') arg = wrapReify(node.left);
|
||||
return new _shiftAst.CallExpression({
|
||||
callee: new _shiftAst.StaticMemberExpression({
|
||||
property: operators[node.operator],
|
||||
object: wrapReify(arg)
|
||||
}),
|
||||
arguments: [
|
||||
node.right
|
||||
]
|
||||
});
|
||||
}
|
||||
// replace pseudo note variables
|
||||
if (node.type === 'IdentifierExpression') {
|
||||
if (isNote(node.name)) {
|
||||
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
|
||||
if (addLocations && isMarkable) return reifyWithLocation(value, node, ast.locations, nodesWithLocation);
|
||||
return new _shiftAst.LiteralStringExpression({
|
||||
value
|
||||
});
|
||||
@ -62557,14 +62662,205 @@ exports.default = (code)=>{
|
||||
name: 'silence'
|
||||
});
|
||||
}
|
||||
if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) // console.log('add', node);
|
||||
return reifyWithLocation(node.value, node, ast.locations, nodesWithLocation);
|
||||
if (!addMiniLocations) return node;
|
||||
// mini notation location handling
|
||||
const miniFunctions = [
|
||||
'mini',
|
||||
'm'
|
||||
];
|
||||
const isAlreadyWrapped = parent?.type === 'CallExpression' && parent.callee.name === 'withLocationOffset';
|
||||
if (node.type === 'CallExpression' && miniFunctions.includes(node.callee.name) && !isAlreadyWrapped) {
|
||||
// mini('c3')
|
||||
if (node.arguments.length > 1) {
|
||||
// TODO: transform mini(...args) to cat(...args.map(mini)) ?
|
||||
console.warn('multi arg mini locations not supported yet...');
|
||||
return node;
|
||||
}
|
||||
return wrapLocationOffset(node, node.arguments, ast.locations, nodesWithLocation);
|
||||
}
|
||||
if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property) && !isAlreadyWrapped) // 'c3'.mini or 'c3'.m
|
||||
return wrapLocationOffset(node, node.object, ast.locations, nodesWithLocation);
|
||||
return node;
|
||||
},
|
||||
leave () {
|
||||
parents.pop();
|
||||
}
|
||||
});
|
||||
return _shiftCodegenDefault.default(shifted);
|
||||
}; // TODO: turn x.groove['[~ x]*2'] into x.groove('[~ x]*2'.m)
|
||||
};
|
||||
function wrapReify(node) {
|
||||
return new _shiftAst.CallExpression({
|
||||
callee: new _shiftAst.IdentifierExpression({
|
||||
name: 'reify'
|
||||
}),
|
||||
arguments: [
|
||||
node
|
||||
]
|
||||
});
|
||||
}
|
||||
function isPatternFactory(node) {
|
||||
return node?.type === 'CallExpression' && Object.keys(Pattern.prototype.factories).includes(node.callee.name);
|
||||
}
|
||||
function canBeOverloaded(node) {
|
||||
return node.type === 'IdentifierExpression' && isNote(node.name) || isPatternFactory(node);
|
||||
// TODO: support sequence(c3).transpose(3).x.y.z
|
||||
}
|
||||
// turn node into withLocationOffset(node, location)
|
||||
function wrapLocationOffset(node, stringNode, locations, nodesWithLocation) {
|
||||
// console.log('wrapppp', stringNode);
|
||||
const expression = {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'IdentifierExpression',
|
||||
name: 'withLocationOffset'
|
||||
},
|
||||
arguments: [
|
||||
node,
|
||||
getLocationObject(stringNode, locations)
|
||||
]
|
||||
};
|
||||
nodesWithLocation.push(expression);
|
||||
// console.log('wrapped', codegen(expression));
|
||||
return expression;
|
||||
}
|
||||
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
|
||||
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
|
||||
function reifyWithLocation(value, node, locations, nodesWithLocation) {
|
||||
// console.log('reifyWithLocation', value, node);
|
||||
const withLocation = new _shiftAst.CallExpression({
|
||||
callee: new _shiftAst.StaticMemberExpression({
|
||||
object: new _shiftAst.CallExpression({
|
||||
callee: new _shiftAst.IdentifierExpression({
|
||||
name: 'reify'
|
||||
}),
|
||||
arguments: [
|
||||
new _shiftAst.LiteralStringExpression({
|
||||
value
|
||||
})
|
||||
]
|
||||
}),
|
||||
property: 'withLocation'
|
||||
}),
|
||||
arguments: [
|
||||
getLocationObject(node, locations)
|
||||
]
|
||||
});
|
||||
nodesWithLocation.push(withLocation);
|
||||
return withLocation;
|
||||
}
|
||||
// returns ast for source location object
|
||||
function getLocationObject(node, locations) {
|
||||
/*const locationAST = parseScript(
|
||||
"x=" + JSON.stringify(ast.locations.get(node))
|
||||
).statements[0].expression.expression;
|
||||
|
||||
console.log("locationAST", locationAST);*/ /*const callAST = parseScript(
|
||||
`reify(${node.name}).withLocation(${JSON.stringify(
|
||||
ast.locations.get(node)
|
||||
)})`
|
||||
).statements[0].expression;*/ const loc = locations.get(node);
|
||||
return {
|
||||
type: 'ObjectExpression',
|
||||
properties: [
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'start'
|
||||
},
|
||||
expression: {
|
||||
type: 'ObjectExpression',
|
||||
properties: [
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'line'
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.start.line
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'column'
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.start.column
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'offset'
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.start.offset
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'end'
|
||||
},
|
||||
expression: {
|
||||
type: 'ObjectExpression',
|
||||
properties: [
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'line'
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.end.line
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'column'
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.end.column
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'offset'
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.end.offset
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
} // TODO: turn x.groove['[~ x]*2'] into x.groove('[~ x]*2'.m)
|
||||
// and ['c1*2'].xx into 'c1*2'.m.xx ??
|
||||
// or just all templated strings?? x.groove(`[~ x]*2`)
|
||||
|
||||
},{"./shift-parser/index.js":"1kFzJ","./shift-traverser":"bogJs","shift-ast":"ig2Ca","shift-codegen":"1GOrI","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"1kFzJ":[function(require,module,exports) {
|
||||
},{"./shift-parser/index.js":"1kFzJ","./shift-traverser":"bogJs","shift-ast":"ig2Ca","shift-codegen":"1GOrI","../../strudel.mjs":"ggZqJ","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"1kFzJ":[function(require,module,exports) {
|
||||
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
||||
parcelHelpers.defineInteropFlag(exports);
|
||||
parcelHelpers.export(exports, "parseModule", ()=>parseModule
|
||||
@ -95594,6 +95890,7 @@ parcelHelpers.export(exports, "minify", ()=>minify
|
||||
var _krillParser = require("../krill-parser");
|
||||
var _strudelMjs = require("../../strudel.mjs");
|
||||
var _tonal = require("@tonaljs/tonal");
|
||||
var _shapeshifter = require("./shapeshifter");
|
||||
const { pure , Pattern , Fraction , stack , slowcat , sequence , timeCat , silence } = _strudelMjs;
|
||||
const applyOptions = (parent)=>(pat, i)=>{
|
||||
const ast = parent.source_[i];
|
||||
@ -95639,6 +95936,7 @@ function resolveReplications(ast) {
|
||||
{
|
||||
type_: 'element',
|
||||
source_: child.source_,
|
||||
location_: child.location_,
|
||||
options_: {
|
||||
operator: {
|
||||
type_: 'stretch',
|
||||
@ -95680,7 +95978,21 @@ function patternifyAST(ast) {
|
||||
return sequence(...children);
|
||||
case 'element':
|
||||
if (ast.source_ === '~') return silence;
|
||||
if (typeof ast.source_ !== 'object') return ast.source_;
|
||||
if (typeof ast.source_ !== 'object') {
|
||||
if (!_shapeshifter.addMiniLocations) return ast.source_;
|
||||
if (!ast.location_) {
|
||||
console.warn('no location for', ast);
|
||||
return ast.source_;
|
||||
}
|
||||
const { start , end } = ast.location_;
|
||||
// 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(ast.source_).withLocation({
|
||||
start,
|
||||
end
|
||||
});
|
||||
}
|
||||
return patternifyAST(ast.source_);
|
||||
case 'stretch':
|
||||
return patternifyAST(ast.source_).slow(ast.arguments_.amount);
|
||||
@ -95742,7 +96054,7 @@ function minify(thing) {
|
||||
return reify(thing);
|
||||
}
|
||||
|
||||
},{"../krill-parser":"l2lgS","../../strudel.mjs":"ggZqJ","@tonaljs/tonal":"4q9Lu","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"l2lgS":[function(require,module,exports) {
|
||||
},{"../krill-parser":"l2lgS","../../strudel.mjs":"ggZqJ","@tonaljs/tonal":"4q9Lu","./shapeshifter":"67UCx","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"l2lgS":[function(require,module,exports) {
|
||||
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
|
||||
parcelHelpers.defineInteropFlag(exports);
|
||||
parcelHelpers.export(exports, "SyntaxError", ()=>peg$SyntaxError
|
||||
@ -97609,6 +97921,7 @@ function peg$parse(input, options1) {
|
||||
this.type_ = "element";
|
||||
this.source_ = source;
|
||||
this.options_ = options;
|
||||
this.location_ = location1();
|
||||
};
|
||||
var CommandStub = function(name, options) {
|
||||
this.type_ = "command";
|
||||
@ -97727,16 +98040,18 @@ var _javascriptJs = require("codemirror/mode/javascript/javascript.js");
|
||||
var _pegjsJs = require("codemirror/mode/pegjs/pegjs.js");
|
||||
var _materialCss = require("codemirror/theme/material.css");
|
||||
var _codemirrorCss = require("codemirror/lib/codemirror.css");
|
||||
function CodeMirror({ value , onChange , options }) {
|
||||
function CodeMirror({ value , onChange , options , editorDidMount }) {
|
||||
options = options || {
|
||||
mode: 'javascript',
|
||||
theme: 'material',
|
||||
lineNumbers: true
|
||||
lineNumbers: true,
|
||||
styleSelectedText: true
|
||||
};
|
||||
return(/*#__PURE__*/ _jsxRuntime.jsx(_reactCodemirror2.Controlled, {
|
||||
value: value,
|
||||
options: options,
|
||||
onBeforeChange: onChange
|
||||
onBeforeChange: onChange,
|
||||
editorDidMount: editorDidMount
|
||||
}));
|
||||
}
|
||||
exports.default = CodeMirror;
|
||||
@ -108958,4 +109273,4 @@ exports.default = cx;
|
||||
|
||||
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}]},["3uVTb"], "3uVTb", "parcelRequire94c2")
|
||||
|
||||
//# sourceMappingURL=index.dc15e374.js.map
|
||||
//# sourceMappingURL=index.a96519ca.js.map
|
||||
File diff suppressed because one or more lines are too long
@ -11,6 +11,6 @@
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<script src="/tutorial/index.dc15e374.js" defer=""></script>
|
||||
<script src="/tutorial/index.a96519ca.js" defer=""></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1936,6 +1936,7 @@ function peg$parse(input, options) {
|
||||
this.type_ = "element";
|
||||
this.source_ = source;
|
||||
this.options_ = options;
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var CommandStub = function(name, options)
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
this.type_ = "element";
|
||||
this.source_ = source;
|
||||
this.options_ = options;
|
||||
this.location_ = location();
|
||||
}
|
||||
|
||||
var CommandStub = function(name, options)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useLayoutEffect, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import * as Tone from 'tone';
|
||||
import CodeMirror from './CodeMirror';
|
||||
import cx from './cx';
|
||||
@ -35,9 +35,33 @@ function getRandomTune() {
|
||||
const randomTune = getRandomTune();
|
||||
|
||||
function App() {
|
||||
const [editor, setEditor] = useState<any>();
|
||||
const doc = useMemo(() => editor?.getDoc(), [editor]);
|
||||
const { setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog } = useRepl({
|
||||
tune: decoded || randomTune,
|
||||
defaultSynth,
|
||||
onEvent: useCallback(
|
||||
(event) => {
|
||||
const locs = event.value.locations;
|
||||
if (!locs) {
|
||||
return;
|
||||
}
|
||||
// mark active event
|
||||
const marks = locs.map(({ start, end }) =>
|
||||
doc.markText(
|
||||
{ line: start.line - 1, ch: start.column },
|
||||
{ line: end.line - 1, ch: end.column },
|
||||
{ css: 'background-color: gray;' }
|
||||
)
|
||||
);
|
||||
//Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler...
|
||||
setTimeout(() => {
|
||||
marks.forEach((mark) => mark.clear());
|
||||
// }, '+' + event.duration * 0.5);
|
||||
}, event.duration * 0.9 * 1000);
|
||||
},
|
||||
[doc]
|
||||
),
|
||||
});
|
||||
const logBox = useRef<any>();
|
||||
// scroll log box to bottom when log changes
|
||||
@ -106,10 +130,12 @@ function App() {
|
||||
<div className={cx('h-full bg-[#2A3236]', error ? 'focus:ring-red-500' : 'focus:ring-slate-800')}>
|
||||
<CodeMirror
|
||||
value={code}
|
||||
editorDidMount={setEditor}
|
||||
options={{
|
||||
mode: 'javascript',
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
styleSelectedText: true,
|
||||
}}
|
||||
onChange={(_: any, __: any, value: any) => setCode(value)}
|
||||
/>
|
||||
|
||||
@ -5,11 +5,12 @@ import 'codemirror/mode/pegjs/pegjs.js';
|
||||
import 'codemirror/theme/material.css';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
|
||||
export default function CodeMirror({ value, onChange, options }: any) {
|
||||
export default function CodeMirror({ value, onChange, options, editorDidMount }: any) {
|
||||
options = options || {
|
||||
mode: 'javascript',
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
styleSelectedText: true,
|
||||
};
|
||||
return <CodeMirror2 value={value} options={options} onBeforeChange={onChange} />;
|
||||
return <CodeMirror2 value={value} options={options} onBeforeChange={onChange} editorDidMount={editorDidMount} />;
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ Object.assign(globalThis, bootstrapped, Tone, toneHelpers);
|
||||
|
||||
export const evaluate: any = (code: string) => {
|
||||
const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code
|
||||
// console.log('shapeshifted', shapeshifted);
|
||||
let evaluated = eval(shapeshifted);
|
||||
if (typeof evaluated === 'function') {
|
||||
evaluated = evaluated();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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;
|
||||
|
||||
@ -49,6 +50,7 @@ function resolveReplications(ast) {
|
||||
{
|
||||
type_: 'element',
|
||||
source_: child.source_,
|
||||
location_: child.location_,
|
||||
options_: {
|
||||
operator: {
|
||||
type_: 'stretch',
|
||||
@ -91,7 +93,18 @@ export function patternifyAST(ast: any): any {
|
||||
return silence;
|
||||
}
|
||||
if (typeof ast.source_ !== 'object') {
|
||||
return ast.source_;
|
||||
if (!addMiniLocations) {
|
||||
return ast.source_;
|
||||
}
|
||||
if (!ast.location_) {
|
||||
console.warn('no location for', ast);
|
||||
return ast.source_;
|
||||
}
|
||||
const { start, end } = ast.location_;
|
||||
// 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(ast.source_).withLocation({ start, end });
|
||||
}
|
||||
return patternifyAST(ast.source_);
|
||||
case 'stretch':
|
||||
|
||||
@ -1,30 +1,266 @@
|
||||
import { parseScript } from './shift-parser/index.js'; // npm module does not work in the browser
|
||||
import { parseScriptWithLocation } from './shift-parser/index.js'; // npm module does not work in the browser
|
||||
import traverser from './shift-traverser'; // npm module does not work in the browser
|
||||
const { replace } = traverser;
|
||||
import { LiteralStringExpression, IdentifierExpression } from 'shift-ast';
|
||||
import { LiteralStringExpression, IdentifierExpression, CallExpression, StaticMemberExpression } from 'shift-ast';
|
||||
import codegen from 'shift-codegen';
|
||||
import * as strudel from '../../strudel.mjs';
|
||||
|
||||
const { Pattern } = strudel;
|
||||
|
||||
const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
|
||||
|
||||
const addLocations = true;
|
||||
export const addMiniLocations = true;
|
||||
|
||||
export default (code) => {
|
||||
const ast = parseScript(code);
|
||||
const shifted = replace(ast, {
|
||||
const ast = parseScriptWithLocation(code);
|
||||
const nodesWithLocation = [];
|
||||
const parents = [];
|
||||
const shifted = replace(ast.tree, {
|
||||
enter(node, parent) {
|
||||
// replace identifiers that are a note with a note string
|
||||
parents.push(parent);
|
||||
const isSynthetic = parents.some((p) => nodesWithLocation.includes(p));
|
||||
if (isSynthetic) {
|
||||
return node;
|
||||
}
|
||||
const grandparent = parents[parents.length - 2];
|
||||
const isTimeCat = parent?.type === 'ArrayExpression' && isPatternFactory(grandparent);
|
||||
const isMarkable = isPatternFactory(parent) || isTimeCat;
|
||||
// operator overloading => still not done
|
||||
const operators = {
|
||||
'*': 'fast',
|
||||
'/': 'slow',
|
||||
'&': 'stack',
|
||||
'&&': 'append',
|
||||
};
|
||||
if (
|
||||
node.type === 'BinaryExpression' &&
|
||||
operators[node.operator] &&
|
||||
['LiteralNumericExpression', 'LiteralStringExpression', 'IdentifierExpression'].includes(node.right?.type) &&
|
||||
canBeOverloaded(node.left)
|
||||
) {
|
||||
let arg = node.left;
|
||||
if (node.left.type === 'IdentifierExpression') {
|
||||
arg = wrapReify(node.left);
|
||||
}
|
||||
return new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
property: operators[node.operator],
|
||||
object: wrapReify(arg),
|
||||
}),
|
||||
arguments: [node.right],
|
||||
});
|
||||
}
|
||||
// replace pseudo note variables
|
||||
if (node.type === 'IdentifierExpression') {
|
||||
if (isNote(node.name)) {
|
||||
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
|
||||
if (addLocations && isMarkable) {
|
||||
return reifyWithLocation(value, node, ast.locations, nodesWithLocation);
|
||||
}
|
||||
return new LiteralStringExpression({ value });
|
||||
}
|
||||
if (node.name === 'r') {
|
||||
return new IdentifierExpression({ name: 'silence' });
|
||||
}
|
||||
}
|
||||
if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) {
|
||||
// console.log('add', node);
|
||||
return reifyWithLocation(node.value, node, ast.locations, nodesWithLocation);
|
||||
}
|
||||
if (!addMiniLocations) {
|
||||
return node;
|
||||
}
|
||||
// mini notation location handling
|
||||
const miniFunctions = ['mini', 'm'];
|
||||
const isAlreadyWrapped = parent?.type === 'CallExpression' && parent.callee.name === 'withLocationOffset';
|
||||
if (node.type === 'CallExpression' && miniFunctions.includes(node.callee.name) && !isAlreadyWrapped) {
|
||||
// mini('c3')
|
||||
if (node.arguments.length > 1) {
|
||||
// TODO: transform mini(...args) to cat(...args.map(mini)) ?
|
||||
console.warn('multi arg mini locations not supported yet...');
|
||||
return node;
|
||||
}
|
||||
return wrapLocationOffset(node, node.arguments, ast.locations, nodesWithLocation);
|
||||
}
|
||||
if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property) && !isAlreadyWrapped) {
|
||||
// 'c3'.mini or 'c3'.m
|
||||
return wrapLocationOffset(node, node.object, ast.locations, nodesWithLocation);
|
||||
}
|
||||
return node;
|
||||
},
|
||||
leave() {
|
||||
parents.pop();
|
||||
},
|
||||
});
|
||||
return codegen(shifted);
|
||||
};
|
||||
|
||||
function wrapReify(node) {
|
||||
return new CallExpression({
|
||||
callee: new IdentifierExpression({
|
||||
name: 'reify',
|
||||
}),
|
||||
arguments: [node],
|
||||
});
|
||||
}
|
||||
|
||||
function isPatternFactory(node) {
|
||||
return node?.type === 'CallExpression' && Object.keys(Pattern.prototype.factories).includes(node.callee.name);
|
||||
}
|
||||
|
||||
function canBeOverloaded(node) {
|
||||
return (node.type === 'IdentifierExpression' && isNote(node.name)) || isPatternFactory(node);
|
||||
// TODO: support sequence(c3).transpose(3).x.y.z
|
||||
}
|
||||
|
||||
// turn node into withLocationOffset(node, location)
|
||||
function wrapLocationOffset(node, stringNode, locations, nodesWithLocation) {
|
||||
// console.log('wrapppp', stringNode);
|
||||
const expression = {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'IdentifierExpression',
|
||||
name: 'withLocationOffset',
|
||||
},
|
||||
arguments: [node, getLocationObject(stringNode, locations)],
|
||||
};
|
||||
nodesWithLocation.push(expression);
|
||||
// console.log('wrapped', codegen(expression));
|
||||
return expression;
|
||||
}
|
||||
|
||||
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
|
||||
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
|
||||
function reifyWithLocation(value, node, locations, nodesWithLocation) {
|
||||
// console.log('reifyWithLocation', value, node);
|
||||
const withLocation = new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
object: new CallExpression({
|
||||
callee: new IdentifierExpression({
|
||||
name: 'reify',
|
||||
}),
|
||||
arguments: [new LiteralStringExpression({ value })],
|
||||
}),
|
||||
property: 'withLocation',
|
||||
}),
|
||||
arguments: [getLocationObject(node, locations)],
|
||||
});
|
||||
nodesWithLocation.push(withLocation);
|
||||
return withLocation;
|
||||
}
|
||||
|
||||
// returns ast for source location object
|
||||
function getLocationObject(node, locations) {
|
||||
/*const locationAST = parseScript(
|
||||
"x=" + JSON.stringify(ast.locations.get(node))
|
||||
).statements[0].expression.expression;
|
||||
|
||||
console.log("locationAST", locationAST);*/
|
||||
|
||||
/*const callAST = parseScript(
|
||||
`reify(${node.name}).withLocation(${JSON.stringify(
|
||||
ast.locations.get(node)
|
||||
)})`
|
||||
).statements[0].expression;*/
|
||||
const loc = locations.get(node);
|
||||
return {
|
||||
type: 'ObjectExpression',
|
||||
properties: [
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'start',
|
||||
},
|
||||
expression: {
|
||||
type: 'ObjectExpression',
|
||||
properties: [
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'line',
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.start.line,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'column',
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.start.column,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'offset',
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.start.offset,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'end',
|
||||
},
|
||||
expression: {
|
||||
type: 'ObjectExpression',
|
||||
properties: [
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'line',
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.end.line,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'column',
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.end.column,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'DataProperty',
|
||||
name: {
|
||||
type: 'StaticPropertyName',
|
||||
value: 'offset',
|
||||
},
|
||||
expression: {
|
||||
type: 'LiteralNumericExpression',
|
||||
value: loc.end.offset,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: turn x.groove['[~ x]*2'] into x.groove('[~ x]*2'.m)
|
||||
// and ['c1*2'].xx into 'c1*2'.m.xx ??
|
||||
// or just all templated strings?? x.groove(`[~ x]*2`)
|
||||
// or just all templated strings?? x.groove(`[~ x]*2`)
|
||||
|
||||
@ -4,12 +4,12 @@ import { Pattern as _Pattern } from '../../strudel.mjs';
|
||||
const Pattern = _Pattern as any;
|
||||
|
||||
export declare interface NoteEvent {
|
||||
value: string;
|
||||
value: string | number;
|
||||
scale?: string;
|
||||
}
|
||||
|
||||
function toNoteEvent(event: string | NoteEvent): NoteEvent {
|
||||
if (typeof event === 'string') {
|
||||
if (typeof event === 'string' || typeof event === 'number') {
|
||||
return { value: event };
|
||||
}
|
||||
if (event.value) {
|
||||
@ -73,6 +73,10 @@ Pattern.prototype._transpose = function (intervalOrSemitones: string | number) {
|
||||
const interval = !isNaN(Number(intervalOrSemitones))
|
||||
? Interval.fromSemitones(intervalOrSemitones as number)
|
||||
: String(intervalOrSemitones);
|
||||
if (typeof value === 'number') {
|
||||
const semitones = typeof interval === 'string' ? Interval.semitones(interval) || 0 : interval;
|
||||
return { value: value + semitones };
|
||||
}
|
||||
return { value: Note.transpose(value, interval), scale };
|
||||
});
|
||||
};
|
||||
@ -86,7 +90,10 @@ Pattern.prototype._transpose = function (intervalOrSemitones: string | number) {
|
||||
Pattern.prototype._scaleTranspose = function (offset: number | string) {
|
||||
return this._mapNotes(({ value, scale }: NoteEvent) => {
|
||||
if (!scale) {
|
||||
throw new Error('can only use scaleOffset after .scale');
|
||||
throw new Error('can only use scaleTranspose after .scale');
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('can only use scaleTranspose with notes');
|
||||
}
|
||||
return { value: scaleTranspose(scale, Number(offset), value), scale };
|
||||
});
|
||||
|
||||
@ -5,11 +5,11 @@ export const timeCatMini = `stack(
|
||||
)`;
|
||||
|
||||
export const timeCat = `stack(
|
||||
timeCat([3, c3], [1, stack(eb3, g3, m(c4, d4).slow(2))]),
|
||||
m(c2, g2),
|
||||
timeCat([3, c3], [1, stack(eb3, g3, cat(c4, d4).slow(2))]),
|
||||
cat(c2, g2),
|
||||
sequence(
|
||||
timeCat([5, eb4], [3, m(f4, eb4, d4)]),
|
||||
m(eb4, c4).slow(2)
|
||||
timeCat([5, eb4], [3, cat(f4, eb4, d4)]),
|
||||
cat(eb4, c4).slow(2)
|
||||
).slow(4)
|
||||
)`;
|
||||
|
||||
@ -178,11 +178,11 @@ export const spanish = `slowcat(
|
||||
stack(g3,b3,d4)
|
||||
)`;
|
||||
|
||||
export const whirlyStrudel = `mini("[e4 [b2 b3] c4]")
|
||||
export const whirlyStrudel = `sequence(e4, [b2, b3], c4)
|
||||
.every(4, fast(2))
|
||||
.every(3, slow(1.5))
|
||||
.fast(slowcat(1.25, 1, 1.5))
|
||||
.every(2, _ => mini("e4 ~ e3 d4 ~"))`;
|
||||
.every(2, _ => sequence(e4, r, e3, d4, r))`;
|
||||
|
||||
export const swimming = `stack(
|
||||
mini(
|
||||
@ -296,13 +296,6 @@ export const giantStepsReggae = `stack(
|
||||
.groove('x ~'.m.fast(4*8))
|
||||
).slow(25)`;
|
||||
|
||||
/* export const transposedChords = `stack(
|
||||
m('c2 eb2 g2'),
|
||||
m('Cm7').voicings(['g2','c4']).slow(2)
|
||||
).transpose(
|
||||
slowcat(1, 2, 3, 2).slow(2)
|
||||
).transpose(5)`; */
|
||||
|
||||
export const transposedChordsHacked = `stack(
|
||||
'c2 eb2 g2'.mini,
|
||||
'Cm7'.pure.voicings(['g2','c4']).slow(2)
|
||||
@ -315,27 +308,12 @@ export const scaleTranspose = `stack(f2, f3, c4, ab4)
|
||||
.scaleTranspose(sequence(0, -1, -2, -3).slow(4))
|
||||
.transpose(sequence(0, 1).slow(16))`;
|
||||
|
||||
/* export const groove = `stack(
|
||||
m('c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]'),
|
||||
m('[C^7 A7] [Dm7 G7]')
|
||||
.groove(m('[x@2 x] [~@2 x] [~ x@2]@2 [x ~@2] ~ [~@2 x@4]@2'))
|
||||
.voicings(['G3','A4'])
|
||||
).slow(4.5)`; */
|
||||
|
||||
export const groove = `stack(
|
||||
'c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]'.mini,
|
||||
'[C^7 A7] [Dm7 G7]'.mini.groove('[x@2 x] [~@2 x] [~ x@2]@2 [x ~@2] ~ [~@2 x@4]@2'.mini)
|
||||
.voicings(['G3','A4'])
|
||||
).slow(4)`;
|
||||
|
||||
/* export const magicSofa = `stack(
|
||||
m('[C^7 F^7 ~]/3 [Dm7 G7 A7 ~]/4')
|
||||
.every(2, fast(2))
|
||||
.voicings(),
|
||||
m('[c2 f2 g2]/3 [d2 g2 a2 e2]/4')
|
||||
).slow(1)
|
||||
.transpose.slowcat(0, 2, 3, 4)`; */
|
||||
|
||||
export const magicSofa = `stack(
|
||||
'<C^7 F^7 ~> <Dm7 G7 A7 ~>'.m
|
||||
.every(2, fast(2))
|
||||
@ -462,66 +440,3 @@ export const caverave = `() => {
|
||||
synths
|
||||
).slow(2);
|
||||
}`;
|
||||
|
||||
export const caveravefuture = `() => {
|
||||
const delay = new FeedbackDelay(1/8, .4).chain(vol(0.5), out);
|
||||
const kick = new MembraneSynth().chain(vol(.8), out);
|
||||
const snare = new NoiseSynth().chain(vol(.8), out);
|
||||
const hihat = new MetalSynth().set(adsr(0, .08, 0, .1)).chain(vol(.3).connect(delay),out);
|
||||
const bass = new Synth().set({ ...osc('sawtooth'), ...adsr(0, .1, .4) }).chain(lowpass(900), vol(.5), out);
|
||||
const keys = new PolySynth().set({ ...osc('sawtooth'), ...adsr(0, .5, .2, .7) }).chain(lowpass(1200), vol(.5), out);
|
||||
|
||||
const drums = stack(
|
||||
\`c1*2\`.tone(kick).bypass(\`<0@7 1>/8\`),
|
||||
\`~ <x!7 [x@3 x]>\`.tone(snare).bypass(\`<0@7 1>/4\`),
|
||||
\`[~ c4]*2\`.tone(hihat)
|
||||
);
|
||||
|
||||
const thru = (x) => x.transpose(\`<0 1>/8\`).transpose(-1);
|
||||
const synths = stack(
|
||||
\`<eb4 d4 c4 b3>/2\`.scale(timeCat([3,'C minor'],[1,'C melodic minor']).slow(8)).groove(\`[~ x]*2\`)
|
||||
.edit(
|
||||
scaleTranspose(0).early(0),
|
||||
scaleTranspose(2).early(1/8),
|
||||
scaleTranspose(7).early(1/4),
|
||||
scaleTranspose(8).early(3/8)
|
||||
).edit(thru).tone(keys).bypass(\`<1 0>/16\`),
|
||||
\`<C2 Bb1 Ab1 [G1 [G2 G1]]>/2\`.groove(\`x [~ x] <[~ [~ x]]!3 [x x]>@2\`).edit(thru).tone(bass),
|
||||
\`<Cm7 Bb7 Fm7 G7b13>/2\`.groove(\`~ [x@0.5 ~]\`.fast(2)).voicings().edit(thru).every(2, early(1/8)).tone(keys).bypass(\`<0@7 1>/8\`.early(1/4)),
|
||||
)
|
||||
return stack(
|
||||
drums.fast(2),
|
||||
synths
|
||||
).slow(2);
|
||||
}`;
|
||||
|
||||
export const caveravefuture2 = `const delay = new FeedbackDelay(1/8, .4).chain(vol(0.5), out);
|
||||
const kick = new MembraneSynth().chain(vol(.8), out);
|
||||
const snare = new NoiseSynth().chain(vol(.8), out);
|
||||
const hihat = new MetalSynth().set(adsr(0, .08, 0, .1)).chain(vol(.3).connect(delay),out);
|
||||
const bass = new Synth().set({ ...osc('sawtooth'), ...adsr(0, .1, .4) }).chain(lowpass(900), vol(.5), out);
|
||||
const keys = new PolySynth().set({ ...osc('sawtooth'), ...adsr(0, .5, .2, .7) }).chain(lowpass(1200), vol(.5), out);
|
||||
|
||||
const drums = stack(
|
||||
"c1*2".tone(kick).bypass("<0@7 1>/8"),
|
||||
"~ <x!7 [x@3 x]>".tone(snare).bypass("<0@7 1>/4"),
|
||||
"[~ c4]*2".tone(hihat)
|
||||
);
|
||||
|
||||
const thru = (x) => x.transpose("<0 1>/8").transpose(-1);
|
||||
const synths = stack(
|
||||
"<eb4 d4 c4 b3>/2".scale(timeCat([3, 'C minor'], [1, 'C melodic minor']).slow(8)).groove("[~ x]*2")
|
||||
.edit(
|
||||
scaleTranspose(0).early(0),
|
||||
scaleTranspose(2).early(1/8),
|
||||
scaleTranspose(7).early(1/4),
|
||||
scaleTranspose(8).early(3/8)
|
||||
).edit(thru).tone(keys).bypass("<1 0>/16"),
|
||||
"<C2 Bb1 Ab1 [G1 [G2 G1]]>/2".groove("x [~ x] <[~ [~ x]]!3 [x x]>@2").edit(thru).tone(bass),
|
||||
"<Cm7 Bb7 Fm7 G7b13>/2".groove("~ [x@0.5 ~]".fast(2)).voicings().edit(thru).every(2, early(1/8)).tone(keys).bypass("<0@7 1>/8".early(1/4)),
|
||||
)
|
||||
$: stack(
|
||||
drums.fast(2),
|
||||
synths
|
||||
).slow(2);
|
||||
`;
|
||||
|
||||
@ -3,7 +3,6 @@ import type { ToneEventCallback } from 'tone';
|
||||
import * as Tone from 'tone';
|
||||
import { TimeSpan } from '../../strudel.mjs';
|
||||
import type { Hap } from './types';
|
||||
import usePostMessage from './usePostMessage';
|
||||
|
||||
export declare interface UseCycleProps {
|
||||
onEvent: ToneEventCallback<any>;
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useCallback, useLayoutEffect, useState, useMemo, useEffect } from 'react';
|
||||
import { useCallback, useState, useMemo } from 'react';
|
||||
import { isNote } from 'tone';
|
||||
import { evaluate } from './evaluate';
|
||||
import { useWebMidi } from './midi';
|
||||
import type { Pattern } from './types';
|
||||
import useCycle from './useCycle';
|
||||
import usePostMessage from './usePostMessage';
|
||||
@ -12,7 +11,7 @@ let s4 = () => {
|
||||
.substring(1);
|
||||
};
|
||||
|
||||
function useRepl({ tune, defaultSynth, autolink = true }) {
|
||||
function useRepl({ tune, defaultSynth, autolink = true, onEvent }: any) {
|
||||
const id = useMemo(() => s4(), []);
|
||||
const [code, setCode] = useState<string>(tune);
|
||||
const [activeCode, setActiveCode] = useState<string>();
|
||||
@ -36,6 +35,8 @@ function useRepl({ tune, defaultSynth, autolink = true }) {
|
||||
setError(undefined);
|
||||
setActiveCode(_code);
|
||||
} catch (err: any) {
|
||||
err.message = 'evaluation error: ' + err.message;
|
||||
console.warn(err)
|
||||
setError(err);
|
||||
}
|
||||
};
|
||||
@ -48,35 +49,40 @@ function useRepl({ tune, defaultSynth, autolink = true }) {
|
||||
};
|
||||
// cycle hook to control scheduling
|
||||
const cycle = useCycle({
|
||||
onEvent: useCallback((time, event) => {
|
||||
try {
|
||||
if (!event.value?.onTrigger) {
|
||||
const note = event.value?.value || event.value;
|
||||
if (!isNote(note)) {
|
||||
throw new Error('not a note: ' + note);
|
||||
}
|
||||
if (defaultSynth) {
|
||||
defaultSynth.triggerAttackRelease(note, event.duration, time);
|
||||
} else {
|
||||
throw new Error('no defaultSynth passed to useRepl.');
|
||||
}
|
||||
/* console.warn('no instrument chosen', event);
|
||||
onEvent: useCallback(
|
||||
(time, event) => {
|
||||
try {
|
||||
onEvent?.(event);
|
||||
if (!event.value?.onTrigger) {
|
||||
const note = event.value?.value || event.value;
|
||||
if (!isNote(note)) {
|
||||
throw new Error('not a note: ' + note);
|
||||
}
|
||||
if (defaultSynth) {
|
||||
defaultSynth.triggerAttackRelease(note, event.duration, time);
|
||||
} else {
|
||||
throw new Error('no defaultSynth passed to useRepl.');
|
||||
}
|
||||
/* console.warn('no instrument chosen', event);
|
||||
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
|
||||
} else {
|
||||
const { onTrigger } = event.value;
|
||||
onTrigger(time, event);
|
||||
} else {
|
||||
const { onTrigger } = event.value;
|
||||
onTrigger(time, event);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.warn(err);
|
||||
err.message = 'unplayable event: ' + err?.message;
|
||||
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.warn(err);
|
||||
err.message = 'unplayable event: ' + err?.message;
|
||||
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
|
||||
}
|
||||
}, []),
|
||||
},
|
||||
[onEvent]
|
||||
),
|
||||
onQuery: useCallback(
|
||||
(span) => {
|
||||
try {
|
||||
return pattern?.query(span) || [];
|
||||
} catch (err: any) {
|
||||
err.message = 'query error: ' + err.message;
|
||||
setError(err);
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ Pattern.prototype.voicings = function (range) {
|
||||
range = ['F3', 'A4'];
|
||||
}
|
||||
return this.fmapNested((event) => {
|
||||
lastVoicing = getVoicing(event.value, lastVoicing, range);
|
||||
lastVoicing = getVoicing(event.value?.value || event.value, lastVoicing, range);
|
||||
return stack(...lastVoicing);
|
||||
});
|
||||
};
|
||||
|
||||
69
strudel.mjs
69
strudel.mjs
@ -148,7 +148,7 @@ class TimeSpan {
|
||||
return result
|
||||
}
|
||||
|
||||
get midpoint() {
|
||||
midpoint() {
|
||||
return(this.begin.add((this.end.sub(this.begin)).div(Fraction(2))))
|
||||
}
|
||||
|
||||
@ -298,6 +298,14 @@ class Pattern {
|
||||
return new Pattern(state => func(this.query(state)))
|
||||
}
|
||||
|
||||
withLocation(location) {
|
||||
return this.fmap(value => {
|
||||
value = typeof value === 'object' && !Array.isArray(value) ? value : { value };
|
||||
const locations = (value.locations || []).concat([location]);
|
||||
return {...value, locations }
|
||||
})
|
||||
}
|
||||
|
||||
withValue(func) {
|
||||
// Returns a new pattern, with the function applied to the value of
|
||||
// each event. It has the alias 'fmap'.
|
||||
@ -478,7 +486,14 @@ class Pattern {
|
||||
_patternify(func) {
|
||||
const pat = this
|
||||
const patterned = function (...args) {
|
||||
// the problem here: args could a pattern that has been turned into an object to add location
|
||||
// to avoid object checking for every pattern method, we can remove it here...
|
||||
// in the future, patternified args should be marked as well + some better object handling
|
||||
args = args.map((arg) =>
|
||||
arg.constructor?.name === 'Pattern' ? arg.fmap((value) => value.value || value) : arg
|
||||
);
|
||||
const pat_arg = sequence(...args)
|
||||
// arg.locations has to go somewhere..
|
||||
return pat_arg.fmap(arg => func.call(pat,arg)).outerJoin()
|
||||
}
|
||||
return patterned
|
||||
@ -680,6 +695,32 @@ function steady(value) {
|
||||
return new Pattern(span => Hap(undefined, span, value))
|
||||
}
|
||||
|
||||
export const signal = func => {
|
||||
const query = span => [new Hap(undefined, span, func(span.midpoint()))]
|
||||
return new Pattern(query)
|
||||
}
|
||||
|
||||
const _toBipolar = pat => pat.fmap(x => (x * 2) - 1)
|
||||
const _fromBipolar = pat => pat.fmap(x => (x + 1) / 2)
|
||||
|
||||
export const sine2 = signal(t => Math.sin(Math.PI * 2 * t))
|
||||
export const sine = _fromBipolar(sine2)
|
||||
|
||||
export const cosine2 = sine2._early(0.25)
|
||||
export const cosine = sine._early(0.25)
|
||||
|
||||
export const saw = signal(t => t % 1)
|
||||
export const saw2 = _toBipolar(saw)
|
||||
|
||||
export const isaw = signal(t => 1 - (t % 1))
|
||||
export const isaw2 = _toBipolar(isaw)
|
||||
|
||||
export const tri2 = fastcat(isaw2, saw2)
|
||||
export const tri = fastcat(isaw, saw)
|
||||
|
||||
export const square = signal(t => Math.floor((t*2) % 2))
|
||||
export const square2 = _toBipolar(square)
|
||||
|
||||
function reify(thing) {
|
||||
// Tunrs something into a pattern, unless it's already a pattern
|
||||
if (thing?.constructor?.name == "Pattern") {
|
||||
@ -882,10 +923,34 @@ Pattern.prototype.bootstrap = () => {
|
||||
return bootstrapped;
|
||||
}
|
||||
|
||||
// this is wrapped around mini patterns to offset krill parser location into the global js code space
|
||||
function withLocationOffset(pat, offset) {
|
||||
return pat.fmap((value) => {
|
||||
value = typeof value === 'object' && !Array.isArray(value) ? value : { value };
|
||||
let locations = (value.locations || []);
|
||||
locations = locations.map(({ start, end }) => {
|
||||
const colOffset = start.line === 1 ? offset.start.column : 0;
|
||||
return {
|
||||
start: {
|
||||
...start,
|
||||
line: start.line - 1 + (offset.start.line - 1) + 1,
|
||||
column: start.column - 1 + colOffset,
|
||||
},
|
||||
end: {
|
||||
...end,
|
||||
line: end.line - 1 + (offset.start.line - 1) + 1,
|
||||
column: end.column - 1 + colOffset,
|
||||
},
|
||||
}});
|
||||
return {...value, locations }
|
||||
});
|
||||
}
|
||||
|
||||
export {Fraction, TimeSpan, Hap, Pattern,
|
||||
pure, stack, slowcat, fastcat, cat, timeCat, sequence, polymeter, pm, polyrhythm, pr, reify, silence,
|
||||
fast, slow, early, late, rev,
|
||||
add, sub, mul, div, union, every, when, off, jux, append, superimpose,
|
||||
struct, mask, invert, inv
|
||||
struct, mask, invert, inv,
|
||||
withLocationOffset
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import Fraction from 'fraction.js'
|
||||
|
||||
import { strict as assert } from 'assert';
|
||||
|
||||
import {TimeSpan, Hap, Pattern, pure, stack, fastcat, slowcat, cat, sequence, polyrhythm, silence, fast, timeCat,add,sub,mul,div, State} from "../strudel.mjs";
|
||||
import {TimeSpan, Hap, State, Pattern, pure, stack, fastcat, slowcat, cat, sequence, polyrhythm, silence, fast, timeCat,add,sub,mul,div,saw,saw2,isaw,isaw2,sine,sine2,square,square2,tri,tri2} from "../strudel.mjs";
|
||||
//import { Time } from 'tone';
|
||||
import pkg from 'tone';
|
||||
const { Time } = pkg;
|
||||
@ -308,4 +308,26 @@ describe('Pattern', function() {
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('signal()', function() {
|
||||
it('Can make saw/saw2', function() {
|
||||
assert.deepStrictEqual(
|
||||
saw.struct(true,true,true,true).firstCycle,
|
||||
sequence(1/8,3/8,5/8,7/8).firstCycle
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
saw2.struct(true,true,true,true).firstCycle,
|
||||
sequence(-3/4,-1/4,1/4,3/4).firstCycle
|
||||
)
|
||||
})
|
||||
it('Can make isaw/isaw2', function() {
|
||||
assert.deepStrictEqual(
|
||||
isaw.struct(true,true,true,true).firstCycle,
|
||||
sequence(7/8,5/8,3/8,1/8).firstCycle
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
isaw2.struct(true,true,true,true).firstCycle,
|
||||
sequence(3/4,1/4,-1/4,-3/4).firstCycle
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user