Merge branch 'main' into stateful-events

This commit is contained in:
alex 2022-02-23 20:24:05 +00:00
commit becc73e58a
27 changed files with 1090 additions and 238 deletions

View File

@ -1936,6 +1936,7 @@ function peg$parse(input, options) {
this.type_ = "element"; this.type_ = "element";
this.source_ = source; this.source_ = source;
this.options_ = options; this.options_ = options;
this.location_ = location();
} }
var CommandStub = function(name, options) var CommandStub = function(name, options)

View File

@ -175,6 +175,13 @@ class Pattern {
_withEvents(func) { _withEvents(func) {
return new Pattern((span) => func(this.query(span))); 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) { withValue(func) {
return new Pattern((span) => this.query(span).map((hap) => hap.withValue(func))); return new Pattern((span) => this.query(span).map((hap) => hap.withValue(func)));
} }
@ -312,6 +319,7 @@ class Pattern {
_patternify(func) { _patternify(func) {
const pat = this; const pat = this;
const patterned = function(...args) { const patterned = function(...args) {
args = args.map((arg) => arg.constructor?.name === "Pattern" ? arg.fmap((value) => value.value || value) : arg);
const pat_arg = sequence(...args); const pat_arg = sequence(...args);
return pat_arg.fmap((arg) => func.call(pat, arg)).outerJoin(); return pat_arg.fmap((arg) => func.call(pat, arg)).outerJoin();
}; };
@ -596,6 +604,28 @@ Pattern.prototype.bootstrap = () => {
})); }));
return bootstrapped; 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 { export {
Fraction, Fraction,
TimeSpan, TimeSpan,
@ -633,5 +663,6 @@ export {
struct, struct,
mask, mask,
invert, invert,
inv inv,
withLocationOffset
}; };

20
docs/dist/App.js vendored
View File

@ -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 * as Tone from "../_snowpack/pkg/tone.js";
import CodeMirror from "./CodeMirror.js"; import CodeMirror from "./CodeMirror.js";
import cx from "./cx.js"; import cx from "./cx.js";
@ -28,9 +28,21 @@ function getRandomTune() {
} }
const randomTune = getRandomTune(); const randomTune = getRandomTune();
function App() { 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({ const {setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog} = useRepl({
tune: decoded || randomTune, 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(); const logBox = useRef();
useLayoutEffect(() => { useLayoutEffect(() => {
@ -95,10 +107,12 @@ function App() {
className: cx("h-full bg-[#2A3236]", error ? "focus:ring-red-500" : "focus:ring-slate-800") className: cx("h-full bg-[#2A3236]", error ? "focus:ring-red-500" : "focus:ring-slate-800")
}, /* @__PURE__ */ React.createElement(CodeMirror, { }, /* @__PURE__ */ React.createElement(CodeMirror, {
value: code, value: code,
editorDidMount: setEditor,
options: { options: {
mode: "javascript", mode: "javascript",
theme: "material", theme: "material",
lineNumbers: true lineNumbers: true,
styleSelectedText: true
}, },
onChange: (_2, __, value) => setCode(value) onChange: (_2, __, value) => setCode(value)
}), /* @__PURE__ */ React.createElement("span", { }), /* @__PURE__ */ React.createElement("span", {

View File

@ -4,15 +4,17 @@ import "../_snowpack/pkg/codemirror/mode/javascript/javascript.js";
import "../_snowpack/pkg/codemirror/mode/pegjs/pegjs.js"; import "../_snowpack/pkg/codemirror/mode/pegjs/pegjs.js";
import "../_snowpack/pkg/codemirror/theme/material.css.proxy.js"; import "../_snowpack/pkg/codemirror/theme/material.css.proxy.js";
import "../_snowpack/pkg/codemirror/lib/codemirror.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 || { options = options || {
mode: "javascript", mode: "javascript",
theme: "material", theme: "material",
lineNumbers: true lineNumbers: true,
styleSelectedText: true
}; };
return /* @__PURE__ */ React.createElement(CodeMirror2, { return /* @__PURE__ */ React.createElement(CodeMirror2, {
value, value,
options, options,
onBeforeChange: onChange onBeforeChange: onChange,
editorDidMount
}); });
} }

12
docs/dist/parse.js vendored
View File

@ -1,6 +1,7 @@
import * as krill from "../_snowpack/link/repl/krill-parser.js"; import * as krill from "../_snowpack/link/repl/krill-parser.js";
import * as strudel from "../_snowpack/link/strudel.js"; import * as strudel from "../_snowpack/link/strudel.js";
import {Scale, Note, Interval} from "../_snowpack/pkg/@tonaljs/tonal.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 {pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence} = strudel;
const applyOptions = (parent) => (pat, i) => { const applyOptions = (parent) => (pat, i) => {
const ast = parent.source_[i]; const ast = parent.source_[i];
@ -39,6 +40,7 @@ function resolveReplications(ast) {
{ {
type_: "element", type_: "element",
source_: child.source_, source_: child.source_,
location_: child.location_,
options_: { options_: {
operator: { operator: {
type_: "stretch", type_: "stretch",
@ -80,7 +82,15 @@ export function patternifyAST(ast) {
return silence; return silence;
} }
if (typeof ast.source_ !== "object") { 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_); return patternifyAST(ast.source_);
case "stretch": case "stretch":

View File

@ -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 import traverser from './shift-traverser/index.js'; // npm module does not work in the browser
const { replace } = traverser; 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 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 isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
const addLocations = true;
export const addMiniLocations = true;
export default (code) => { export default (code) => {
const ast = parseScript(code); const ast = parseScriptWithLocation(code);
const shifted = replace(ast, { const nodesWithLocation = [];
const parents = [];
const shifted = replace(ast.tree, {
enter(node, parent) { 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 (node.type === 'IdentifierExpression') {
if (isNote(node.name)) { if (isNote(node.name)) {
const value = node.name[1] === 's' ? node.name.replace('s', '#') : 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 }); return new LiteralStringExpression({ value });
} }
if (node.name === 'r') { if (node.name === 'r') {
return new IdentifierExpression({ name: 'silence' }); 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); 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) // TODO: turn x.groove['[~ x]*2'] into x.groove('[~ x]*2'.m)
// and ['c1*2'].xx into 'c1*2'.m.xx ?? // 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
View File

@ -2,7 +2,7 @@ import {Note, Interval, Scale} from "../_snowpack/pkg/@tonaljs/tonal.js";
import {Pattern as _Pattern} from "../_snowpack/link/strudel.js"; import {Pattern as _Pattern} from "../_snowpack/link/strudel.js";
const Pattern = _Pattern; const Pattern = _Pattern;
function toNoteEvent(event) { function toNoteEvent(event) {
if (typeof event === "string") { if (typeof event === "string" || typeof event === "number") {
return {value: event}; return {value: event};
} }
if (event.value) { if (event.value) {
@ -53,13 +53,20 @@ Pattern.prototype._mapNotes = function(func) {
Pattern.prototype._transpose = function(intervalOrSemitones) { Pattern.prototype._transpose = function(intervalOrSemitones) {
return this._mapNotes(({value, scale}) => { return this._mapNotes(({value, scale}) => {
const interval = !isNaN(Number(intervalOrSemitones)) ? Interval.fromSemitones(intervalOrSemitones) : String(intervalOrSemitones); 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}; return {value: Note.transpose(value, interval), scale};
}); });
}; };
Pattern.prototype._scaleTranspose = function(offset) { Pattern.prototype._scaleTranspose = function(offset) {
return this._mapNotes(({value, scale}) => { return this._mapNotes(({value, scale}) => {
if (!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}; return {value: scaleTranspose(scale, Number(offset), value), scale};
}); });

73
docs/dist/tunes.js vendored
View File

@ -4,11 +4,11 @@ export const timeCatMini = `stack(
'[eb4@5 [f4 eb4 d4]@3] [eb4 c4]/2'.mini.slow(8) '[eb4@5 [f4 eb4 d4]@3] [eb4 c4]/2'.mini.slow(8)
)`; )`;
export const timeCat = `stack( export const timeCat = `stack(
timeCat([3, c3], [1, stack(eb3, g3, m(c4, d4).slow(2))]), timeCat([3, c3], [1, stack(eb3, g3, cat(c4, d4).slow(2))]),
m(c2, g2), cat(c2, g2),
sequence( sequence(
timeCat([5, eb4], [3, m(f4, eb4, d4)]), timeCat([5, eb4], [3, cat(f4, eb4, d4)]),
m(eb4, c4).slow(2) cat(eb4, c4).slow(2)
).slow(4) ).slow(4)
)`; )`;
export const shapeShifted = `stack( export const shapeShifted = `stack(
@ -120,11 +120,11 @@ export const spanish = `slowcat(
stack(ab3,c4,eb4), stack(ab3,c4,eb4),
stack(g3,b3,d4) 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(4, fast(2))
.every(3, slow(1.5)) .every(3, slow(1.5))
.fast(slowcat(1.25, 1, 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( export const swimming = `stack(
mini( mini(
'~', '~',
@ -357,64 +357,3 @@ export const caverave = `() => {
synths synths
).slow(2); ).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);
`;

View File

@ -6,7 +6,7 @@ import usePostMessage from "./usePostMessage.js";
let s4 = () => { let s4 = () => {
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1); 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 id = useMemo(() => s4(), []);
const [code, setCode] = useState(tune); const [code, setCode] = useState(tune);
const [activeCode, setActiveCode] = useState(); const [activeCode, setActiveCode] = useState();
@ -30,6 +30,8 @@ function useRepl({tune, defaultSynth, autolink = true}) {
setError(void 0); setError(void 0);
setActiveCode(_code); setActiveCode(_code);
} catch (err) { } catch (err) {
err.message = "evaluation error: " + err.message;
console.warn(err);
setError(err); setError(err);
} }
}; };
@ -43,6 +45,7 @@ function useRepl({tune, defaultSynth, autolink = true}) {
const cycle = useCycle({ const cycle = useCycle({
onEvent: useCallback((time, event) => { onEvent: useCallback((time, event) => {
try { try {
onEvent?.(event);
if (!event.value?.onTrigger) { if (!event.value?.onTrigger) {
const note = event.value?.value || event.value; const note = event.value?.value || event.value;
if (!isNote(note)) { if (!isNote(note)) {
@ -62,11 +65,12 @@ function useRepl({tune, defaultSynth, autolink = true}) {
err.message = "unplayable event: " + err?.message; err.message = "unplayable event: " + err?.message;
pushLog(err.message); pushLog(err.message);
} }
}, []), }, [onEvent]),
onQuery: useCallback((span) => { onQuery: useCallback((span) => {
try { try {
return pattern?.query(span) || []; return pattern?.query(span) || [];
} catch (err) { } catch (err) {
err.message = "query error: " + err.message;
setError(err); setError(err);
return []; return [];
} }

View File

@ -18,7 +18,7 @@ Pattern.prototype.voicings = function(range) {
range = ["F3", "A4"]; range = ["F3", "A4"];
} }
return this.fmapNested((event) => { return this.fmapNested((event) => {
lastVoicing = getVoicing(event.value, lastVoicing, range); lastVoicing = getVoicing(event.value?.value || event.value, lastVoicing, range);
return stack(...lastVoicing); return stack(...lastVoicing);
}); });
}; };

View File

@ -41619,7 +41619,7 @@ var _usePostMessageDefault = parcelHelpers.interopDefault(_usePostMessage);
let s4 = ()=>{ let s4 = ()=>{
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1); 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 id = _react.useMemo(()=>s4()
, []); , []);
const [code, setCode] = _react.useState(tune); const [code, setCode] = _react.useState(tune);
@ -41646,6 +41646,8 @@ function useRepl({ tune , defaultSynth , autolink =true }) {
setError(undefined); setError(undefined);
setActiveCode(_code); setActiveCode(_code);
} catch (err) { } catch (err) {
err.message = 'evaluation error: ' + err.message;
console.warn(err);
setError(err); setError(err);
} }
}; };
@ -41661,6 +41663,7 @@ function useRepl({ tune , defaultSynth , autolink =true }) {
const cycle1 = _useCycleDefault.default({ const cycle1 = _useCycleDefault.default({
onEvent: _react.useCallback((time, event)=>{ onEvent: _react.useCallback((time, event)=>{
try { try {
onEvent?.(event);
if (!event.value?.onTrigger) { if (!event.value?.onTrigger) {
const note = event.value?.value || event.value; const note = event.value?.value || event.value;
if (!_tone.isNote(note)) throw new Error('not a note: ' + note); 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; err.message = 'unplayable event: ' + err?.message;
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
} }
}, []), }, [
onEvent
]),
onQuery: _react.useCallback((span)=>{ onQuery: _react.useCallback((span)=>{
try { try {
return pattern?.query(span) || []; return pattern?.query(span) || [];
} catch (err) { } catch (err) {
err.message = 'query error: ' + err.message;
setError(err); setError(err);
return []; return [];
} }
@ -41791,6 +41797,7 @@ hackLiteral(String, [
Object.assign(globalThis, bootstrapped, _tone1, _tone); Object.assign(globalThis, bootstrapped, _tone1, _tone);
const evaluate = (code)=>{ const evaluate = (code)=>{
const shapeshifted = _shapeshifterDefault.default(code); // transform syntactically correct js code to semantically usable code const shapeshifted = _shapeshifterDefault.default(code); // transform syntactically correct js code to semantically usable code
// console.log('shapeshifted', shapeshifted);
let evaluated = eval(shapeshifted); let evaluated = eval(shapeshifted);
if (typeof evaluated === 'function') evaluated = evaluated(); if (typeof evaluated === 'function') evaluated = evaluated();
const pattern = _parse.minify(evaluated); // eval and minify (if user entered a string) 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, "inv", ()=>inv
); );
parcelHelpers.export(exports, "withLocationOffset", ()=>withLocationOffset
);
var _fractionJs = require("fraction.js"); var _fractionJs = require("fraction.js");
var _fractionJsDefault = parcelHelpers.interopDefault(_fractionJs); var _fractionJsDefault = parcelHelpers.interopDefault(_fractionJs);
var _ramda = require("ramda"); // will remove this as soon as compose is implemented here 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)) 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) { withValue(func) {
// Returns a new pattern, with the function applied to the value of // Returns a new pattern, with the function applied to the value of
// each event. It has the alias 'fmap'. // each event. It has the alias 'fmap'.
@ -42273,7 +42296,14 @@ class Pattern {
_patternify(func) { _patternify(func) {
const pat = this; const pat = this;
const patterned = function(...args) { 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); const pat_arg = sequence(...args);
// arg.locations has to go somewhere..
return pat_arg.fmap((arg)=>func.call(pat, arg) return pat_arg.fmap((arg)=>func.call(pat, arg)
).outerJoin(); ).outerJoin();
}; };
@ -42688,6 +42718,34 @@ Pattern.prototype.bootstrap = ()=>{
})); }));
return bootstrapped; 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) { },{"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' 'A4'
]; ];
return this.fmapNested((event)=>{ return this.fmapNested((event)=>{
lastVoicing = getVoicing(event.value, lastVoicing, range); lastVoicing = getVoicing(event.value?.value || event.value, lastVoicing, range);
return _strudelMjs.stack(...lastVoicing); return _strudelMjs.stack(...lastVoicing);
}); });
}; };
@ -62414,7 +62472,7 @@ var _tonal = require("@tonaljs/tonal");
var _strudelMjs = require("../../strudel.mjs"); var _strudelMjs = require("../../strudel.mjs");
const Pattern = _strudelMjs.Pattern; const Pattern = _strudelMjs.Pattern;
function toNoteEvent(event) { function toNoteEvent(event) {
if (typeof event === 'string') return { if (typeof event === 'string' || typeof event === 'number') return {
value: event value: event
}; };
if (event.value) return event; if (event.value) return event;
@ -62464,6 +62522,12 @@ Pattern.prototype._mapNotes = function(func) {
Pattern.prototype._transpose = function(intervalOrSemitones) { Pattern.prototype._transpose = function(intervalOrSemitones) {
return this._mapNotes(({ value , scale })=>{ return this._mapNotes(({ value , scale })=>{
const interval = !isNaN(Number(intervalOrSemitones)) ? _tonal.Interval.fromSemitones(intervalOrSemitones) : String(intervalOrSemitones); 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 { return {
value: _tonal.Note.transpose(value, interval), value: _tonal.Note.transpose(value, interval),
scale scale
@ -62477,7 +62541,8 @@ Pattern.prototype._transpose = function(intervalOrSemitones) {
// or even `stack(c3).superimpose(transpose.slowcat(7, 5))` or // or even `stack(c3).superimpose(transpose.slowcat(7, 5))` or
Pattern.prototype._scaleTranspose = function(offset) { Pattern.prototype._scaleTranspose = function(offset) {
return this._mapNotes(({ value , scale })=>{ 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 { return {
value: scaleTranspose(scale, Number(offset), value), value: scaleTranspose(scale, Number(offset), value),
scale scale
@ -62532,23 +62597,63 @@ Pattern.prototype.define('groove', (groove, pat)=>pat.groove(groove)
},{"../../strudel.mjs":"ggZqJ"}],"67UCx":[function(require,module,exports) { },{"../../strudel.mjs":"ggZqJ"}],"67UCx":[function(require,module,exports) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js"); var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports); parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "addMiniLocations", ()=>addMiniLocations
);
var _indexJs = require("./shift-parser/index.js"); // npm module does not work in the browser 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 _shiftTraverser = require("./shift-traverser"); // npm module does not work in the browser
var _shiftTraverserDefault = parcelHelpers.interopDefault(_shiftTraverser); var _shiftTraverserDefault = parcelHelpers.interopDefault(_shiftTraverser);
var _shiftAst = require("shift-ast"); var _shiftAst = require("shift-ast");
var _shiftCodegen = require("shift-codegen"); var _shiftCodegen = require("shift-codegen");
var _shiftCodegenDefault = parcelHelpers.interopDefault(_shiftCodegen); var _shiftCodegenDefault = parcelHelpers.interopDefault(_shiftCodegen);
var _strudelMjs = require("../../strudel.mjs");
const { replace } = _shiftTraverserDefault.default; const { replace } = _shiftTraverserDefault.default;
const { Pattern } = _strudelMjs;
const isNote = (name)=>/^[a-gC-G][bs]?[0-9]$/.test(name) const isNote = (name)=>/^[a-gC-G][bs]?[0-9]$/.test(name)
; ;
const addLocations = true;
const addMiniLocations = true;
exports.default = (code)=>{ exports.default = (code)=>{
const ast = _indexJs.parseScript(code); const ast = _indexJs.parseScriptWithLocation(code);
const shifted = replace(ast, { const nodesWithLocation = [];
const parents = [];
const shifted = replace(ast.tree, {
enter (node, parent) { 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 (node.type === 'IdentifierExpression') {
if (isNote(node.name)) { if (isNote(node.name)) {
const value = node.name[1] === 's' ? node.name.replace('s', '#') : 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({ return new _shiftAst.LiteralStringExpression({
value value
}); });
@ -62557,14 +62662,205 @@ exports.default = (code)=>{
name: 'silence' 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); 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 ?? // 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`)
},{"./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"); var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports); parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "parseModule", ()=>parseModule parcelHelpers.export(exports, "parseModule", ()=>parseModule
@ -95594,6 +95890,7 @@ parcelHelpers.export(exports, "minify", ()=>minify
var _krillParser = require("../krill-parser"); var _krillParser = require("../krill-parser");
var _strudelMjs = require("../../strudel.mjs"); var _strudelMjs = require("../../strudel.mjs");
var _tonal = require("@tonaljs/tonal"); var _tonal = require("@tonaljs/tonal");
var _shapeshifter = require("./shapeshifter");
const { pure , Pattern , Fraction , stack , slowcat , sequence , timeCat , silence } = _strudelMjs; const { pure , Pattern , Fraction , stack , slowcat , sequence , timeCat , silence } = _strudelMjs;
const applyOptions = (parent)=>(pat, i)=>{ const applyOptions = (parent)=>(pat, i)=>{
const ast = parent.source_[i]; const ast = parent.source_[i];
@ -95639,6 +95936,7 @@ function resolveReplications(ast) {
{ {
type_: 'element', type_: 'element',
source_: child.source_, source_: child.source_,
location_: child.location_,
options_: { options_: {
operator: { operator: {
type_: 'stretch', type_: 'stretch',
@ -95680,7 +95978,21 @@ function patternifyAST(ast) {
return sequence(...children); return sequence(...children);
case 'element': case 'element':
if (ast.source_ === '~') return silence; 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_); return patternifyAST(ast.source_);
case 'stretch': case 'stretch':
return patternifyAST(ast.source_).slow(ast.arguments_.amount); return patternifyAST(ast.source_).slow(ast.arguments_.amount);
@ -95742,7 +96054,7 @@ function minify(thing) {
return reify(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"); var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports); parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "SyntaxError", ()=>peg$SyntaxError parcelHelpers.export(exports, "SyntaxError", ()=>peg$SyntaxError
@ -97609,6 +97921,7 @@ function peg$parse(input, options1) {
this.type_ = "element"; this.type_ = "element";
this.source_ = source; this.source_ = source;
this.options_ = options; this.options_ = options;
this.location_ = location1();
}; };
var CommandStub = function(name, options) { var CommandStub = function(name, options) {
this.type_ = "command"; this.type_ = "command";
@ -97727,16 +98040,18 @@ var _javascriptJs = require("codemirror/mode/javascript/javascript.js");
var _pegjsJs = require("codemirror/mode/pegjs/pegjs.js"); var _pegjsJs = require("codemirror/mode/pegjs/pegjs.js");
var _materialCss = require("codemirror/theme/material.css"); var _materialCss = require("codemirror/theme/material.css");
var _codemirrorCss = require("codemirror/lib/codemirror.css"); var _codemirrorCss = require("codemirror/lib/codemirror.css");
function CodeMirror({ value , onChange , options }) { function CodeMirror({ value , onChange , options , editorDidMount }) {
options = options || { options = options || {
mode: 'javascript', mode: 'javascript',
theme: 'material', theme: 'material',
lineNumbers: true lineNumbers: true,
styleSelectedText: true
}; };
return(/*#__PURE__*/ _jsxRuntime.jsx(_reactCodemirror2.Controlled, { return(/*#__PURE__*/ _jsxRuntime.jsx(_reactCodemirror2.Controlled, {
value: value, value: value,
options: options, options: options,
onBeforeChange: onChange onBeforeChange: onChange,
editorDidMount: editorDidMount
})); }));
} }
exports.default = CodeMirror; exports.default = CodeMirror;
@ -108958,4 +109273,4 @@ exports.default = cx;
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}]},["3uVTb"], "3uVTb", "parcelRequire94c2") },{"@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

View File

@ -11,6 +11,6 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
<noscript>You need to enable JavaScript to run this app.</noscript> <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> </body>
</html> </html>

View File

@ -1936,6 +1936,7 @@ function peg$parse(input, options) {
this.type_ = "element"; this.type_ = "element";
this.source_ = source; this.source_ = source;
this.options_ = options; this.options_ = options;
this.location_ = location();
} }
var CommandStub = function(name, options) var CommandStub = function(name, options)

View File

@ -24,6 +24,7 @@
this.type_ = "element"; this.type_ = "element";
this.source_ = source; this.source_ = source;
this.options_ = options; this.options_ = options;
this.location_ = location();
} }
var CommandStub = function(name, options) var CommandStub = function(name, options)

View File

@ -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 * as Tone from 'tone';
import CodeMirror from './CodeMirror'; import CodeMirror from './CodeMirror';
import cx from './cx'; import cx from './cx';
@ -35,9 +35,33 @@ function getRandomTune() {
const randomTune = getRandomTune(); const randomTune = getRandomTune();
function App() { 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({ const { setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog } = useRepl({
tune: decoded || randomTune, tune: decoded || randomTune,
defaultSynth, 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>(); const logBox = useRef<any>();
// scroll log box to bottom when log changes // 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')}> <div className={cx('h-full bg-[#2A3236]', error ? 'focus:ring-red-500' : 'focus:ring-slate-800')}>
<CodeMirror <CodeMirror
value={code} value={code}
editorDidMount={setEditor}
options={{ options={{
mode: 'javascript', mode: 'javascript',
theme: 'material', theme: 'material',
lineNumbers: true, lineNumbers: true,
styleSelectedText: true,
}} }}
onChange={(_: any, __: any, value: any) => setCode(value)} onChange={(_: any, __: any, value: any) => setCode(value)}
/> />

View File

@ -5,11 +5,12 @@ import 'codemirror/mode/pegjs/pegjs.js';
import 'codemirror/theme/material.css'; import 'codemirror/theme/material.css';
import 'codemirror/lib/codemirror.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 || { options = options || {
mode: 'javascript', mode: 'javascript',
theme: 'material', theme: 'material',
lineNumbers: true, lineNumbers: true,
styleSelectedText: true,
}; };
return <CodeMirror2 value={value} options={options} onBeforeChange={onChange} />; return <CodeMirror2 value={value} options={options} onBeforeChange={onChange} editorDidMount={editorDidMount} />;
} }

View File

@ -32,6 +32,7 @@ Object.assign(globalThis, bootstrapped, Tone, toneHelpers);
export const evaluate: any = (code: string) => { export const evaluate: any = (code: string) => {
const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code
// console.log('shapeshifted', shapeshifted);
let evaluated = eval(shapeshifted); let evaluated = eval(shapeshifted);
if (typeof evaluated === 'function') { if (typeof evaluated === 'function') {
evaluated = evaluated(); evaluated = evaluated();

View File

@ -1,6 +1,7 @@
import * as krill from '../krill-parser'; import * as krill from '../krill-parser';
import * as strudel from '../../strudel.mjs'; import * as strudel from '../../strudel.mjs';
import { Scale, Note, Interval } from '@tonaljs/tonal'; import { Scale, Note, Interval } from '@tonaljs/tonal';
import { addMiniLocations } from './shapeshifter';
const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence } = strudel; const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence } = strudel;
@ -49,6 +50,7 @@ function resolveReplications(ast) {
{ {
type_: 'element', type_: 'element',
source_: child.source_, source_: child.source_,
location_: child.location_,
options_: { options_: {
operator: { operator: {
type_: 'stretch', type_: 'stretch',
@ -91,7 +93,18 @@ export function patternifyAST(ast: any): any {
return silence; return silence;
} }
if (typeof ast.source_ !== 'object') { 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_); return patternifyAST(ast.source_);
case 'stretch': case 'stretch':

View File

@ -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 import traverser from './shift-traverser'; // npm module does not work in the browser
const { replace } = traverser; const { replace } = traverser;
import { LiteralStringExpression, IdentifierExpression } from 'shift-ast'; import { LiteralStringExpression, IdentifierExpression, CallExpression, StaticMemberExpression } from 'shift-ast';
import codegen from 'shift-codegen'; 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 isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
const addLocations = true;
export const addMiniLocations = true;
export default (code) => { export default (code) => {
const ast = parseScript(code); const ast = parseScriptWithLocation(code);
const shifted = replace(ast, { const nodesWithLocation = [];
const parents = [];
const shifted = replace(ast.tree, {
enter(node, parent) { 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 (node.type === 'IdentifierExpression') {
if (isNote(node.name)) { if (isNote(node.name)) {
const value = node.name[1] === 's' ? node.name.replace('s', '#') : 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 }); return new LiteralStringExpression({ value });
} }
if (node.name === 'r') { if (node.name === 'r') {
return new IdentifierExpression({ name: 'silence' }); 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); 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) // TODO: turn x.groove['[~ x]*2'] into x.groove('[~ x]*2'.m)
// and ['c1*2'].xx into 'c1*2'.m.xx ?? // 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`)

View File

@ -4,12 +4,12 @@ import { Pattern as _Pattern } from '../../strudel.mjs';
const Pattern = _Pattern as any; const Pattern = _Pattern as any;
export declare interface NoteEvent { export declare interface NoteEvent {
value: string; value: string | number;
scale?: string; scale?: string;
} }
function toNoteEvent(event: string | NoteEvent): NoteEvent { function toNoteEvent(event: string | NoteEvent): NoteEvent {
if (typeof event === 'string') { if (typeof event === 'string' || typeof event === 'number') {
return { value: event }; return { value: event };
} }
if (event.value) { if (event.value) {
@ -73,6 +73,10 @@ Pattern.prototype._transpose = function (intervalOrSemitones: string | number) {
const interval = !isNaN(Number(intervalOrSemitones)) const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones as number) ? Interval.fromSemitones(intervalOrSemitones as number)
: String(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 }; 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) { Pattern.prototype._scaleTranspose = function (offset: number | string) {
return this._mapNotes(({ value, scale }: NoteEvent) => { return this._mapNotes(({ value, scale }: NoteEvent) => {
if (!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 }; return { value: scaleTranspose(scale, Number(offset), value), scale };
}); });

View File

@ -5,11 +5,11 @@ export const timeCatMini = `stack(
)`; )`;
export const timeCat = `stack( export const timeCat = `stack(
timeCat([3, c3], [1, stack(eb3, g3, m(c4, d4).slow(2))]), timeCat([3, c3], [1, stack(eb3, g3, cat(c4, d4).slow(2))]),
m(c2, g2), cat(c2, g2),
sequence( sequence(
timeCat([5, eb4], [3, m(f4, eb4, d4)]), timeCat([5, eb4], [3, cat(f4, eb4, d4)]),
m(eb4, c4).slow(2) cat(eb4, c4).slow(2)
).slow(4) ).slow(4)
)`; )`;
@ -178,11 +178,11 @@ export const spanish = `slowcat(
stack(g3,b3,d4) 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(4, fast(2))
.every(3, slow(1.5)) .every(3, slow(1.5))
.fast(slowcat(1.25, 1, 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( export const swimming = `stack(
mini( mini(
@ -296,13 +296,6 @@ export const giantStepsReggae = `stack(
.groove('x ~'.m.fast(4*8)) .groove('x ~'.m.fast(4*8))
).slow(25)`; ).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( export const transposedChordsHacked = `stack(
'c2 eb2 g2'.mini, 'c2 eb2 g2'.mini,
'Cm7'.pure.voicings(['g2','c4']).slow(2) '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)) .scaleTranspose(sequence(0, -1, -2, -3).slow(4))
.transpose(sequence(0, 1).slow(16))`; .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( export const groove = `stack(
'c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]'.mini, '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) '[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']) .voicings(['G3','A4'])
).slow(4)`; ).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( export const magicSofa = `stack(
'<C^7 F^7 ~> <Dm7 G7 A7 ~>'.m '<C^7 F^7 ~> <Dm7 G7 A7 ~>'.m
.every(2, fast(2)) .every(2, fast(2))
@ -462,66 +440,3 @@ export const caverave = `() => {
synths synths
).slow(2); ).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);
`;

View File

@ -3,7 +3,6 @@ import type { ToneEventCallback } from 'tone';
import * as Tone from 'tone'; import * as Tone from 'tone';
import { TimeSpan } from '../../strudel.mjs'; import { TimeSpan } from '../../strudel.mjs';
import type { Hap } from './types'; import type { Hap } from './types';
import usePostMessage from './usePostMessage';
export declare interface UseCycleProps { export declare interface UseCycleProps {
onEvent: ToneEventCallback<any>; onEvent: ToneEventCallback<any>;

View File

@ -1,7 +1,6 @@
import { useCallback, useLayoutEffect, useState, useMemo, useEffect } from 'react'; import { useCallback, useState, useMemo } from 'react';
import { isNote } from 'tone'; import { isNote } from 'tone';
import { evaluate } from './evaluate'; import { evaluate } from './evaluate';
import { useWebMidi } from './midi';
import type { Pattern } from './types'; import type { Pattern } from './types';
import useCycle from './useCycle'; import useCycle from './useCycle';
import usePostMessage from './usePostMessage'; import usePostMessage from './usePostMessage';
@ -12,7 +11,7 @@ let s4 = () => {
.substring(1); .substring(1);
}; };
function useRepl({ tune, defaultSynth, autolink = true }) { function useRepl({ tune, defaultSynth, autolink = true, onEvent }: any) {
const id = useMemo(() => s4(), []); const id = useMemo(() => s4(), []);
const [code, setCode] = useState<string>(tune); const [code, setCode] = useState<string>(tune);
const [activeCode, setActiveCode] = useState<string>(); const [activeCode, setActiveCode] = useState<string>();
@ -36,6 +35,8 @@ function useRepl({ tune, defaultSynth, autolink = true }) {
setError(undefined); setError(undefined);
setActiveCode(_code); setActiveCode(_code);
} catch (err: any) { } catch (err: any) {
err.message = 'evaluation error: ' + err.message;
console.warn(err)
setError(err); setError(err);
} }
}; };
@ -48,35 +49,40 @@ function useRepl({ tune, defaultSynth, autolink = true }) {
}; };
// cycle hook to control scheduling // cycle hook to control scheduling
const cycle = useCycle({ const cycle = useCycle({
onEvent: useCallback((time, event) => { onEvent: useCallback(
try { (time, event) => {
if (!event.value?.onTrigger) { try {
const note = event.value?.value || event.value; onEvent?.(event);
if (!isNote(note)) { if (!event.value?.onTrigger) {
throw new Error('not a note: ' + note); const note = event.value?.value || event.value;
} if (!isNote(note)) {
if (defaultSynth) { throw new Error('not a note: ' + note);
defaultSynth.triggerAttackRelease(note, event.duration, time); }
} else { if (defaultSynth) {
throw new Error('no defaultSynth passed to useRepl.'); defaultSynth.triggerAttackRelease(note, event.duration, time);
} } else {
/* console.warn('no instrument chosen', event); throw new Error('no defaultSynth passed to useRepl.');
}
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */ throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
} else { } else {
const { onTrigger } = event.value; const { onTrigger } = event.value;
onTrigger(time, event); 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); [onEvent]
err.message = 'unplayable event: ' + err?.message; ),
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
}
}, []),
onQuery: useCallback( onQuery: useCallback(
(span) => { (span) => {
try { try {
return pattern?.query(span) || []; return pattern?.query(span) || [];
} catch (err: any) { } catch (err: any) {
err.message = 'query error: ' + err.message;
setError(err); setError(err);
return []; return [];
} }

View File

@ -32,7 +32,7 @@ Pattern.prototype.voicings = function (range) {
range = ['F3', 'A4']; range = ['F3', 'A4'];
} }
return this.fmapNested((event) => { return this.fmapNested((event) => {
lastVoicing = getVoicing(event.value, lastVoicing, range); lastVoicing = getVoicing(event.value?.value || event.value, lastVoicing, range);
return stack(...lastVoicing); return stack(...lastVoicing);
}); });
}; };

View File

@ -148,7 +148,7 @@ class TimeSpan {
return result return result
} }
get midpoint() { midpoint() {
return(this.begin.add((this.end.sub(this.begin)).div(Fraction(2)))) 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))) 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) { withValue(func) {
// Returns a new pattern, with the function applied to the value of // Returns a new pattern, with the function applied to the value of
// each event. It has the alias 'fmap'. // each event. It has the alias 'fmap'.
@ -478,7 +486,14 @@ class Pattern {
_patternify(func) { _patternify(func) {
const pat = this const pat = this
const patterned = function (...args) { 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) const pat_arg = sequence(...args)
// arg.locations has to go somewhere..
return pat_arg.fmap(arg => func.call(pat,arg)).outerJoin() return pat_arg.fmap(arg => func.call(pat,arg)).outerJoin()
} }
return patterned return patterned
@ -680,6 +695,32 @@ function steady(value) {
return new Pattern(span => Hap(undefined, span, 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) { function reify(thing) {
// Tunrs something into a pattern, unless it's already a pattern // Tunrs something into a pattern, unless it's already a pattern
if (thing?.constructor?.name == "Pattern") { if (thing?.constructor?.name == "Pattern") {
@ -882,10 +923,34 @@ Pattern.prototype.bootstrap = () => {
return bootstrapped; 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, export {Fraction, TimeSpan, Hap, Pattern,
pure, stack, slowcat, fastcat, cat, timeCat, sequence, polymeter, pm, polyrhythm, pr, reify, silence, pure, stack, slowcat, fastcat, cat, timeCat, sequence, polymeter, pm, polyrhythm, pr, reify, silence,
fast, slow, early, late, rev, fast, slow, early, late, rev,
add, sub, mul, div, union, every, when, off, jux, append, superimpose, add, sub, mul, div, union, every, when, off, jux, append, superimpose,
struct, mask, invert, inv struct, mask, invert, inv,
withLocationOffset
} }

View File

@ -2,7 +2,7 @@ import Fraction from 'fraction.js'
import { strict as assert } from 'assert'; 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 { Time } from 'tone';
import pkg from 'tone'; import pkg from 'tone';
const { Time } = pkg; 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
)
})
})
}) })