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.source_ = source;
this.options_ = options;
this.location_ = location();
}
var CommandStub = function(name, options)

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();
};
@ -596,6 +604,28 @@ Pattern.prototype.bootstrap = () => {
}));
return bootstrapped;
};
function withLocationOffset(pat, offset) {
return pat.fmap((value) => {
value = typeof value === "object" && !Array.isArray(value) ? value : {value};
let locations = value.locations || [];
locations = locations.map(({start, end}) => {
const colOffset = start.line === 1 ? offset.start.column : 0;
return {
start: {
...start,
line: start.line - 1 + (offset.start.line - 1) + 1,
column: start.column - 1 + colOffset
},
end: {
...end,
line: end.line - 1 + (offset.start.line - 1) + 1,
column: end.column - 1 + colOffset
}
};
});
return {...value, locations};
});
}
export {
Fraction,
TimeSpan,
@ -633,5 +663,6 @@ export {
struct,
mask,
invert,
inv
inv,
withLocationOffset
};

20
docs/dist/App.js vendored
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
});
}

12
docs/dist/parse.js vendored
View File

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

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
const { replace } = traverser;
import { LiteralStringExpression, IdentifierExpression } from '../_snowpack/pkg/shift-ast.js';
import { LiteralStringExpression, IdentifierExpression, CallExpression, StaticMemberExpression } from '../_snowpack/pkg/shift-ast.js';
import codegen from '../_snowpack/pkg/shift-codegen.js';
import * as strudel from '../_snowpack/link/strudel.js';
const { Pattern } = strudel;
const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
const addLocations = true;
export const addMiniLocations = true;
export default (code) => {
const ast = parseScript(code);
const shifted = replace(ast, {
const ast = parseScriptWithLocation(code);
const nodesWithLocation = [];
const parents = [];
const shifted = replace(ast.tree, {
enter(node, parent) {
// replace identifiers that are a note with a note string
parents.push(parent);
const isSynthetic = parents.some((p) => nodesWithLocation.includes(p));
if (isSynthetic) {
return node;
}
const grandparent = parents[parents.length - 2];
const isTimeCat = parent?.type === 'ArrayExpression' && isPatternFactory(grandparent);
const isMarkable = isPatternFactory(parent) || isTimeCat;
// operator overloading => still not done
const operators = {
'*': 'fast',
'/': 'slow',
'&': 'stack',
'&&': 'append',
};
if (
node.type === 'BinaryExpression' &&
operators[node.operator] &&
['LiteralNumericExpression', 'LiteralStringExpression', 'IdentifierExpression'].includes(node.right?.type) &&
canBeOverloaded(node.left)
) {
let arg = node.left;
if (node.left.type === 'IdentifierExpression') {
arg = wrapReify(node.left);
}
return new CallExpression({
callee: new StaticMemberExpression({
property: operators[node.operator],
object: wrapReify(arg),
}),
arguments: [node.right],
});
}
// replace pseudo note variables
if (node.type === 'IdentifierExpression') {
if (isNote(node.name)) {
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
if (addLocations && isMarkable) {
return reifyWithLocation(value, node, ast.locations, nodesWithLocation);
}
return new LiteralStringExpression({ value });
}
if (node.name === 'r') {
return new IdentifierExpression({ name: 'silence' });
}
}
if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) {
// console.log('add', node);
return reifyWithLocation(node.value, node, ast.locations, nodesWithLocation);
}
if (!addMiniLocations) {
return node;
}
// mini notation location handling
const miniFunctions = ['mini', 'm'];
const isAlreadyWrapped = parent?.type === 'CallExpression' && parent.callee.name === 'withLocationOffset';
if (node.type === 'CallExpression' && miniFunctions.includes(node.callee.name) && !isAlreadyWrapped) {
// mini('c3')
if (node.arguments.length > 1) {
// TODO: transform mini(...args) to cat(...args.map(mini)) ?
console.warn('multi arg mini locations not supported yet...');
return node;
}
return wrapLocationOffset(node, node.arguments, ast.locations, nodesWithLocation);
}
if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property) && !isAlreadyWrapped) {
// 'c3'.mini or 'c3'.m
return wrapLocationOffset(node, node.object, ast.locations, nodesWithLocation);
}
return node;
},
leave() {
parents.pop();
},
});
return codegen(shifted);
};
function wrapReify(node) {
return new CallExpression({
callee: new IdentifierExpression({
name: 'reify',
}),
arguments: [node],
});
}
function isPatternFactory(node) {
return node?.type === 'CallExpression' && Object.keys(Pattern.prototype.factories).includes(node.callee.name);
}
function canBeOverloaded(node) {
return (node.type === 'IdentifierExpression' && isNote(node.name)) || isPatternFactory(node);
// TODO: support sequence(c3).transpose(3).x.y.z
}
// turn node into withLocationOffset(node, location)
function wrapLocationOffset(node, stringNode, locations, nodesWithLocation) {
// console.log('wrapppp', stringNode);
const expression = {
type: 'CallExpression',
callee: {
type: 'IdentifierExpression',
name: 'withLocationOffset',
},
arguments: [node, getLocationObject(stringNode, locations)],
};
nodesWithLocation.push(expression);
// console.log('wrapped', codegen(expression));
return expression;
}
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
function reifyWithLocation(value, node, locations, nodesWithLocation) {
// console.log('reifyWithLocation', value, node);
const withLocation = new CallExpression({
callee: new StaticMemberExpression({
object: new CallExpression({
callee: new IdentifierExpression({
name: 'reify',
}),
arguments: [new LiteralStringExpression({ value })],
}),
property: 'withLocation',
}),
arguments: [getLocationObject(node, locations)],
});
nodesWithLocation.push(withLocation);
return withLocation;
}
// returns ast for source location object
function getLocationObject(node, locations) {
/*const locationAST = parseScript(
"x=" + JSON.stringify(ast.locations.get(node))
).statements[0].expression.expression;
console.log("locationAST", locationAST);*/
/*const callAST = parseScript(
`reify(${node.name}).withLocation(${JSON.stringify(
ast.locations.get(node)
)})`
).statements[0].expression;*/
const loc = locations.get(node);
return {
type: 'ObjectExpression',
properties: [
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'start',
},
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'line',
},
expression: {
type: 'LiteralNumericExpression',
value: loc.start.line,
},
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'column',
},
expression: {
type: 'LiteralNumericExpression',
value: loc.start.column,
},
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'offset',
},
expression: {
type: 'LiteralNumericExpression',
value: loc.start.offset,
},
},
],
},
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'end',
},
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'line',
},
expression: {
type: 'LiteralNumericExpression',
value: loc.end.line,
},
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'column',
},
expression: {
type: 'LiteralNumericExpression',
value: loc.end.column,
},
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'offset',
},
expression: {
type: 'LiteralNumericExpression',
value: loc.end.offset,
},
},
],
},
},
],
};
}
// TODO: turn x.groove['[~ x]*2'] into x.groove('[~ x]*2'.m)
// and ['c1*2'].xx into 'c1*2'.m.xx ??
// or just all templated strings?? x.groove(`[~ x]*2`)
// or just all templated strings?? x.groove(`[~ x]*2`)

