patterns with double quotes and backticks

+ improved highlighting
+ many shapeshifter fixes
+ add highlighting to minirepl
+ add error handling to minirepl
This commit is contained in:
Felix Roos 2022-02-22 00:27:38 +01:00
parent 9a480344de
commit 522005a7ab
9 changed files with 286 additions and 138 deletions

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } 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, { markEvent } from './CodeMirror';
import cx from './cx'; import cx from './cx';
import { evaluate } from './evaluate'; import { evaluate } from './evaluate';
import logo from './logo.svg'; import logo from './logo.svg';
@ -36,32 +36,10 @@ const randomTune = getRandomTune();
function App() { function App() {
const [editor, setEditor] = useState<any>(); 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( onEvent: useCallback(markEvent(editor), [editor]),
(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
@ -136,6 +114,7 @@ function App() {
theme: 'material', theme: 'material',
lineNumbers: true, lineNumbers: true,
styleSelectedText: true, styleSelectedText: true,
cursorBlinkRate: 0,
}} }}
onChange={(_: any, __: any, value: any) => setCode(value)} onChange={(_: any, __: any, value: any) => setCode(value)}
/> />

View File

@ -2,8 +2,9 @@ import React from 'react';
import { Controlled as CodeMirror2 } from 'react-codemirror2'; import { Controlled as CodeMirror2 } from 'react-codemirror2';
import 'codemirror/mode/javascript/javascript.js'; import 'codemirror/mode/javascript/javascript.js';
import 'codemirror/mode/pegjs/pegjs.js'; 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';
import 'codemirror/theme/material.css';
export default function CodeMirror({ value, onChange, options, editorDidMount }: any) { export default function CodeMirror({ value, onChange, options, editorDidMount }: any) {
options = options || { options = options || {
@ -11,6 +12,29 @@ export default function CodeMirror({ value, onChange, options, editorDidMount }:
theme: 'material', theme: 'material',
lineNumbers: true, lineNumbers: true,
styleSelectedText: true, styleSelectedText: true,
cursorBlinkRate: 500,
}; };
return <CodeMirror2 value={value} options={options} onBeforeChange={onChange} editorDidMount={editorDidMount} />; return <CodeMirror2 value={value} options={options} onBeforeChange={onChange} editorDidMount={editorDidMount} />;
} }
export const markEvent = (editor) => (event) => {
const locs = event.value.locations;
if (!locs || !editor) {
return;
}
// mark active event
const marks = locs.map(({ start, end }) =>
editor
.getDoc()
.markText(
{ line: start.line - 1, ch: start.column },
{ line: end.line - 1, ch: end.column },
{ css: 'background-color: #FFCA28; color: black' }
)
);
//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);
};

View File

@ -37,10 +37,12 @@ export const evaluate: any = (code: string) => {
if (typeof evaluated === 'function') { if (typeof evaluated === 'function') {
evaluated = evaluated(); evaluated = evaluated();
} }
const pattern = minify(evaluated); // eval and minify (if user entered a string) if (typeof evaluated === 'string') {
if (pattern?.constructor?.name !== 'Pattern') { evaluated = strudel.withLocationOffset(minify(evaluated), { start: { line: 1, column: -1 } });
const message = `got "${typeof pattern}" instead of pattern`;
throw new Error(message + (typeof pattern === 'function' ? ', did you forget to call a function?' : '.'));
} }
return { mode: 'javascript', pattern: pattern }; if (evaluated?.constructor?.name !== 'Pattern') {
const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
}
return { mode: 'javascript', pattern: evaluated };
}; };

View File

