mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-26 13:08:28 +00:00
Merge branch 'main' into stateful-events
This commit is contained in:
commit
becc73e58a
@ -1936,6 +1936,7 @@ function peg$parse(input, options) {
|
|||||||
this.type_ = "element";
|
this.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)
|
||||||
|
|||||||
@ -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
20
docs/dist/App.js
vendored
@ -1,4 +1,4 @@
|
|||||||
import React, {useCallback, useLayoutEffect, useRef} from "../_snowpack/pkg/react.js";
|
import React, {useCallback, useLayoutEffect, useMemo, useRef, useState} from "../_snowpack/pkg/react.js";
|
||||||
import * as Tone from "../_snowpack/pkg/tone.js";
|
import * 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", {
|
||||||
|
|||||||
8
docs/dist/CodeMirror.js
vendored
8
docs/dist/CodeMirror.js
vendored
@ -4,15 +4,17 @@ import "../_snowpack/pkg/codemirror/mode/javascript/javascript.js";
|
|||||||
import "../_snowpack/pkg/codemirror/mode/pegjs/pegjs.js";
|
import "../_snowpack/pkg/codemirror/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
12
docs/dist/parse.js
vendored
@ -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":
|
||||||
|
|||||||
246
docs/dist/shapeshifter.js
vendored
246
docs/dist/shapeshifter.js
vendored
@ -1,30 +1,266 @@
|
|||||||
import { parseScript } from './shift-parser/index.js'; // npm module does not work in the browser
|
import { parseScriptWithLocation } from './shift-parser/index.js'; // npm module does not work in the browser
|
||||||
import traverser from './shift-traverser/index.js'; // npm module does not work in the browser
|
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
11
docs/dist/tonal.js
vendored
@ -2,7 +2,7 @@ import {Note, Interval, Scale} from "../_snowpack/pkg/@tonaljs/tonal.js";
|
|||||||
import {Pattern as _Pattern} from "../_snowpack/link/strudel.js";
|
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
73
docs/dist/tunes.js
vendored
@ -4,11 +4,11 @@ export const timeCatMini = `stack(
|
|||||||
'[eb4@5 [f4 eb4 d4]@3] [eb4 c4]/2'.mini.slow(8)
|
'[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);
|
|
||||||
`;
|
|
||||||
|
|||||||
8
docs/dist/useRepl.js
vendored
8
docs/dist/useRepl.js
vendored
@ -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 [];
|
||||||
}
|
}
|
||||||
|
|||||||
2
docs/dist/voicings.js
vendored
2
docs/dist/voicings.js
vendored
@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
@ -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>
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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':
|
||||||
|
|||||||
@ -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`)
|
||||||
@ -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 };
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
|
||||||
`;
|
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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 [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
69
strudel.mjs
69
strudel.mjs
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user