This commit is contained in:
Felix Roos 2022-02-20 20:31:55 +01:00
parent 1de371e944
commit f8df4b485d
9 changed files with 397 additions and 95 deletions

View File

@ -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();
};

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 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", {

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/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
});
}

View File

@ -1,30 +1,189 @@
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 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 isPatternArg = (parent) =>
parent?.type === 'CallExpression' && Object.keys(Pattern.prototype.factories).includes(parent.callee.name);
const isTimeCat = parent?.type === 'ArrayExpression' && isPatternArg(grandparent);
const isMarkable = isPatternArg(parent) || isTimeCat;
// 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 addPureWithLocation(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 addPureWithLocation(node.value, node, ast.locations, nodesWithLocation);
}
return node;
},
leave() {
parents.pop();
},
});
return codegen(shifted);
};
// turns node in pure(value).withLocation(location), where location is the node's location in the source code
// with this, the pure pattern can pass its location to the event, to know where to highlight when it's active
function addPureWithLocation(value, node, locations, nodesWithLocation) {
// console.log('addPure', value, node);
const withLocation = new CallExpression({
callee: new StaticMemberExpression({
object: new CallExpression({
callee: new IdentifierExpression({
name: 'pure',
}),
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(
`pure(${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`)

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)
)`;
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);
`;

View File

@ -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();
@ -43,6 +43,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,7 +63,7 @@ function useRepl({tune, defaultSynth, autolink = true}) {
err.message = "unplayable event: " + err?.message;
pushLog(err.message);
}
}, []),
}, [onEvent]),
onQuery: useCallback((span) => {
try {
return pattern?.query(span) || [];

View File

@ -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);
@ -41661,6 +41661,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,7 +41677,9 @@ 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) || [];
@ -42107,6 +42110,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 +42290,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();
};
@ -62538,17 +62562,32 @@ 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;
exports.default = (code)=>{
const ast = _indexJs.parseScript(code);
const shifted = replace(ast, {
enter (node, parent) {
// replace identifiers that are a note with a note string
const ast = _indexJs.parseScriptWithLocation(code);
const nodesWithLocation = [];
const parents = [];
const shifted = replace(ast.tree, {
enter (node, parent1) {
parents.push(parent1);
const isSynthetic = parents.some((p)=>nodesWithLocation.includes(p)
);
if (isSynthetic) return node;
const grandparent = parents[parents.length - 2];
const isPatternArg = (parent)=>parent?.type === 'CallExpression' && Object.keys(Pattern.prototype.factories).includes(parent.callee.name)
;
const isTimeCat = parent1?.type === 'ArrayExpression' && isPatternArg(grandparent);
const isMarkable = isPatternArg(parent1) || isTimeCat;
// 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 addPureWithLocation(value, node, ast.locations, nodesWithLocation);
return new _shiftAst.LiteralStringExpression({
value
});
@ -62557,14 +62596,152 @@ exports.default = (code)=>{
name: 'silence'
});
}
if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) // console.log('add', node);
return addPureWithLocation(node.value, node, ast.locations, nodesWithLocation);
return node;
},
leave () {
parents.pop();
}
});
return _shiftCodegenDefault.default(shifted);
}; // TODO: turn x.groove['[~ x]*2'] into x.groove('[~ x]*2'.m)
};
// turns node in pure(value).withLocation(location), where location is the node's location in the source code
// with this, the pure pattern can pass its location to the event, to know where to highlight when it's active
function addPureWithLocation(value, node, locations, nodesWithLocation) {
// console.log('addPure', value, node);
const withLocation = new _shiftAst.CallExpression({
callee: new _shiftAst.StaticMemberExpression({
object: new _shiftAst.CallExpression({
callee: new _shiftAst.IdentifierExpression({
name: 'pure'
}),
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(
`pure(${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
@ -97727,16 +97904,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 +109137,4 @@ exports.default = cx;
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}]},["3uVTb"], "3uVTb", "parcelRequire94c2")
//# sourceMappingURL=index.dc15e374.js.map
//# sourceMappingURL=index.98344030.js.map

File diff suppressed because one or more lines are too long

View File

@ -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.98344030.js" defer=""></script>
</body>
</html>