@ -1,7 +1,13 @@
import { parseScriptWithLocation } 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, CallExpression, StaticMemberExpression } from 'shift-ast'; import {
LiteralStringExpression,
IdentifierExpression,
CallExpression,
StaticMemberExpression,
Script,
} from 'shift-ast';
import codegen from 'shift-codegen'; import codegen from 'shift-codegen';
import * as strudel from '../../strudel.mjs'; import * as strudel from '../../strudel.mjs';
@ -12,20 +18,50 @@ const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
const addLocations = true; const addLocations = true;
export const addMiniLocations = true; export const addMiniLocations = true;
/*
not supported for highlighting:
- 'b3'.p
- mini('b3') / m('b3')
- 'b3'.m / 'b3'.mini
*/
export default (code) => { export default (code) => {
const ast = parseScriptWithLocation(code); const ast = parseScriptWithLocation(code);
const nodesWithLocation = []; const artificialNodes = [];
const parents = []; const parents = [];
const shifted = replace(ast.tree, { const shifted = replace(ast.tree, {
enter(node, parent) { enter(node, parent) {
parents.push(parent); parents.push(parent);
const isSynthetic = parents.some((p) => nodesWithLocation.includes(p)); const isSynthetic = parents.some((p) => artificialNodes.includes(p));
if (isSynthetic) { if (isSynthetic) {
return node; return node;
} }
const grandparent = parents[parents.length - 2];
const isTimeCat = parent?.type === 'ArrayExpression' && isPatternFactory(grandparent); // replace template string `xxx` with 'xxx'.m
const isMarkable = isPatternFactory(parent) || isTimeCat; if (isBackTickString(node)) {
const minified = getMinified(node.elements[0].rawValue);
return wrapLocationOffset(minified, node, ast.locations, artificialNodes);
}
// allows to use top level strings, which are normally directives... but we don't need directives
if (node.type === 'Script' && node.directives.length === 1 && !node.statements.length) {
const minified = getMinified(node.directives[0].rawValue);
const wrapped = wrapLocationOffset(minified, node.directives[0], ast.locations, artificialNodes);
return new Script({ directives: [], statements: [wrapped] });
}
// replace double quote string "xxx" with 'xxx'.m
if (isStringWithDoubleQuotes(node, ast.locations, code)) {
const minified = getMinified(node.value);
return wrapLocationOffset(minified, node, ast.locations, artificialNodes);
}
// replace double quote string "xxx" with 'xxx'.m
if (isStringWithDoubleQuotes(node, ast.locations, code)) {
const minified = getMinified(node.value);
return wrapLocationOffset(minified, node, ast.locations, artificialNodes);
}
// operator overloading => still not done // operator overloading => still not done
const operators = { const operators = {
'*': 'fast', '*': 'fast',
@ -41,22 +77,28 @@ export default (code) => {
) { ) {
let arg = node.left; let arg = node.left;
if (node.left.type === 'IdentifierExpression') { if (node.left.type === 'IdentifierExpression') {
arg = wrapReify(node.left); arg = wrapFunction('reify', node.left);
} }
return new CallExpression({ return new CallExpression({
callee: new StaticMemberExpression({ callee: new StaticMemberExpression({
property: operators[node.operator], property: operators[node.operator],
object: wrapReify(arg), object: wrapFunction('reify', arg),
}), }),
arguments: [node.right], arguments: [node.right],
}); });
} }
const isMarkable = isPatternArg(parents) || hasModifierCall(parent);
// add to location to pure(x) calls
if (node.type === 'CallExpression' && node.callee.name === 'pure') {
return reifyWithLocation(node.arguments[0].name, node.arguments[0], ast.locations, artificialNodes);
}
// replace pseudo note variables // 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) { if (addLocations && isMarkable) {
return reifyWithLocation(value, node, ast.locations, nodesWithLocation); return reifyWithLocation(value, node, ast.locations, artificialNodes);
} }
return new LiteralStringExpression({ value }); return new LiteralStringExpression({ value });
} }
@ -66,10 +108,10 @@ export default (code) => {
} }
if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) { if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) {
// console.log('add', node); // console.log('add', node);
return reifyWithLocation(node.value, node, ast.locations, nodesWithLocation); return reifyWithLocation(node.value, node, ast.locations, artificialNodes);
} }
if (!addMiniLocations) { if (!addMiniLocations) {
return node; return wrapFunction('reify', node);
} }
// mini notation location handling // mini notation location handling
const miniFunctions = ['mini', 'm']; const miniFunctions = ['mini', 'm'];
@ -81,11 +123,11 @@ export default (code) => {
console.warn('multi arg mini locations not supported yet...'); console.warn('multi arg mini locations not supported yet...');
return node; return node;
} }
return wrapLocationOffset(node, node.arguments, ast.locations, nodesWithLocation); return wrapLocationOffset(node, node.arguments, ast.locations, artificialNodes);
} }
if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property) && !isAlreadyWrapped) { if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property) && !isAlreadyWrapped) {
// 'c3'.mini or 'c3'.m // 'c3'.mini or 'c3'.m
return wrapLocationOffset(node, node.object, ast.locations, nodesWithLocation); return wrapLocationOffset(node, node.object, ast.locations, artificialNodes);
} }
return node; return node;
}, },
@ -96,15 +138,58 @@ export default (code) => {
return codegen(shifted); return codegen(shifted);
}; };
function wrapReify(node) { function wrapFunction(name, ...arguments) {
return new CallExpression({ return new CallExpression({
callee: new IdentifierExpression({ callee: new IdentifierExpression({ name }),
name: 'reify', arguments,
}),
arguments: [node],
}); });
} }
function getMinified(value) {
return new StaticMemberExpression({
object: new LiteralStringExpression({ value }),
property: 'm',
});
}
function isBackTickString(node) {
return node.type === 'TemplateExpression' && node.elements.length === 1;
}
function isStringWithDoubleQuotes(node, locations, code) {
if (node.type !== 'LiteralStringExpression') {
return false;
}
const loc = locations.get(node);
const snippet = code.slice(loc.start.offset, loc.end.offset);
return snippet[0] === '"'; // we can trust the end is also ", as the parsing did not fail
}
// returns true if the given parents belong to a pattern argument node
// this is used to check if a node should receive a location for highlighting
function isPatternArg(parents) {
if (!parents.length) {
return false;
}
const ancestors = parents.slice(0, -1);
const parent = parents[parents.length - 1];
if (isPatternFactory(parent)) {
return true;
}
if (parent?.type === 'ArrayExpression') {
return isPatternArg(ancestors);
}
return false;
}
function hasModifierCall(parent) {
// TODO: modifiers are more than composables, for example every is not composable but should be seen as modifier..
// need all prototypes of Pattern
return (
parent?.type === 'StaticMemberExpression' && Object.keys(Pattern.prototype.composable).includes(parent.property)
);
}
function isPatternFactory(node) { function isPatternFactory(node) {
return node?.type === 'CallExpression' && Object.keys(Pattern.prototype.factories).includes(node.callee.name); return node?.type === 'CallExpression' && Object.keys(Pattern.prototype.factories).includes(node.callee.name);
} }
@ -115,7 +200,7 @@ function canBeOverloaded(node) {
} }
// turn node into withLocationOffset(node, location) // turn node into withLocationOffset(node, location)
function wrapLocationOffset(node, stringNode, locations, nodesWithLocation) { function wrapLocationOffset(node, stringNode, locations, artificialNodes) {
// console.log('wrapppp', stringNode); // console.log('wrapppp', stringNode);
const expression = { const expression = {
type: 'CallExpression', type: 'CallExpression',
@ -125,28 +210,22 @@ function wrapLocationOffset(node, stringNode, locations, nodesWithLocation) {
}, },
arguments: [node, getLocationObject(stringNode, locations)], arguments: [node, getLocationObject(stringNode, locations)],
}; };
nodesWithLocation.push(expression); artificialNodes.push(expression);
// console.log('wrapped', codegen(expression)); // console.log('wrapped', codegen(expression));
return expression; return expression;
} }
// turns node in reify(value).withLocation(location), where location is the node's location in the source code // 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 // 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) { function reifyWithLocation(value, node, locations, artificialNodes) {
// console.log('reifyWithLocation', value, node);
const withLocation = new CallExpression({ const withLocation = new CallExpression({
callee: new StaticMemberExpression({ callee: new StaticMemberExpression({
object: new CallExpression({ object: wrapFunction('reify', new LiteralStringExpression({ value })),
callee: new IdentifierExpression({
name: 'reify',
}),
arguments: [new LiteralStringExpression({ value })],
}),
property: 'withLocation', property: 'withLocation',
}), }),
arguments: [getLocationObject(node, locations)], arguments: [getLocationObject(node, locations)],
}); });
nodesWithLocation.push(withLocation); artificialNodes.push(withLocation);
return withLocation; return withLocation;
} }