11
docs/dist/tonal.js vendored
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";
const Pattern = _Pattern;
function toNoteEvent(event) {
if (typeof event === "string") {
if (typeof event === "string" || typeof event === "number") {
return {value: event};
}
if (event.value) {
@ -53,13 +53,20 @@ Pattern.prototype._mapNotes = function(func) {
Pattern.prototype._transpose = function(intervalOrSemitones) {
return this._mapNotes(({value, scale}) => {
const interval = !isNaN(Number(intervalOrSemitones)) ? Interval.fromSemitones(intervalOrSemitones) : String(intervalOrSemitones);
if (typeof value === "number") {
const semitones = typeof interval === "string" ? Interval.semitones(interval) || 0 : interval;
return {value: value + semitones};
}
return {value: Note.transpose(value, interval), scale};
});
};
Pattern.prototype._scaleTranspose = function(offset) {
return this._mapNotes(({value, scale}) => {
if (!scale) {
throw new Error("can only use scaleOffset after .scale");
throw new Error("can only use scaleTranspose after .scale");
}
if (typeof value !== "string") {
throw new Error("can only use scaleTranspose with notes");
}
return {value: scaleTranspose(scale, Number(offset), value), scale};
});

73
docs/dist/tunes.js vendored
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();
@ -30,6 +30,8 @@ function useRepl({tune, defaultSynth, autolink = true}) {
setError(void 0);
setActiveCode(_code);
} catch (err) {
err.message = "evaluation error: " + err.message;
console.warn(err);
setError(err);
}
};
@ -43,6 +45,7 @@ function useRepl({tune, defaultSynth, autolink = true}) {
const cycle = useCycle({
onEvent: useCallback((time, event) => {
try {
onEvent?.(event);
if (!event.value?.onTrigger) {
const note = event.value?.value || event.value;
if (!isNote(note)) {
@ -62,11 +65,12 @@ function useRepl({tune, defaultSynth, autolink = true}) {
err.message = "unplayable event: " + err?.message;
pushLog(err.message);
}
}, []),
}, [onEvent]),
onQuery: useCallback((span) => {
try {
return pattern?.query(span) || [];
} catch (err) {
err.message = "query error: " + err.message;
setError(err);
return [];
}

View File

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

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);
@ -41646,6 +41646,8 @@ function useRepl({ tune , defaultSynth , autolink =true }) {
setError(undefined);
setActiveCode(_code);
} catch (err) {
err.message = 'evaluation error: ' + err.message;
console.warn(err);
setError(err);
}
};
@ -41661,6 +41663,7 @@ function useRepl({ tune , defaultSynth , autolink =true }) {
const cycle1 = _useCycleDefault.default({
onEvent: _react.useCallback((time, event)=>{
try {
onEvent?.(event);
if (!event.value?.onTrigger) {
const note = event.value?.value || event.value;
if (!_tone.isNote(note)) throw new Error('not a note: ' + note);
@ -41676,11 +41679,14 @@ function useRepl({ tune , defaultSynth , autolink =true }) {
err.message = 'unplayable event: ' + err?.message;
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
}
}, []),
}, [
onEvent
]),
onQuery: _react.useCallback((span)=>{
try {
return pattern?.query(span) || [];
} catch (err) {
err.message = 'query error: ' + err.message;
setError(err);
return [];
}
@ -41791,6 +41797,7 @@ hackLiteral(String, [
Object.assign(globalThis, bootstrapped, _tone1, _tone);
const evaluate = (code)=>{
const shapeshifted = _shapeshifterDefault.default(code); // transform syntactically correct js code to semantically usable code
// console.log('shapeshifted', shapeshifted);
let evaluated = eval(shapeshifted);
if (typeof evaluated === 'function') evaluated = evaluated();
const pattern = _parse.minify(evaluated); // eval and minify (if user entered a string)
@ -41887,6 +41894,8 @@ parcelHelpers.export(exports, "invert", ()=>invert
);
parcelHelpers.export(exports, "inv", ()=>inv
);
parcelHelpers.export(exports, "withLocationOffset", ()=>withLocationOffset
);
var _fractionJs = require("fraction.js");
var _fractionJsDefault = parcelHelpers.interopDefault(_fractionJs);
var _ramda = require("ramda"); // will remove this as soon as compose is implemented here
@ -42107,6 +42116,20 @@ class Pattern {
return new Pattern((span)=>func(this.query(span))
);
}
withLocation(location) {
return this.fmap((value)=>{
value = typeof value === 'object' && !Array.isArray(value) ? value : {
value
};
const locations = (value.locations || []).concat([
location
]);
return {
...value,
locations
};
});
}
withValue(func) {
// Returns a new pattern, with the function applied to the value of
// each event. It has the alias 'fmap'.
@ -42273,7 +42296,14 @@ class Pattern {
_patternify(func) {
const pat = this;
const patterned = function(...args) {
// the problem here: args could a pattern that has been turned into an object to add location
// to avoid object checking for every pattern method, we can remove it here...
// in the future, patternified args should be marked as well + some better object handling
args = args.map((arg)=>arg.constructor?.name === 'Pattern' ? arg.fmap((value)=>value.value || value
) : arg
);
const pat_arg = sequence(...args);
// arg.locations has to go somewhere..
return pat_arg.fmap((arg)=>func.call(pat, arg)
).outerJoin();
};
@ -42688,6 +42718,34 @@ Pattern.prototype.bootstrap = ()=>{
}));
return bootstrapped;
};
// this is wrapped around mini patterns to offset krill parser location into the global js code space
function withLocationOffset(pat, offset) {
return pat.fmap((value)=>{
value = typeof value === 'object' && !Array.isArray(value) ? value : {
value
};
let locations = value.locations || [];
locations = locations.map(({ start , end })=>{
const colOffset = start.line === 1 ? offset.start.column : 0;
return {
start: {
...start,
line: start.line - 1 + (offset.start.line - 1) + 1,
column: start.column - 1 + colOffset
},
end: {
...end,
line: end.line - 1 + (offset.start.line - 1) + 1,
column: end.column - 1 + colOffset
}
};
});
return {
...value,
locations
};
});
}
},{"fraction.js":"1Q5M2","ramda":"10uzi","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"1Q5M2":[function(require,module,exports) {
/**
@ -57782,7 +57840,7 @@ Pattern.prototype.voicings = function(range) {
'A4'
];
return this.fmapNested((event)=>{
lastVoicing = getVoicing(event.value, lastVoicing, range);
lastVoicing = getVoicing(event.value?.value || event.value, lastVoicing, range);
return _strudelMjs.stack(...lastVoicing);
});
};
@ -62414,7 +62472,7 @@ var _tonal = require("@tonaljs/tonal");
var _strudelMjs = require("../../strudel.mjs");
const Pattern = _strudelMjs.Pattern;
function toNoteEvent(event) {
if (typeof event === 'string') return {
if (typeof event === 'string' || typeof event === 'number') return {
value: event
};
if (event.value) return event;
@ -62464,6 +62522,12 @@ Pattern.prototype._mapNotes = function(func) {
Pattern.prototype._transpose = function(intervalOrSemitones) {
return this._mapNotes(({ value , scale })=>{
const interval = !isNaN(Number(intervalOrSemitones)) ? _tonal.Interval.fromSemitones(intervalOrSemitones) : String(intervalOrSemitones);
if (typeof value === 'number') {
const semitones = typeof interval === 'string' ? _tonal.Interval.semitones(interval) || 0 : interval;
return {
value: value + semitones
};
}
return {
value: _tonal.Note.transpose(value, interval),
scale
@ -62477,7 +62541,8 @@ Pattern.prototype._transpose = function(intervalOrSemitones) {
// or even `stack(c3).superimpose(transpose.slowcat(7, 5))` or
Pattern.prototype._scaleTranspose = function(offset) {
return this._mapNotes(({ value , scale })=>{
if (!scale) throw new Error('can only use scaleOffset after .scale');
if (!scale) throw new Error('can only use scaleTranspose after .scale');
if (typeof value !== 'string') throw new Error('can only use scaleTranspose with notes');
return {
value: scaleTranspose(scale, Number(offset), value),
scale
@ -62532,23 +62597,63 @@ Pattern.prototype.define('groove', (groove, pat)=>pat.groove(groove)
},{"../../strudel.mjs":"ggZqJ"}],"67UCx":[function(require,module,exports) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "addMiniLocations", ()=>addMiniLocations
);
var _indexJs = require("./shift-parser/index.js"); // npm module does not work in the browser
var _shiftTraverser = require("./shift-traverser"); // npm module does not work in the browser
var _shiftTraverserDefault = parcelHelpers.interopDefault(_shiftTraverser);
var _shiftAst = require("shift-ast");
var _shiftCodegen = require("shift-codegen");
var _shiftCodegenDefault = parcelHelpers.interopDefault(_shiftCodegen);
var _strudelMjs = require("../../strudel.mjs");
const { replace } = _shiftTraverserDefault.default;
const { Pattern } = _strudelMjs;
const isNote = (name)=>/^[a-gC-G][bs]?[0-9]$/.test(name)
;
const addLocations = true;
const addMiniLocations = true;
exports.default = (code)=>{
const ast = _indexJs.parseScript(code);
const shifted = replace(ast, {
const ast = _indexJs.parseScriptWithLocation(code);
const nodesWithLocation = [];
const parents = [];
const shifted = replace(ast.tree, {
enter (node, parent) {
// replace identifiers that are a note with a note string
parents.push(parent);
const isSynthetic = parents.some((p)=>nodesWithLocation.includes(p)
);
if (isSynthetic) return node;
const grandparent = parents[parents.length - 2];
const isTimeCat = parent?.type === 'ArrayExpression' && isPatternFactory(grandparent);
const isMarkable = isPatternFactory(parent) || isTimeCat;
// operator overloading => still not done
const operators = {
'*': 'fast',
'/': 'slow',
'&': 'stack',
'&&': 'append'
};
if (node.type === 'BinaryExpression' && operators[node.operator] && [
'LiteralNumericExpression',
'LiteralStringExpression',
'IdentifierExpression'
].includes(node.right?.type) && canBeOverloaded(node.left)) {
let arg = node.left;
if (node.left.type === 'IdentifierExpression') arg = wrapReify(node.left);
return new _shiftAst.CallExpression({
callee: new _shiftAst.StaticMemberExpression({
property: operators[node.operator],
object: wrapReify(arg)
}),
arguments: [
node.right
]
});
}
// replace pseudo note variables
if (node.type === 'IdentifierExpression') {
if (isNote(node.name)) {
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
if (addLocations && isMarkable) return reifyWithLocation(value, node, ast.locations, nodesWithLocation);
return new _shiftAst.LiteralStringExpression({
value
});
@ -62557,14 +62662,205 @@ exports.default = (code)=>{
name: 'silence'
});
}
if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) // console.log('add', node);
return reifyWithLocation(node.value, node, ast.locations, nodesWithLocation);
if (!addMiniLocations) return node;
// mini notation location handling
const miniFunctions = [
'mini',
'm'
];
const isAlreadyWrapped = parent?.type === 'CallExpression' && parent.callee.name === 'withLocationOffset';
if (node.type === 'CallExpression' && miniFunctions.includes(node.callee.name) && !isAlreadyWrapped) {
// mini('c3')
if (node.arguments.length > 1) {
// TODO: transform mini(...args) to cat(...args.map(mini)) ?
console.warn('multi arg mini locations not supported yet...');
return node;
}
return wrapLocationOffset(node, node.arguments, ast.locations, nodesWithLocation);
}
if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property) && !isAlreadyWrapped) // 'c3'.mini or 'c3'.m
return wrapLocationOffset(node, node.object, ast.locations, nodesWithLocation);
return node;
},
leave () {
parents.pop();
}
});
return _shiftCodegenDefault.default(shifted);
}; // TODO: turn x.groove['[~ x]*2'] into x.groove('[~ x]*2'.m)
};
function wrapReify(node) {
return new _shiftAst.CallExpression({
callee: new _shiftAst.IdentifierExpression({
name: 'reify'
}),
arguments: [
node
]
});
}
function isPatternFactory(node) {
return node?.type === 'CallExpression' && Object.keys(Pattern.prototype.factories).includes(node.callee.name);
}
function canBeOverloaded(node) {
return node.type === 'IdentifierExpression' && isNote(node.name) || isPatternFactory(node);
// TODO: support sequence(c3).transpose(3).x.y.z
}
// turn node into withLocationOffset(node, location)
function wrapLocationOffset(node, stringNode, locations, nodesWithLocation) {
// console.log('wrapppp', stringNode);
const expression = {
type: 'CallExpression',
callee: {
type: 'IdentifierExpression',
name: 'withLocationOffset'
},
arguments: [
node,
getLocationObject(stringNode, locations)
]
};
nodesWithLocation.push(expression);
// console.log('wrapped', codegen(expression));
return expression;
}
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
function reifyWithLocation(value, node, locations, nodesWithLocation) {
// console.log('reifyWithLocation', value, node);
const withLocation = new _shiftAst.CallExpression({
callee: new _shiftAst.StaticMemberExpression({
object: new _shiftAst.CallExpression({
callee: new _shiftAst.IdentifierExpression({
name: 'reify'
}),
arguments: [
new _shiftAst.LiteralStringExpression({
value
})
]
}),
property: 'withLocation'
}),
arguments: [
getLocationObject(node, locations)
]
});
nodesWithLocation.push(withLocation);
return withLocation;
}
// returns ast for source location object
function getLocationObject(node, locations) {
/*const locationAST = parseScript(
"x=" + JSON.stringify(ast.locations.get(node))
).statements[0].expression.expression;
console.log("locationAST", locationAST);*/ /*const callAST = parseScript(
`reify(${node.name}).withLocation(${JSON.stringify(
ast.locations.get(node)
)})`
).statements[0].expression;*/ const loc = locations.get(node);
return {
type: 'ObjectExpression',
properties: [
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'start'
},
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'line'
},
expression: {
type: 'LiteralNumericExpression',
value: loc.start.line
}
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'column'
},
expression: {
type: 'LiteralNumericExpression',
value: loc.start.column
}
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'offset'
},
expression: {
type: 'LiteralNumericExpression',
value: loc.start.offset
}
},
]
}
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'end'
},
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'line'
},
expression: {
type: 'LiteralNumericExpression',
value: loc.end.line
}
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'column'
},
expression: {
type: 'LiteralNumericExpression',
value: loc.end.column
}
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'offset'
},
expression: {
type: 'LiteralNumericExpression',
value: loc.end.offset
}
},
]
}
},
]
};
} // TODO: turn x.groove['[~ x]*2'] into x.groove('[~ x]*2'.m)
// and ['c1*2'].xx into 'c1*2'.m.xx ??
// or just all templated strings?? x.groove(`[~ x]*2`)
},{"./shift-parser/index.js":"1kFzJ","./shift-traverser":"bogJs","shift-ast":"ig2Ca","shift-codegen":"1GOrI","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"1kFzJ":[function(require,module,exports) {
},{"./shift-parser/index.js":"1kFzJ","./shift-traverser":"bogJs","shift-ast":"ig2Ca","shift-codegen":"1GOrI","../../strudel.mjs":"ggZqJ","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"1kFzJ":[function(require,module,exports) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "parseModule", ()=>parseModule
@ -95594,6 +95890,7 @@ parcelHelpers.export(exports, "minify", ()=>minify
var _krillParser = require("../krill-parser");
var _strudelMjs = require("../../strudel.mjs");
var _tonal = require("@tonaljs/tonal");
var _shapeshifter = require("./shapeshifter");
const { pure , Pattern , Fraction , stack , slowcat , sequence , timeCat , silence } = _strudelMjs;
const applyOptions = (parent)=>(pat, i)=>{
const ast = parent.source_[i];
@ -95639,6 +95936,7 @@ function resolveReplications(ast) {
{
type_: 'element',
source_: child.source_,
location_: child.location_,
options_: {
operator: {
type_: 'stretch',
@ -95680,7 +95978,21 @@ function patternifyAST(ast) {
return sequence(...children);
case 'element':
if (ast.source_ === '~') return silence;
if (typeof ast.source_ !== 'object') return ast.source_;
if (typeof ast.source_ !== 'object') {
if (!_shapeshifter.addMiniLocations) return ast.source_;
if (!ast.location_) {
console.warn('no location for', ast);
return ast.source_;
}
const { start , end } = ast.location_;
// return ast.source_;
// the following line expects the shapeshifter to wrap this in withLocationOffset
// because location_ is only relative to the mini string, but we need it relative to whole code
return pure(ast.source_).withLocation({
start,
end
});
}
return patternifyAST(ast.source_);
case 'stretch':
return patternifyAST(ast.source_).slow(ast.arguments_.amount);
@ -95742,7 +96054,7 @@ function minify(thing) {
return reify(thing);
}
},{"../krill-parser":"l2lgS","../../strudel.mjs":"ggZqJ","@tonaljs/tonal":"4q9Lu","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"l2lgS":[function(require,module,exports) {
},{"../krill-parser":"l2lgS","../../strudel.mjs":"ggZqJ","@tonaljs/tonal":"4q9Lu","./shapeshifter":"67UCx","@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}],"l2lgS":[function(require,module,exports) {
var parcelHelpers = require("@parcel/transformer-js/src/esmodule-helpers.js");
parcelHelpers.defineInteropFlag(exports);
parcelHelpers.export(exports, "SyntaxError", ()=>peg$SyntaxError
@ -97609,6 +97921,7 @@ function peg$parse(input, options1) {
this.type_ = "element";
this.source_ = source;
this.options_ = options;
this.location_ = location1();
};
var CommandStub = function(name, options) {
this.type_ = "command";
@ -97727,16 +98040,18 @@ var _javascriptJs = require("codemirror/mode/javascript/javascript.js");
var _pegjsJs = require("codemirror/mode/pegjs/pegjs.js");
var _materialCss = require("codemirror/theme/material.css");
var _codemirrorCss = require("codemirror/lib/codemirror.css");
function CodeMirror({ value , onChange , options }) {
function CodeMirror({ value , onChange , options , editorDidMount }) {
options = options || {
mode: 'javascript',
theme: 'material',
lineNumbers: true
lineNumbers: true,
styleSelectedText: true
};
return(/*#__PURE__*/ _jsxRuntime.jsx(_reactCodemirror2.Controlled, {
value: value,
options: options,
onBeforeChange: onChange
onBeforeChange: onChange,
editorDidMount: editorDidMount
}));
}
exports.default = CodeMirror;
@ -108958,4 +109273,4 @@ exports.default = cx;
},{"@parcel/transformer-js/src/esmodule-helpers.js":"gkKU3"}]},["3uVTb"], "3uVTb", "parcelRequire94c2")
//# sourceMappingURL=index.dc15e374.js.map
//# sourceMappingURL=index.a96519ca.js.map

File diff suppressed because one or more lines are too long

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

View File

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

View File

@ -24,6 +24,7 @@
this.type_ = "element";
this.source_ = source;
this.options_ = options;
this.location_ = location();
}
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 CodeMirror from './CodeMirror';
import cx from './cx';
@ -35,9 +35,33 @@ function getRandomTune() {
const randomTune = getRandomTune();
function App() {
const [editor, setEditor] = useState<any>();
const doc = useMemo(() => editor?.getDoc(), [editor]);
const { setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog } = useRepl({
tune: decoded || randomTune,
defaultSynth,
onEvent: useCallback(
(event) => {
const locs = event.value.locations;
if (!locs) {
return;
}
// mark active event
const marks = locs.map(({ start, end }) =>
doc.markText(
{ line: start.line - 1, ch: start.column },
{ line: end.line - 1, ch: end.column },
{ css: 'background-color: gray;' }
)
);
//Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler...
setTimeout(() => {
marks.forEach((mark) => mark.clear());
// }, '+' + event.duration * 0.5);
}, event.duration * 0.9 * 1000);
},
[doc]
),
});
const logBox = useRef<any>();
// scroll log box to bottom when log changes
@ -106,10 +130,12 @@ function App() {
<div className={cx('h-full bg-[#2A3236]', error ? 'focus:ring-red-500' : 'focus:ring-slate-800')}>
<CodeMirror
value={code}
editorDidMount={setEditor}
options={{
mode: 'javascript',
theme: 'material',
lineNumbers: true,
styleSelectedText: true,
}}
onChange={(_: any, __: any, value: any) => setCode(value)}
/>

View File

@ -5,11 +5,12 @@ import 'codemirror/mode/pegjs/pegjs.js';
import 'codemirror/theme/material.css';
import 'codemirror/lib/codemirror.css';
export default function CodeMirror({ value, onChange, options }: any) {
export default function CodeMirror({ value, onChange, options, editorDidMount }: any) {
options = options || {
mode: 'javascript',
theme: 'material',
lineNumbers: true,
styleSelectedText: true,
};
return <CodeMirror2 value={value} options={options} onBeforeChange={onChange} />;
return <CodeMirror2 value={value} options={options} onBeforeChange={onChange} editorDidMount={editorDidMount} />;
}

View File

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

View File

@ -1,6 +1,7 @@
import * as krill from '../krill-parser';
import * as strudel from '../../strudel.mjs';
import { Scale, Note, Interval } from '@tonaljs/tonal';
import { addMiniLocations } from './shapeshifter';
const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence } = strudel;
@ -49,6 +50,7 @@ function resolveReplications(ast) {
{
type_: 'element',
source_: child.source_,
location_: child.location_,
options_: {
operator: {
type_: 'stretch',
@ -91,7 +93,18 @@ export function patternifyAST(ast: any): any {
return silence;
}
if (typeof ast.source_ !== 'object') {
return ast.source_;
if (!addMiniLocations) {
return ast.source_;
}
if (!ast.location_) {
console.warn('no location for', ast);
return ast.source_;
}
const { start, end } = ast.location_;
// return ast.source_;
// the following line expects the shapeshifter to wrap this in withLocationOffset
// because location_ is only relative to the mini string, but we need it relative to whole code
return pure(ast.source_).withLocation({ start, end });
}
return patternifyAST(ast.source_);
case 'stretch':

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
const { replace } = traverser;
import { LiteralStringExpression, IdentifierExpression } from 'shift-ast';
import { LiteralStringExpression, IdentifierExpression, CallExpression, StaticMemberExpression } from 'shift-ast';
import codegen from 'shift-codegen';
import * as strudel from '../../strudel.mjs';
const { Pattern } = strudel;
const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
const addLocations = true;
export const addMiniLocations = true;
export default (code) => {
const ast = parseScript(code);
const shifted = replace(ast, {
const ast = parseScriptWithLocation(code);
const nodesWithLocation = [];
const parents = [];
const shifted = replace(ast.tree, {
enter(node, parent) {
// replace identifiers that are a note with a note string
parents.push(parent);
const isSynthetic = parents.some((p) => nodesWithLocation.includes(p));
if (isSynthetic) {
return node;
}
const grandparent = parents[parents.length - 2];
const isTimeCat = parent?.type === 'ArrayExpression' && isPatternFactory(grandparent);
const isMarkable = isPatternFactory(parent) || isTimeCat;
// operator overloading => still not done
const operators = {
'*': 'fast',
'/': 'slow',
'&': 'stack',
'&&': 'append',
};
if (
node.type === 'BinaryExpression' &&
operators[node.operator] &&
['LiteralNumericExpression', 'LiteralStringExpression', 'IdentifierExpression'].includes(node.right?.type) &&
canBeOverloaded(node.left)
) {
let arg = node.left;
if (node.left.type === 'IdentifierExpression') {
arg = wrapReify(node.left);
}
return new CallExpression({
callee: new StaticMemberExpression({
property: operators[node.operator],
object: wrapReify(arg),
}),
arguments: [node.right],
});
}
// replace pseudo note variables
if (node.type === 'IdentifierExpression') {
if (isNote(node.name)) {
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
if (addLocations && isMarkable) {
return reifyWithLocation(value, node, ast.locations, nodesWithLocation);
}
return new LiteralStringExpression({ value });
}
if (node.name === 'r') {
return new IdentifierExpression({ name: 'silence' });
}
}
if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) {
// console.log('add', node);
return reifyWithLocation(node.value, node, ast.locations, nodesWithLocation);
}
if (!addMiniLocations) {
return node;
}
// mini notation location handling
const miniFunctions = ['mini', 'm'];
const isAlreadyWrapped = parent?.type === 'CallExpression' && parent.callee.name === 'withLocationOffset';
if (node.type === 'CallExpression' && miniFunctions.includes(node.callee.name) && !isAlreadyWrapped) {
// mini('c3')
if (node.arguments.length > 1) {
// TODO: transform mini(...args) to cat(...args.map(mini)) ?
console.warn('multi arg mini locations not supported yet...');
return node;
}
return wrapLocationOffset(node, node.arguments, ast.locations, nodesWithLocation);
}
if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property) && !isAlreadyWrapped) {
// 'c3'.mini or 'c3'.m
return wrapLocationOffset(node, node.object, ast.locations, nodesWithLocation);
}
return node;
},
leave() {
parents.pop();
},
});
return codegen(shifted);
};
function wrapReify(node) {
return new CallExpression({
callee: new IdentifierExpression({
name: 'reify',
}),
arguments: [node],
});
}
function isPatternFactory(node) {
return node?.type === 'CallExpression' && Object.keys(Pattern.prototype.factories).includes(node.callee.name);
}
function canBeOverloaded(node) {
return (node.type === 'IdentifierExpression' && isNote(node.name)) || isPatternFactory(node);
// TODO: support sequence(c3).transpose(3).x.y.z
}
// turn node into withLocationOffset(node, location)
function wrapLocationOffset(node, stringNode, locations, nodesWithLocation) {
// console.log('wrapppp', stringNode);
const expression = {
type: 'CallExpression',
callee: {
type: 'IdentifierExpression',
name: 'withLocationOffset',
},
arguments: [node, getLocationObject(stringNode, locations)],
};
nodesWithLocation.push(expression);
// console.log('wrapped', codegen(expression));
return expression;
}
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
function reifyWithLocation(value, node, locations, nodesWithLocation) {
// console.log('reifyWithLocation', value, node);
const withLocation = new CallExpression({
callee: new StaticMemberExpression({
object: new CallExpression({
callee: new IdentifierExpression({
name: 'reify',
}),
arguments: [new LiteralStringExpression({ value })],
}),
property: 'withLocation',
}),
arguments: [getLocationObject(node, locations)],
});
nodesWithLocation.push(withLocation);
return withLocation;
}
// returns ast for source location object
function getLocationObject(node, locations) {
/*const locationAST = parseScript(
"x=" + JSON.stringify(ast.locations.get(node))
).statements[0].expression.expression;
console.log("locationAST", locationAST);*/
/*const callAST = parseScript(
`reify(${node.name}).withLocation(${JSON.stringify(
ast.locations.get(node)
)})`
).statements[0].expression;*/
const loc = locations.get(node);
return {
type: 'ObjectExpression',
properties: [
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'start',
},
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'line',
},
expression: {
type: 'LiteralNumericExpression',
value: loc.start.line,
},
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'column',
},
expression: {
type: 'LiteralNumericExpression',
value: loc.start.column,
},
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'offset',
},
expression: {
type: 'LiteralNumericExpression',
value: loc.start.offset,
},
},
],
},
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'end',
},
expression: {
type: 'ObjectExpression',
properties: [
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'line',
},
expression: {
type: 'LiteralNumericExpression',
value: loc.end.line,
},
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'column',
},
expression: {
type: 'LiteralNumericExpression',
value: loc.end.column,
},
},
{
type: 'DataProperty',
name: {
type: 'StaticPropertyName',
value: 'offset',
},
expression: {
type: 'LiteralNumericExpression',
value: loc.end.offset,
},
},
],
},
},
],
};
}
// TODO: turn x.groove['[~ x]*2'] into x.groove('[~ x]*2'.m)
// and ['c1*2'].xx into 'c1*2'.m.xx ??
// or just all templated strings?? x.groove(`[~ x]*2`)
// or just all templated strings?? x.groove(`[~ x]*2`)