View File

@ -440,3 +440,20 @@ export const caverave = `() => {
synths synths
).slow(2); ).slow(2);
}`; }`;
export const callcenterhero = `()=>{
const bpm = 90;
const lead = polysynth().set({...osc('sine4'),...adsr(.004)}).chain(vol(0.15),out)
const bass = fmsynth({...osc('sawtooth6'),...adsr(0.05,.6,0.8,0.1)}).chain(vol(0.6), out);
const s = scale(slowcat('F3 minor', 'Ab3 major', 'Bb3 dorian', 'C4 phrygian dominant').slow(4));
return stack(
"0 2".groove("<x ~> [x ~]").edit(s).scaleTranspose(stack(0,2)).tone(lead),
"<6 7 9 7>".groove("[~ [x ~]*2]*2").edit(s).scaleTranspose('[0,2] [2,4]'.m.fast(2).every(4,rev)).tone(lead),
"-14".groove("[~ x@0.8]*2".early(0.01)).edit(s).tone(bass),
"c2*2".tone(membrane().chain(vol(0.6), out)),
"~ c2".tone(noise().chain(vol(0.2), out)),
"c4*4".tone(metal(adsr(0,.05,0)).chain(vol(0.03), out))
)
.slow(120 / bpm)
}
`;

View File

@ -1,7 +1,7 @@
import React, { useMemo } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import * as Tone from 'tone'; import * as Tone from 'tone';
import useRepl from '../useRepl'; import useRepl from '../useRepl';
import CodeMirror from '../CodeMirror'; import CodeMirror, { markEvent } from '../CodeMirror';
import cx from '../cx'; import cx from '../cx';
const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.Destination).set({ const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.Destination).set({
@ -11,42 +11,87 @@ const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.Destina
}, },
}); });
function MiniRepl({ tune, height = 100 }) { function MiniRepl({ tune, maxHeight = 500 }) {
const { code, setCode, activateCode, activeCode, setPattern, error, cycle, dirty, log, togglePlay } = useRepl({ const [editor, setEditor] = useState<any>();
const { code, setCode, activateCode, activeCode, setPattern, error, cycle, dirty, log, togglePlay, hash } = useRepl({
tune, tune,
defaultSynth, defaultSynth,
autolink: false, autolink: false,
onEvent: useCallback(markEvent(editor), [editor]),
}); });
const lines = code.split('\n').length;
const height = Math.min(lines * 30 + 30, maxHeight);
return ( return (
<div className="flex space-y-0 overflow-auto" style={{ height }}> <div className="rounded-md overflow-hidden">
<div className="w-16 flex flex-col"> <div className="flex justify-between bg-slate-700 border-t border-slate-500">
<button <div className="flex">
className="grow bg-slate-700 border-b border-slate-500 text-white hover:bg-slate-600 " <button
onClick={() => togglePlay()} className={cx(
> 'w-16 flex items-center justify-center p-1 bg-slate-700 border-r border-slate-500 text-white hover:bg-slate-600',
{cycle.started ? 'pause' : 'play'} cycle.started ? 'animate-pulse' : ''
</button> )}
<button onClick={() => togglePlay()}
className={cx( >
'grow border-slate-500 hover:bg-slate-600', {!cycle.started ? (
activeCode && dirty ? 'bg-slate-700 text-white' : 'bg-slate-600 text-slate-400 cursor-not-allowed' <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
)} <path
onClick={() => activateCode()} fillRule="evenodd"
> d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
update clipRule="evenodd"
</button> />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
)}
</button>
<button
className={cx(
'w-16 flex items-center justify-center p-1 border-slate-500 hover:bg-slate-600',
dirty
? 'bg-slate-700 border-r border-slate-500 text-white'
: 'bg-slate-600 text-slate-400 cursor-not-allowed'
)}
onClick={() => activateCode()}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
<div className="text-right p-1 text-sm">{error && <span className="text-red-200">{error.message}</span>}</div>{' '}
</div> </div>
<CodeMirror <div className="flex space-y-0 overflow-auto" style={{ height }}>
className="w-full" <CodeMirror
value={code} className="w-full"
options={{ value={code}
mode: 'javascript', editorDidMount={setEditor}
theme: 'material', options={{
lineNumbers: true, mode: 'javascript',
}} theme: 'material',
onChange={(_: any, __: any, value: any) => setCode(value)} lineNumbers: true,
/> }}
{/* <textarea className="w-full" value={code} onChange={(e) => setCode(e.target.value)} /> */} onChange={(_: any, __: any, value: any) => setCode(value)}
/>
</div>
{/* <div className="bg-slate-700 border-t border-slate-500 content-right pr-2 text-right">
<a href={`https://strudel.tidalcycles.org/#${hash}`} className="text-white items-center inline-flex">
<span>open in REPL</span>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
</a>
</div> */}
</div> </div>
); );
} }

View File

@ -25,29 +25,28 @@ To get a taste of what Strudel can do, check out this track:
const keys = new PolySynth().set({ ...osc('sawtooth'), ...adsr(0, .5, .2, .7) }).chain(lowpass(1200), vol(.5), out); const keys = new PolySynth().set({ ...osc('sawtooth'), ...adsr(0, .5, .2, .7) }).chain(lowpass(1200), vol(.5), out);
const drums = stack( const drums = stack(
'c1*2'.m.tone(kick).bypass('<0@7 1>/8'.m), "c1*2".tone(kick).bypass("<0@7 1>/8"),
'~ <x!7 [x@3 x]>'.m.tone(snare).bypass('<0@7 1>/4'.m), "~ <x!7 [x@3 x]>".tone(snare).bypass("<0@7 1>/4"),
'[~ c4]*2'.m.tone(hihat) "[~ c4]*2".tone(hihat)
); );
const thru = (x) => x.transpose('<0 1>/8'.m).transpose(-1); const thru = (x) => x.transpose("<0 1>/8").transpose(-1);
const synths = stack( const synths = stack(
'<eb4 d4 c4 b3>/2'.m.scale(timeCat([3,'C minor'],[1,'C melodic minor']).slow(8)).groove('[~ x]*2'.m) "<eb4 d4 c4 b3>/2".scale(timeCat([3,'C minor'],[1,'C melodic minor']).slow(8)).groove("[~ x]*2")
.edit( .edit(
scaleTranspose(0).early(0), scaleTranspose(0).early(0),
scaleTranspose(2).early(1/8), scaleTranspose(2).early(1/8),
scaleTranspose(7).early(1/4), scaleTranspose(7).early(1/4),
scaleTranspose(8).early(3/8) scaleTranspose(8).early(3/8)
).edit(thru).tone(keys).bypass('<1 0>/16'.m), ).edit(thru).tone(keys).bypass("<1 0>/16"),
'<C2 Bb1 Ab1 [G1 [G2 G1]]>/2'.m.groove('[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2'.m.fast(2)).edit(thru).tone(bass), "<C2 Bb1 Ab1 [G1 [G2 G1]]>/2".groove("[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2".fast(2)).edit(thru).tone(bass),
'<Cm7 Bb7 Fm7 G7b13>/2'.m.groove('~ [x@0.1 ~]'.m.fast(2)).voicings().edit(thru).every(2, early(1/8)).tone(keys).bypass('<0@7 1>/8'.m.early(1/4)) "<Cm7 Bb7 Fm7 G7b13>/2".groove("~ [x@0.1 ~]".fast(2)).voicings().edit(thru).every(2, early(1/8)).tone(keys).bypass("<0@7 1>/8".early(1/4))
) )
return stack( return stack(
drums.fast(2), drums.fast(2),
synths synths
).slow(2); ).slow(2);
}`} }`}
height={400}
/> />
[Open this track in the REPL](https://strudel.tidalcycles.org/#KCkgPT4gewogIGNvbnN0IGRlbGF5ID0gbmV3IEZlZWRiYWNrRGVsYXkoMS84LCAuNCkuY2hhaW4odm9sKDAuNSksIG91dCk7CiAgY29uc3Qga2ljayA9IG5ldyBNZW1icmFuZVN5bnRoKCkuY2hhaW4odm9sKC44KSwgb3V0KTsKICBjb25zdCBzbmFyZSA9IG5ldyBOb2lzZVN5bnRoKCkuY2hhaW4odm9sKC44KSwgb3V0KTsKICBjb25zdCBoaWhhdCA9IG5ldyBNZXRhbFN5bnRoKCkuc2V0KGFkc3IoMCwgLjA4LCAwLCAuMSkpLmNoYWluKHZvbCguMykuY29ubmVjdChkZWxheSksb3V0KTsKICBjb25zdCBiYXNzID0gbmV3IFN5bnRoKCkuc2V0KHsgLi4ub3NjKCdzYXd0b290aCcpLCAuLi5hZHNyKDAsIC4xLCAuNCkgfSkuY2hhaW4obG93cGFzcyg5MDApLCB2b2woLjUpLCBvdXQpOwogIGNvbnN0IGtleXMgPSBuZXcgUG9seVN5bnRoKCkuc2V0KHsgLi4ub3NjKCdzYXd0b290aCcpLCAuLi5hZHNyKDAsIC41LCAuMiwgLjcpIH0pLmNoYWluKGxvd3Bhc3MoMTIwMCksIHZvbCguNSksIG91dCk7CiAgCiAgY29uc3QgZHJ1bXMgPSBzdGFjaygKICAgICdjMSoyJy5tLnRvbmUoa2ljaykuYnlwYXNzKCc8MEA3IDE%2BLzgnLm0pLAogICAgJ34gPHghNyBbeEAzIHhdPicubS50b25lKHNuYXJlKS5ieXBhc3MoJzwwQDcgMT4vNCcubSksCiAgICAnW34gYzRdKjInLm0udG9uZShoaWhhdCkKICApOwogIAogIGNvbnN0IHRocnUgPSAoeCkgPT4geC50cmFuc3Bvc2UoJzwwIDE%2BLzgnLm0pLnRyYW5zcG9zZSgtMSk7CiAgY29uc3Qgc3ludGhzID0gc3RhY2soCiAgICAnPGViNCBkNCBjNCBiMz4vMicubS5zY2FsZSh0aW1lQ2F0KFszLCdDIG1pbm9yJ10sWzEsJ0MgbWVsb2RpYyBtaW5vciddKS5zbG93KDgpKS5ncm9vdmUoJ1t%2BIHhdKjInLm0pCiAgICAuZWRpdCgKICAgICAgc2NhbGVUcmFuc3Bvc2UoMCkuZWFybHkoMCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDIpLmVhcmx5KDEvOCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDcpLmVhcmx5KDEvNCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDgpLmVhcmx5KDMvOCkKICAgICkuZWRpdCh0aHJ1KS50b25lKGtleXMpLmJ5cGFzcygnPDEgMD4vMTYnLm0pLAogICAgJzxDMiBCYjEgQWIxIFtHMSBbRzIgRzFdXT4vMicubS5ncm9vdmUoJ1t4IFt%2BIHhdIDxbfiBbfiB4XV0hMyBbeCB4XT5AMl0vMicubS5mYXN0KDIpKS5lZGl0KHRocnUpLnRvbmUoYmFzcyksCiAgICAnPENtNyBCYjcgRm03IEc3YjEzPi8yJy5tLmdyb292ZSgnfiBbeEAwLjEgfl0nLm0uZmFzdCgyKSkudm9pY2luZ3MoKS5lZGl0KHRocnUpLmV2ZXJ5KDIsIGVhcmx5KDEvOCkpLnRvbmUoa2V5cykuYnlwYXNzKCc8MEA3IDE%2BLzgnLm0uZWFybHkoMS80KSkKICApCiAgcmV0dXJuIHN0YWNrKAogICAgZHJ1bXMuZmFzdCgyKSwgCiAgICBzeW50aHMKICApLnNsb3coMik7Cn0%3D) [Open this track in the REPL](https://strudel.tidalcycles.org/#KCkgPT4gewogIGNvbnN0IGRlbGF5ID0gbmV3IEZlZWRiYWNrRGVsYXkoMS84LCAuNCkuY2hhaW4odm9sKDAuNSksIG91dCk7CiAgY29uc3Qga2ljayA9IG5ldyBNZW1icmFuZVN5bnRoKCkuY2hhaW4odm9sKC44KSwgb3V0KTsKICBjb25zdCBzbmFyZSA9IG5ldyBOb2lzZVN5bnRoKCkuY2hhaW4odm9sKC44KSwgb3V0KTsKICBjb25zdCBoaWhhdCA9IG5ldyBNZXRhbFN5bnRoKCkuc2V0KGFkc3IoMCwgLjA4LCAwLCAuMSkpLmNoYWluKHZvbCguMykuY29ubmVjdChkZWxheSksb3V0KTsKICBjb25zdCBiYXNzID0gbmV3IFN5bnRoKCkuc2V0KHsgLi4ub3NjKCdzYXd0b290aCcpLCAuLi5hZHNyKDAsIC4xLCAuNCkgfSkuY2hhaW4obG93cGFzcyg5MDApLCB2b2woLjUpLCBvdXQpOwogIGNvbnN0IGtleXMgPSBuZXcgUG9seVN5bnRoKCkuc2V0KHsgLi4ub3NjKCdzYXd0b290aCcpLCAuLi5hZHNyKDAsIC41LCAuMiwgLjcpIH0pLmNoYWluKGxvd3Bhc3MoMTIwMCksIHZvbCguNSksIG91dCk7CiAgCiAgY29uc3QgZHJ1bXMgPSBzdGFjaygKICAgICdjMSoyJy5tLnRvbmUoa2ljaykuYnlwYXNzKCc8MEA3IDE%2BLzgnLm0pLAogICAgJ34gPHghNyBbeEAzIHhdPicubS50b25lKHNuYXJlKS5ieXBhc3MoJzwwQDcgMT4vNCcubSksCiAgICAnW34gYzRdKjInLm0udG9uZShoaWhhdCkKICApOwogIAogIGNvbnN0IHRocnUgPSAoeCkgPT4geC50cmFuc3Bvc2UoJzwwIDE%2BLzgnLm0pLnRyYW5zcG9zZSgtMSk7CiAgY29uc3Qgc3ludGhzID0gc3RhY2soCiAgICAnPGViNCBkNCBjNCBiMz4vMicubS5zY2FsZSh0aW1lQ2F0KFszLCdDIG1pbm9yJ10sWzEsJ0MgbWVsb2RpYyBtaW5vciddKS5zbG93KDgpKS5ncm9vdmUoJ1t%2BIHhdKjInLm0pCiAgICAuZWRpdCgKICAgICAgc2NhbGVUcmFuc3Bvc2UoMCkuZWFybHkoMCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDIpLmVhcmx5KDEvOCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDcpLmVhcmx5KDEvNCksCiAgICAgIHNjYWxlVHJhbnNwb3NlKDgpLmVhcmx5KDMvOCkKICAgICkuZWRpdCh0aHJ1KS50b25lKGtleXMpLmJ5cGFzcygnPDEgMD4vMTYnLm0pLAogICAgJzxDMiBCYjEgQWIxIFtHMSBbRzIgRzFdXT4vMicubS5ncm9vdmUoJ1t4IFt%2BIHhdIDxbfiBbfiB4XV0hMyBbeCB4XT5AMl0vMicubS5mYXN0KDIpKS5lZGl0KHRocnUpLnRvbmUoYmFzcyksCiAgICAnPENtNyBCYjcgRm03IEc3YjEzPi8yJy5tLmdyb292ZSgnfiBbeEAwLjEgfl0nLm0uZmFzdCgyKSkudm9pY2luZ3MoKS5lZGl0KHRocnUpLmV2ZXJ5KDIsIGVhcmx5KDEvOCkpLnRvbmUoa2V5cykuYnlwYXNzKCc8MEA3IDE%2BLzgnLm0uZWFybHkoMS80KSkKICApCiAgcmV0dXJuIHN0YWNrKAogICAgZHJ1bXMuZmFzdCgyKSwgCiAgICBzeW50aHMKICApLnNsb3coMik7Cn0%3D)
@ -86,7 +85,6 @@ Before diving deeper into the details, here is a flavor of how the mini language
[[a1 a2]*4] [[a1 a2]*4]
] ]
]/16\``} ]/16\``}
height={600}
/> />
The snippet above is enclosed in backticks (`), which allows you to write multi-line strings. The snippet above is enclosed in backticks (`), which allows you to write multi-line strings.
@ -263,7 +261,6 @@ You can nest functions inside one another:
stack(b3,d3,fs4), stack(b3,d3,fs4),
stack(b3,e4,g4) stack(b3,e4,g4)
)`} )`}
height={200}
/> />
The above is equivalent to The above is equivalent to
@ -324,7 +321,7 @@ Note that we have to wrap b4 in **pure** to be able to call a the pattern modifi
There is the shorthand **p** for this: There is the shorthand **p** for this:
<MiniRepl tune={`cat(e5, b4.p.late(0.5))`} /> <MiniRepl tune={`cat(e5, b4.late(0.5))`} />
### late(cycles) ### late(cycles)
@ -344,11 +341,11 @@ Will reverse the pattern:
Will apply the given function every n cycles: Will apply the given function every n cycles:
<MiniRepl tune={`cat(e5, b4.p.every(4, late(0.5)))`} /> <MiniRepl tune={`cat(e5, pure(b4).every(4, late(0.5)))`} />
Note that late is called directly. This is a shortcut for: Note that late is called directly. This is a shortcut for:
<MiniRepl tune={`cat(e5, b4.p.every(4, x => x.late(0.5)))`} /> <MiniRepl tune={`cat(e5, pure(b4).every(4, x => x.late(0.5)))`} />
TODO: should the function really run the first cycle? TODO: should the function really run the first cycle?
@ -377,15 +374,14 @@ To make the sounds more interesting, we can use Tone.js instruments ands effects
<MiniRepl <MiniRepl
tune={`stack( tune={`stack(
"[c5 c5 bb4 c5] [~ g4 ~ g4] [c5 f5 e5 c5] ~".m "[c5 c5 bb4 c5] [~ g4 ~ g4] [c5 f5 e5 c5] ~"
.tone(synth(adsr(0,.1,0,0)).chain(out)), .tone(synth(adsr(0,.1,0,0)).chain(out)),
"[c2 c3]*8".m "[c2 c3]*8"
.tone(synth({ .tone(synth({
...osc('sawtooth'), ...osc('sawtooth'),
...adsr(0,.1,0.4,0) ...adsr(0,.1,0.4,0)
}).chain(lowpass(300), out)) }).chain(lowpass(300), out))
).slow(4)`} ).slow(4)`}
height={300}
/> />
### tone(instrument) ### tone(instrument)
@ -393,7 +389,7 @@ To make the sounds more interesting, we can use Tone.js instruments ands effects
To change the instrument of a pattern, you can pass any [Tone.js Source](https://tonejs.github.io/docs/14.7.77/index.html) to .tone: To change the instrument of a pattern, you can pass any [Tone.js Source](https://tonejs.github.io/docs/14.7.77/index.html) to .tone:
<MiniRepl <MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".m.slow(4) tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
.tone(new FMSynth().toDestination())`} .tone(new FMSynth().toDestination())`}
/> />
@ -418,7 +414,7 @@ const synth = (options) => new Synth(options);
Shortcut for Tone.Destination. Intended to be used with Tone's .chain: Shortcut for Tone.Destination. Intended to be used with Tone's .chain:
<MiniRepl <MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".m.slow(4) tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
.tone(membrane().chain(out))`} .tone(membrane().chain(out))`}
/> />
@ -429,7 +425,7 @@ This alone is not really useful, so read on..
Helper that returns a Gain Node with the given volume. Intended to be used with Tone's .chain: Helper that returns a Gain Node with the given volume. Intended to be used with Tone's .chain:
<MiniRepl <MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".m.slow(4) tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
.tone(noise().chain(vol(0.5), out))`} .tone(noise().chain(vol(0.5), out))`}
/> />
@ -438,7 +434,7 @@ Helper that returns a Gain Node with the given volume. Intended to be used with
Helper to set the waveform of a synth, monosynth or polysynth: Helper to set the waveform of a synth, monosynth or polysynth:
<MiniRepl <MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".m.slow(4) tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
.tone(synth(osc('sawtooth4')).chain(out))`} .tone(synth(osc('sawtooth4')).chain(out))`}
/> />
@ -449,7 +445,7 @@ The base types are `sine`, `square`, `sawtooth`, `triangle`. You can also append
Helper that returns a Filter Node of type lowpass with the given cutoff. Intended to be used with Tone's .chain: Helper that returns a Filter Node of type lowpass with the given cutoff. Intended to be used with Tone's .chain:
<MiniRepl <MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".m.slow(4) tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
.tone(synth(osc('sawtooth')).chain(lowpass(800), out))`} .tone(synth(osc('sawtooth')).chain(lowpass(800), out))`}
/> />
@ -458,7 +454,7 @@ Helper that returns a Filter Node of type lowpass with the given cutoff. Intende
Helper that returns a Filter Node of type highpass with the given cutoff. Intended to be used with Tone's .chain: Helper that returns a Filter Node of type highpass with the given cutoff. Intended to be used with Tone's .chain:
<MiniRepl <MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".m.slow(4) tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
.tone(synth(osc('sawtooth')).chain(highpass(2000), out))`} .tone(synth(osc('sawtooth')).chain(highpass(2000), out))`}
/> />
@ -467,7 +463,7 @@ Helper that returns a Filter Node of type highpass with the given cutoff. Intend
Helper to set the envelope of a Tone.js instrument. Intended to be used with Tone's .set: Helper to set the envelope of a Tone.js instrument. Intended to be used with Tone's .set:
<MiniRepl <MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".m.slow(4) tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".slow(4)
.tone(synth(adsr(0,.1,0,0)).chain(out))`} .tone(synth(adsr(0,.1,0,0)).chain(out))`}
/> />
@ -482,8 +478,8 @@ It would be great to get this to work without glitches though, because it is fun
With .synth, you can create a synth with a variable wave type: With .synth, you can create a synth with a variable wave type:
<MiniRepl <MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".m. tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~"
synth('<sawtooth8 square8>'.m).slow(4)`} .synth("<sawtooth8 square8>").slow(4)`}
/> />
#### adsr(attack, decay?, sustain?, release?) #### adsr(attack, decay?, sustain?, release?)
@ -491,8 +487,8 @@ synth('<sawtooth8 square8>'.m).slow(4)`}
Chainable Envelope helper: Chainable Envelope helper:
<MiniRepl <MiniRepl
tune={`"[c5 c5 bb4 c5] [~ g4 ~ g4] [c5 f5 e5 c5] ~".m.slow(4). tune={`"[c5 c5 bb4 c5] [~ g4 ~ g4] [c5 f5 e5 c5] ~".slow(4)
synth('sawtooth16').adsr(0,.1,0,0)`} .synth('sawtooth16').adsr(0,.1,0,0)`}
/> />
Due to having more than one argument, this method is not patternified. Due to having more than one argument, this method is not patternified.
@ -502,8 +498,8 @@ Due to having more than one argument, this method is not patternified.
Patternified filter: Patternified filter:
<MiniRepl <MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".m. tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~"
synth('sawtooth16').filter('[500 2000]*8'.m).slow(4)`} .synth('sawtooth16').filter("[500 2000]*8").slow(4)`}
/> />
#### gain(value) #### gain(value)
@ -511,8 +507,8 @@ synth('sawtooth16').filter('[500 2000]*8'.m).slow(4)`}
Patternified gain: Patternified gain:
<MiniRepl <MiniRepl
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".m. tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~"
synth('sawtooth16').gain('[.2 .8]*8'.m).slow(4)`} .synth('sawtooth16').gain("[.2 .8]*8").slow(4)`}
/> />
#### autofilter(value) #### autofilter(value)
@ -520,8 +516,8 @@ synth('sawtooth16').gain('[.2 .8]*8'.m).slow(4)`}
Patternified autofilter: Patternified autofilter:
<MiniRepl <MiniRepl
tune={`"c2 c3".m. tune={`"c2 c3"
synth('sawtooth16').autofilter('<1 4 8>'.m)`} .synth('sawtooth16').autofilter("<1 4 8>"")`}
/> />
## Tonal API ## Tonal API
@ -532,20 +528,20 @@ The Tonal API, uses [tonaljs](https://github.com/tonaljs/tonal) to provide helpe
Transposes all notes to the given number of semitones: Transposes all notes to the given number of semitones:
<MiniRepl tune={`"c2 c3".m.fast(2).transpose('<0 -2 5 3>'.m.slow(2)).transpose(0)`} /> <MiniRepl tune={`"c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).transpose(0)`} />
This method gets really exciting when we use it with a pattern as above. This method gets really exciting when we use it with a pattern as above.
Instead of numbers, scientific interval notation can be used as well: Instead of numbers, scientific interval notation can be used as well:
<MiniRepl tune={`"c2 c3".m.fast(2).transpose('<1P -2M 4P 3m>'.m.slow(2)).transpose(1)`} /> <MiniRepl tune={`"c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).transpose(1)`} />
### scale(name) ### scale(name)
Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like scaleTranpose. Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like scaleTranpose.
<MiniRepl <MiniRepl
tune={`"0 2 4 6 4 2".m tune={`"0 2 4 6 4 2"
.scale(slowcat('C2 major', 'C2 minor').slow(2))`} .scale(slowcat('C2 major', 'C2 minor').slow(2))`}
/> />
@ -558,16 +554,16 @@ All the available scale names can be found [here](https://github.com/tonaljs/ton
Transposes notes inside the scale by the number of steps: Transposes notes inside the scale by the number of steps:
<MiniRepl <MiniRepl
tune={`"-8 [2,4,6]".m tune={`"-8 [2,4,6]"
.scale('C4 bebop major') .scale('C4 bebop major')
.scaleTranspose('<0 -1 -2 -3 -4 -5 -6 -4>'.m)`} .scaleTranspose("<0 -1 -2 -3 -4 -5 -6 -4>")`}
/> />
### voicings(range?) ### voicings(range?)
Turns chord symbols into voicings, using the smoothest voice leading possible: Turns chord symbols into voicings, using the smoothest voice leading possible:
<MiniRepl tune={`stack("<C^7 A7 Dm7 G7>".m.voicings(), '<C3 A2 D3 G2>'.m)`} /> <MiniRepl tune={`stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>")`} />
TODO: use voicing collection as first param + patternify. TODO: use voicing collection as first param + patternify.
@ -575,16 +571,15 @@ TODO: use voicing collection as first param + patternify.
Turns chord symbols into root notes of chords in given octave. Turns chord symbols into root notes of chords in given octave.
<MiniRepl tune={`"<C^7 A7b13 Dm7 G7>".m.rootNotes(3)`} /> <MiniRepl tune={`"<C^7 A7b13 Dm7 G7>".rootNotes(3)`} />
Together with edit, groove and voicings, this can be used to create a basic backing track: Together with edit, groove and voicings, this can be used to create a basic backing track:
<MiniRepl <MiniRepl
tune={`"<C^7 A7b13 Dm7 G7>".m.edit( tune={`"<C^7 A7b13 Dm7 G7>".edit(
x => x.voicings(['d3','g4']).groove('~ x'.m), x => x.voicings(['d3','g4']).groove("~ x"),
x => x.rootNotes(2).tone(synth(osc('sawtooth4')).chain(out)) x => x.rootNotes(2).tone(synth(osc('sawtooth4')).chain(out))
)`} )`}
height={150}
/> />
TODO: use range instead of octave. TODO: use range instead of octave.
@ -604,7 +599,7 @@ Midi is currently not supported by the mini repl used here, but you can [open th
In the REPL, you will se a log of the available MIDI devices. In the REPL, you will se a log of the available MIDI devices.
<!--<MiniRepl <!--<MiniRepl
tune={`stack("<C^7 A7 Dm7 G7>".m.voicings(), '<C3 A2 D3 G2>'.m) tune={`stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>")
.midi()`} .midi()`}
/>--> />-->

View File

@ -17,8 +17,10 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent }: any) {
const [activeCode, setActiveCode] = useState<string>(); const [activeCode, setActiveCode] = useState<string>();
const [log, setLog] = useState(''); const [log, setLog] = useState('');
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();
const [hash, setHash] = useState('');
const [pattern, setPattern] = useState<Pattern>(); const [pattern, setPattern] = useState<Pattern>();
const dirty = code !== activeCode; const dirty = code !== activeCode || error;
const generateHash = () => encodeURIComponent(btoa(code));
const activateCode = (_code = code) => { const activateCode = (_code = code) => {
!cycle.started && cycle.start(); !cycle.started && cycle.start();
broadcast({ type: 'start', from: id }); broadcast({ type: 'start', from: id });
@ -32,11 +34,12 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent }: any) {
if (autolink) { if (autolink) {
window.location.hash = '#' + encodeURIComponent(btoa(code)); window.location.hash = '#' + encodeURIComponent(btoa(code));
} }
setHash(generateHash());
setError(undefined); setError(undefined);
setActiveCode(_code); setActiveCode(_code);
} catch (err: any) { } catch (err: any) {
err.message = 'evaluation error: ' + err.message; err.message = 'evaluation error: ' + err.message;
console.warn(err) console.warn(err);
setError(err); setError(err);
} }
}; };
@ -153,6 +156,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent }: any) {
activateCode, activateCode,
activeCode, activeCode,
pushLog, pushLog,
hash,
}; };
} }

View File

@ -620,6 +620,9 @@ class Pattern {
edit(...funcs) { edit(...funcs) {
return stack(...funcs.map(func => func(this))); return stack(...funcs.map(func => func(this)));
} }
pipe(func) {
return func(this);
}
_bypass(on) { _bypass(on) {
on = Boolean(parseInt(on)); on = Boolean(parseInt(on));