View File

@ -4,12 +4,12 @@ import { Pattern as _Pattern } from '../../strudel.mjs';
const Pattern = _Pattern as any;
export declare interface NoteEvent {
value: string;
value: string | number;
scale?: string;
}
function toNoteEvent(event: string | NoteEvent): NoteEvent {
if (typeof event === 'string') {
if (typeof event === 'string' || typeof event === 'number') {
return { value: event };
}
if (event.value) {
@ -73,6 +73,10 @@ Pattern.prototype._transpose = function (intervalOrSemitones: string | number) {
const interval = !isNaN(Number(intervalOrSemitones))
? Interval.fromSemitones(intervalOrSemitones as number)
: String(intervalOrSemitones);
if (typeof value === 'number') {
const semitones = typeof interval === 'string' ? Interval.semitones(interval) || 0 : interval;
return { value: value + semitones };
}
return { value: Note.transpose(value, interval), scale };
});
};
@ -86,7 +90,10 @@ Pattern.prototype._transpose = function (intervalOrSemitones: string | number) {
Pattern.prototype._scaleTranspose = function (offset: number | string) {
return this._mapNotes(({ value, scale }: NoteEvent) => {
if (!scale) {
throw new Error('can only use scaleOffset after .scale');
throw new Error('can only use scaleTranspose after .scale');
}
if (typeof value !== 'string') {
throw new Error('can only use scaleTranspose with notes');
}
return { value: scaleTranspose(scale, Number(offset), value), scale };
});

View File

@ -5,11 +5,11 @@ export const timeCatMini = `stack(
)`;
export const timeCat = `stack(
timeCat([3, c3], [1, stack(eb3, g3, m(c4, d4).slow(2))]),
m(c2, g2),
timeCat([3, c3], [1, stack(eb3, g3, cat(c4, d4).slow(2))]),
cat(c2, g2),
sequence(
timeCat([5, eb4], [3, m(f4, eb4, d4)]),
m(eb4, c4).slow(2)
timeCat([5, eb4], [3, cat(f4, eb4, d4)]),
cat(eb4, c4).slow(2)
).slow(4)
)`;
@ -178,11 +178,11 @@ export const spanish = `slowcat(
stack(g3,b3,d4)
)`;
export const whirlyStrudel = `mini("[e4 [b2 b3] c4]")
export const whirlyStrudel = `sequence(e4, [b2, b3], c4)
.every(4, fast(2))
.every(3, slow(1.5))
.fast(slowcat(1.25, 1, 1.5))
.every(2, _ => mini("e4 ~ e3 d4 ~"))`;
.every(2, _ => sequence(e4, r, e3, d4, r))`;
export const swimming = `stack(
mini(
@ -296,13 +296,6 @@ export const giantStepsReggae = `stack(
.groove('x ~'.m.fast(4*8))
).slow(25)`;
/* export const transposedChords = `stack(
m('c2 eb2 g2'),
m('Cm7').voicings(['g2','c4']).slow(2)
).transpose(
slowcat(1, 2, 3, 2).slow(2)
).transpose(5)`; */
export const transposedChordsHacked = `stack(
'c2 eb2 g2'.mini,
'Cm7'.pure.voicings(['g2','c4']).slow(2)
@ -315,27 +308,12 @@ export const scaleTranspose = `stack(f2, f3, c4, ab4)
.scaleTranspose(sequence(0, -1, -2, -3).slow(4))
.transpose(sequence(0, 1).slow(16))`;
/* export const groove = `stack(
m('c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]'),
m('[C^7 A7] [Dm7 G7]')
.groove(m('[x@2 x] [~@2 x] [~ x@2]@2 [x ~@2] ~ [~@2 x@4]@2'))
.voicings(['G3','A4'])
).slow(4.5)`; */
export const groove = `stack(
'c2 g2 a2 [e2@2 eb2] d2 a2 g2 [d2 ~ db2]'.mini,
'[C^7 A7] [Dm7 G7]'.mini.groove('[x@2 x] [~@2 x] [~ x@2]@2 [x ~@2] ~ [~@2 x@4]@2'.mini)
.voicings(['G3','A4'])
).slow(4)`;
/* export const magicSofa = `stack(
m('[C^7 F^7 ~]/3 [Dm7 G7 A7 ~]/4')
.every(2, fast(2))
.voicings(),
m('[c2 f2 g2]/3 [d2 g2 a2 e2]/4')
).slow(1)
.transpose.slowcat(0, 2, 3, 4)`; */
export const magicSofa = `stack(
'<C^7 F^7 ~> <Dm7 G7 A7 ~>'.m
.every(2, fast(2))
@ -462,66 +440,3 @@ export const caverave = `() => {
synths
).slow(2);
}`;
export const caveravefuture = `() => {
const delay = new FeedbackDelay(1/8, .4).chain(vol(0.5), out);
const kick = new MembraneSynth().chain(vol(.8), out);
const snare = new NoiseSynth().chain(vol(.8), out);
const hihat = new MetalSynth().set(adsr(0, .08, 0, .1)).chain(vol(.3).connect(delay),out);
const bass = new Synth().set({ ...osc('sawtooth'), ...adsr(0, .1, .4) }).chain(lowpass(900), vol(.5), out);
const keys = new PolySynth().set({ ...osc('sawtooth'), ...adsr(0, .5, .2, .7) }).chain(lowpass(1200), vol(.5), out);
const drums = stack(
\`c1*2\`.tone(kick).bypass(\`<0@7 1>/8\`),
\`~ <x!7 [x@3 x]>\`.tone(snare).bypass(\`<0@7 1>/4\`),
\`[~ c4]*2\`.tone(hihat)
);
const thru = (x) => x.transpose(\`<0 1>/8\`).transpose(-1);
const synths = stack(
\`<eb4 d4 c4 b3>/2\`.scale(timeCat([3,'C minor'],[1,'C melodic minor']).slow(8)).groove(\`[~ x]*2\`)
.edit(
scaleTranspose(0).early(0),
scaleTranspose(2).early(1/8),
scaleTranspose(7).early(1/4),
scaleTranspose(8).early(3/8)
).edit(thru).tone(keys).bypass(\`<1 0>/16\`),
\`<C2 Bb1 Ab1 [G1 [G2 G1]]>/2\`.groove(\`x [~ x] <[~ [~ x]]!3 [x x]>@2\`).edit(thru).tone(bass),
\`<Cm7 Bb7 Fm7 G7b13>/2\`.groove(\`~ [x@0.5 ~]\`.fast(2)).voicings().edit(thru).every(2, early(1/8)).tone(keys).bypass(\`<0@7 1>/8\`.early(1/4)),
)
return stack(
drums.fast(2),
synths
).slow(2);
}`;
export const caveravefuture2 = `const delay = new FeedbackDelay(1/8, .4).chain(vol(0.5), out);
const kick = new MembraneSynth().chain(vol(.8), out);
const snare = new NoiseSynth().chain(vol(.8), out);
const hihat = new MetalSynth().set(adsr(0, .08, 0, .1)).chain(vol(.3).connect(delay),out);
const bass = new Synth().set({ ...osc('sawtooth'), ...adsr(0, .1, .4) }).chain(lowpass(900), vol(.5), out);
const keys = new PolySynth().set({ ...osc('sawtooth'), ...adsr(0, .5, .2, .7) }).chain(lowpass(1200), vol(.5), out);
const drums = stack(
"c1*2".tone(kick).bypass("<0@7 1>/8"),
"~ <x!7 [x@3 x]>".tone(snare).bypass("<0@7 1>/4"),
"[~ c4]*2".tone(hihat)
);
const thru = (x) => x.transpose("<0 1>/8").transpose(-1);
const synths = stack(
"<eb4 d4 c4 b3>/2".scale(timeCat([3, 'C minor'], [1, 'C melodic minor']).slow(8)).groove("[~ x]*2")
.edit(
scaleTranspose(0).early(0),
scaleTranspose(2).early(1/8),
scaleTranspose(7).early(1/4),
scaleTranspose(8).early(3/8)
).edit(thru).tone(keys).bypass("<1 0>/16"),
"<C2 Bb1 Ab1 [G1 [G2 G1]]>/2".groove("x [~ x] <[~ [~ x]]!3 [x x]>@2").edit(thru).tone(bass),
"<Cm7 Bb7 Fm7 G7b13>/2".groove("~ [x@0.5 ~]".fast(2)).voicings().edit(thru).every(2, early(1/8)).tone(keys).bypass("<0@7 1>/8".early(1/4)),
)
$: stack(
drums.fast(2),
synths
).slow(2);
`;

View File

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

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

View File

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

View File

@ -148,7 +148,7 @@ class TimeSpan {
return result
}
get midpoint() {
midpoint() {
return(this.begin.add((this.end.sub(this.begin)).div(Fraction(2))))
}
@ -298,6 +298,14 @@ class Pattern {
return new Pattern(state => func(this.query(state)))
}
withLocation(location) {
return this.fmap(value => {
value = typeof value === 'object' && !Array.isArray(value) ? value : { value };
const locations = (value.locations || []).concat([location]);
return {...value, locations }
})
}
withValue(func) {
// Returns a new pattern, with the function applied to the value of
// each event. It has the alias 'fmap'.
@ -478,7 +486,14 @@ class Pattern {
_patternify(func) {
const pat = this
const patterned = function (...args) {
// the problem here: args could a pattern that has been turned into an object to add location
// to avoid object checking for every pattern method, we can remove it here...
// in the future, patternified args should be marked as well + some better object handling
args = args.map((arg) =>
arg.constructor?.name === 'Pattern' ? arg.fmap((value) => value.value || value) : arg
);
const pat_arg = sequence(...args)
// arg.locations has to go somewhere..
return pat_arg.fmap(arg => func.call(pat,arg)).outerJoin()
}
return patterned
@ -680,6 +695,32 @@ function steady(value) {
return new Pattern(span => Hap(undefined, span, value))
}
export const signal = func => {
const query = span => [new Hap(undefined, span, func(span.midpoint()))]
return new Pattern(query)
}
const _toBipolar = pat => pat.fmap(x => (x * 2) - 1)
const _fromBipolar = pat => pat.fmap(x => (x + 1) / 2)
export const sine2 = signal(t => Math.sin(Math.PI * 2 * t))
export const sine = _fromBipolar(sine2)
export const cosine2 = sine2._early(0.25)
export const cosine = sine._early(0.25)
export const saw = signal(t => t % 1)
export const saw2 = _toBipolar(saw)
export const isaw = signal(t => 1 - (t % 1))
export const isaw2 = _toBipolar(isaw)
export const tri2 = fastcat(isaw2, saw2)
export const tri = fastcat(isaw, saw)
export const square = signal(t => Math.floor((t*2) % 2))
export const square2 = _toBipolar(square)
function reify(thing) {
// Tunrs something into a pattern, unless it's already a pattern
if (thing?.constructor?.name == "Pattern") {
@ -882,10 +923,34 @@ Pattern.prototype.bootstrap = () => {
return bootstrapped;
}
// this is wrapped around mini patterns to offset krill parser location into the global js code space
function withLocationOffset(pat, offset) {
return pat.fmap((value) => {
value = typeof value === 'object' && !Array.isArray(value) ? value : { value };
let locations = (value.locations || []);
locations = locations.map(({ start, end }) => {
const colOffset = start.line === 1 ? offset.start.column : 0;
return {
start: {
...start,
line: start.line - 1 + (offset.start.line - 1) + 1,
column: start.column - 1 + colOffset,
},
end: {
...end,
line: end.line - 1 + (offset.start.line - 1) + 1,
column: end.column - 1 + colOffset,
},
}});
return {...value, locations }
});
}
export {Fraction, TimeSpan, Hap, Pattern,
pure, stack, slowcat, fastcat, cat, timeCat, sequence, polymeter, pm, polyrhythm, pr, reify, silence,
fast, slow, early, late, rev,
add, sub, mul, div, union, every, when, off, jux, append, superimpose,
struct, mask, invert, inv
struct, mask, invert, inv,
withLocationOffset
}

View File

@ -2,7 +2,7 @@ import Fraction from 'fraction.js'
import { strict as assert } from 'assert';
import {TimeSpan, Hap, Pattern, pure, stack, fastcat, slowcat, cat, sequence, polyrhythm, silence, fast, timeCat,add,sub,mul,div, State} from "../strudel.mjs";
import {TimeSpan, Hap, State, Pattern, pure, stack, fastcat, slowcat, cat, sequence, polyrhythm, silence, fast, timeCat,add,sub,mul,div,saw,saw2,isaw,isaw2,sine,sine2,square,square2,tri,tri2} from "../strudel.mjs";
//import { Time } from 'tone';
import pkg from 'tone';
const { Time } = pkg;
@ -308,4 +308,26 @@ describe('Pattern', function() {
)
})
})
describe('signal()', function() {
it('Can make saw/saw2', function() {
assert.deepStrictEqual(
saw.struct(true,true,true,true).firstCycle,
sequence(1/8,3/8,5/8,7/8).firstCycle
)
assert.deepStrictEqual(
saw2.struct(true,true,true,true).firstCycle,
sequence(-3/4,-1/4,1/4,3/4).firstCycle
)
})
it('Can make isaw/isaw2', function() {
assert.deepStrictEqual(
isaw.struct(true,true,true,true).firstCycle,
sequence(7/8,5/8,3/8,1/8).firstCycle
)
assert.deepStrictEqual(
isaw2.struct(true,true,true,true).firstCycle,
sequence(3/4,1/4,-1/4,-3/4).firstCycle
)
})
})
})