mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 21:58:31 +00:00
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:
parent
9a480344de
commit
522005a7ab
@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import * as Tone from 'tone';
|
||||
import CodeMirror from './CodeMirror';
|
||||
import CodeMirror, { markEvent } from './CodeMirror';
|
||||
import cx from './cx';
|
||||
import { evaluate } from './evaluate';
|
||||
import logo from './logo.svg';
|
||||
@ -36,32 +36,10 @@ const randomTune = getRandomTune();
|
||||
|
||||
function App() {
|
||||
const [editor, setEditor] = useState<any>();
|
||||
const doc = useMemo(() => editor?.getDoc(), [editor]);
|
||||
const { setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog } = useRepl({
|
||||
tune: decoded || randomTune,
|
||||
defaultSynth,
|
||||
onEvent: useCallback(
|
||||
(event) => {
|
||||
const locs = event.value.locations;
|
||||
if (!locs) {
|
||||
return;
|
||||
}
|
||||
// mark active event
|
||||
const marks = locs.map(({ start, end }) =>
|
||||
doc.markText(
|
||||
{ line: start.line - 1, ch: start.column },
|
||||
{ line: end.line - 1, ch: end.column },
|
||||
{ css: 'background-color: gray;' }
|
||||
)
|
||||
);
|
||||
//Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler...
|
||||
setTimeout(() => {
|
||||
marks.forEach((mark) => mark.clear());
|
||||
// }, '+' + event.duration * 0.5);
|
||||
}, event.duration * 0.9 * 1000);
|
||||
},
|
||||
[doc]
|
||||
),
|
||||
onEvent: useCallback(markEvent(editor), [editor]),
|
||||
});
|
||||
const logBox = useRef<any>();
|
||||
// scroll log box to bottom when log changes
|
||||
@ -136,6 +114,7 @@ function App() {
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
styleSelectedText: true,
|
||||
cursorBlinkRate: 0,
|
||||
}}
|
||||
onChange={(_: any, __: any, value: any) => setCode(value)}
|
||||
/>
|
||||
|
||||
@ -2,8 +2,9 @@ import React from 'react';
|
||||
import { Controlled as CodeMirror2 } from 'react-codemirror2';
|
||||
import 'codemirror/mode/javascript/javascript.js';
|
||||
import 'codemirror/mode/pegjs/pegjs.js';
|
||||
import 'codemirror/theme/material.css';
|
||||
// import 'codemirror/theme/material.css';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'codemirror/theme/material.css';
|
||||
|
||||
export default function CodeMirror({ value, onChange, options, editorDidMount }: any) {
|
||||
options = options || {
|
||||
@ -11,6 +12,29 @@ export default function CodeMirror({ value, onChange, options, editorDidMount }:
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
styleSelectedText: true,
|
||||
cursorBlinkRate: 500,
|
||||
};
|
||||
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);
|
||||
};
|
||||
|
||||
@ -37,10 +37,12 @@ export const evaluate: any = (code: string) => {
|
||||
if (typeof evaluated === 'function') {
|
||||
evaluated = evaluated();
|
||||
}
|
||||
const pattern = minify(evaluated); // eval and minify (if user entered a string)
|
||||
if (pattern?.constructor?.name !== 'Pattern') {
|
||||
const message = `got "${typeof pattern}" instead of pattern`;
|
||||
throw new Error(message + (typeof pattern === 'function' ? ', did you forget to call a function?' : '.'));
|
||||
if (typeof evaluated === 'string') {
|
||||
evaluated = strudel.withLocationOffset(minify(evaluated), { start: { line: 1, column: -1 } });
|
||||
}
|
||||
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 };
|
||||
};
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
import { parseScriptWithLocation } from './shift-parser/index.js'; // npm module does not work in the browser
|
||||
import traverser from './shift-traverser'; // npm module does not work in the browser
|
||||
const { replace } = traverser;
|
||||
import { LiteralStringExpression, IdentifierExpression, CallExpression, StaticMemberExpression } from 'shift-ast';
|
||||
import {
|
||||
LiteralStringExpression,
|
||||
IdentifierExpression,
|
||||
CallExpression,
|
||||
StaticMemberExpression,
|
||||
Script,
|
||||
} from 'shift-ast';
|
||||
import codegen from 'shift-codegen';
|
||||
import * as strudel from '../../strudel.mjs';
|
||||
|
||||
@ -12,20 +18,50 @@ const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
|
||||
const addLocations = true;
|
||||
export const addMiniLocations = true;
|
||||
|
||||
/*
|
||||
not supported for highlighting:
|
||||
- 'b3'.p
|
||||
- mini('b3') / m('b3')
|
||||
- 'b3'.m / 'b3'.mini
|
||||
*/
|
||||
|
||||
export default (code) => {
|
||||
const ast = parseScriptWithLocation(code);
|
||||
const nodesWithLocation = [];
|
||||
const artificialNodes = [];
|
||||
const parents = [];
|
||||
const shifted = replace(ast.tree, {
|
||||
enter(node, parent) {
|
||||
parents.push(parent);
|
||||
const isSynthetic = parents.some((p) => nodesWithLocation.includes(p));
|
||||
const isSynthetic = parents.some((p) => artificialNodes.includes(p));
|
||||
if (isSynthetic) {
|
||||
return node;
|
||||
}
|
||||
const grandparent = parents[parents.length - 2];
|
||||
const isTimeCat = parent?.type === 'ArrayExpression' && isPatternFactory(grandparent);
|
||||
const isMarkable = isPatternFactory(parent) || isTimeCat;
|
||||
|
||||
// replace template string `xxx` with 'xxx'.m
|
||||
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
|
||||
const operators = {
|
||||
'*': 'fast',
|
||||
@ -41,22 +77,28 @@ export default (code) => {
|
||||
) {
|
||||
let arg = node.left;
|
||||
if (node.left.type === 'IdentifierExpression') {
|
||||
arg = wrapReify(node.left);
|
||||
arg = wrapFunction('reify', node.left);
|
||||
}
|
||||
return new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
property: operators[node.operator],
|
||||
object: wrapReify(arg),
|
||||
object: wrapFunction('reify', arg),
|
||||
}),
|
||||
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
|
||||
if (node.type === 'IdentifierExpression') {
|
||||
if (isNote(node.name)) {
|
||||
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
|
||||
if (addLocations && isMarkable) {
|
||||
return reifyWithLocation(value, node, ast.locations, nodesWithLocation);
|
||||
return reifyWithLocation(value, node, ast.locations, artificialNodes);
|
||||
}
|
||||
return new LiteralStringExpression({ value });
|
||||
}
|
||||
@ -66,10 +108,10 @@ export default (code) => {
|
||||
}
|
||||
if (addLocations && node.type === 'LiteralStringExpression' && isMarkable) {
|
||||
// console.log('add', node);
|
||||
return reifyWithLocation(node.value, node, ast.locations, nodesWithLocation);
|
||||
return reifyWithLocation(node.value, node, ast.locations, artificialNodes);
|
||||
}
|
||||
if (!addMiniLocations) {
|
||||
return node;
|
||||
return wrapFunction('reify', node);
|
||||
}
|
||||
// mini notation location handling
|
||||
const miniFunctions = ['mini', 'm'];
|
||||
@ -81,11 +123,11 @@ export default (code) => {
|
||||
console.warn('multi arg mini locations not supported yet...');
|
||||
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) {
|
||||
// 'c3'.mini or 'c3'.m
|
||||
return wrapLocationOffset(node, node.object, ast.locations, nodesWithLocation);
|
||||
return wrapLocationOffset(node, node.object, ast.locations, artificialNodes);
|
||||
}
|
||||
return node;
|
||||
},
|
||||
@ -96,15 +138,58 @@ export default (code) => {
|
||||
return codegen(shifted);
|
||||
};
|
||||
|
||||
function wrapReify(node) {
|
||||
function wrapFunction(name, ...arguments) {
|
||||
return new CallExpression({
|
||||
callee: new IdentifierExpression({
|
||||
name: 'reify',
|
||||
}),
|
||||
arguments: [node],
|
||||
callee: new IdentifierExpression({ name }),
|
||||
arguments,
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
function wrapLocationOffset(node, stringNode, locations, nodesWithLocation) {
|
||||
function wrapLocationOffset(node, stringNode, locations, artificialNodes) {
|
||||
// console.log('wrapppp', stringNode);
|
||||
const expression = {
|
||||
type: 'CallExpression',
|
||||
@ -125,28 +210,22 @@ function wrapLocationOffset(node, stringNode, locations, nodesWithLocation) {
|
||||
},
|
||||
arguments: [node, getLocationObject(stringNode, locations)],
|
||||
};
|
||||
nodesWithLocation.push(expression);
|
||||
artificialNodes.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);
|
||||
function reifyWithLocation(value, node, locations, artificialNodes) {
|
||||
const withLocation = new CallExpression({
|
||||
callee: new StaticMemberExpression({
|
||||
object: new CallExpression({
|
||||
callee: new IdentifierExpression({
|
||||
name: 'reify',
|
||||
}),
|
||||
arguments: [new LiteralStringExpression({ value })],
|
||||
}),
|
||||
object: wrapFunction('reify', new LiteralStringExpression({ value })),
|
||||
property: 'withLocation',
|
||||
}),
|
||||
arguments: [getLocationObject(node, locations)],
|
||||
});
|
||||
nodesWithLocation.push(withLocation);
|
||||
artificialNodes.push(withLocation);
|
||||
return withLocation;
|
||||
}
|
||||
|
||||
|
||||
@ -440,3 +440,20 @@ export const caverave = `() => {
|
||||
synths
|
||||
).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)
|
||||
}
|
||||
`;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import * as Tone from 'tone';
|
||||
import useRepl from '../useRepl';
|
||||
import CodeMirror from '../CodeMirror';
|
||||
import CodeMirror, { markEvent } from '../CodeMirror';
|
||||
import cx from '../cx';
|
||||
|
||||
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 }) {
|
||||
const { code, setCode, activateCode, activeCode, setPattern, error, cycle, dirty, log, togglePlay } = useRepl({
|
||||
function MiniRepl({ tune, maxHeight = 500 }) {
|
||||
const [editor, setEditor] = useState<any>();
|
||||
const { code, setCode, activateCode, activeCode, setPattern, error, cycle, dirty, log, togglePlay, hash } = useRepl({
|
||||
tune,
|
||||
defaultSynth,
|
||||
autolink: false,
|
||||
onEvent: useCallback(markEvent(editor), [editor]),
|
||||
});
|
||||
const lines = code.split('\n').length;
|
||||
const height = Math.min(lines * 30 + 30, maxHeight);
|
||||
return (
|
||||
<div className="flex space-y-0 overflow-auto" style={{ height }}>
|
||||
<div className="w-16 flex flex-col">
|
||||
<button
|
||||
className="grow bg-slate-700 border-b border-slate-500 text-white hover:bg-slate-600 "
|
||||
onClick={() => togglePlay()}
|
||||
>
|
||||
{cycle.started ? 'pause' : 'play'}
|
||||
</button>
|
||||
<button
|
||||
className={cx(
|
||||
'grow border-slate-500 hover:bg-slate-600',
|
||||
activeCode && dirty ? 'bg-slate-700 text-white' : 'bg-slate-600 text-slate-400 cursor-not-allowed'
|
||||
)}
|
||||
onClick={() => activateCode()}
|
||||
>
|
||||
update
|
||||
</button>
|
||||
<div className="rounded-md overflow-hidden">
|
||||
<div className="flex justify-between bg-slate-700 border-t border-slate-500">
|
||||
<div className="flex">
|
||||
<button
|
||||
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 ? 'animate-pulse' : ''
|
||||
)}
|
||||
onClick={() => togglePlay()}
|
||||
>
|
||||
{!cycle.started ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
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"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</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>
|
||||
<CodeMirror
|
||||
className="w-full"
|
||||
value={code}
|
||||
options={{
|
||||
mode: 'javascript',
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
}}
|
||||
onChange={(_: any, __: any, value: any) => setCode(value)}
|
||||
/>
|
||||
{/* <textarea className="w-full" value={code} onChange={(e) => setCode(e.target.value)} /> */}
|
||||
<div className="flex space-y-0 overflow-auto" style={{ height }}>
|
||||
<CodeMirror
|
||||
className="w-full"
|
||||
value={code}
|
||||
editorDidMount={setEditor}
|
||||
options={{
|
||||
mode: 'javascript',
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
}}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 drums = stack(
|
||||
'c1*2'.m.tone(kick).bypass('<0@7 1>/8'.m),
|
||||
'~ <x!7 [x@3 x]>'.m.tone(snare).bypass('<0@7 1>/4'.m),
|
||||
'[~ c4]*2'.m.tone(hihat)
|
||||
"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'.m).transpose(-1);
|
||||
const thru = (x) => x.transpose("<0 1>/8").transpose(-1);
|
||||
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(
|
||||
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'.m),
|
||||
'<C2 Bb1 Ab1 [G1 [G2 G1]]>/2'.m.groove('[x [~ x] <[~ [~ x]]!3 [x x]>@2]/2'.m.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))
|
||||
).edit(thru).tone(keys).bypass("<1 0>/16"),
|
||||
"<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".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(
|
||||
drums.fast(2),
|
||||
synths
|
||||
).slow(2);
|
||||
}`}
|
||||
height={400}
|
||||
/>
|
||||
|
||||
[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]
|
||||
]
|
||||
]/16\``}
|
||||
height={600}
|
||||
/>
|
||||
|
||||
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,e4,g4)
|
||||
)`}
|
||||
height={200}
|
||||
/>
|
||||
|
||||
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:
|
||||
|
||||
<MiniRepl tune={`cat(e5, b4.p.late(0.5))`} />
|
||||
<MiniRepl tune={`cat(e5, b4.late(0.5))`} />
|
||||
|
||||
### late(cycles)
|
||||
|
||||
@ -344,11 +341,11 @@ Will reverse the pattern:
|
||||
|
||||
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:
|
||||
|
||||
<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?
|
||||
|
||||
@ -377,15 +374,14 @@ To make the sounds more interesting, we can use Tone.js instruments ands effects
|
||||
|
||||
<MiniRepl
|
||||
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)),
|
||||
"[c2 c3]*8".m
|
||||
"[c2 c3]*8"
|
||||
.tone(synth({
|
||||
...osc('sawtooth'),
|
||||
...adsr(0,.1,0.4,0)
|
||||
}).chain(lowpass(300), out))
|
||||
).slow(4)`}
|
||||
height={300}
|
||||
/>
|
||||
|
||||
### 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:
|
||||
|
||||
<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())`}
|
||||
/>
|
||||
|
||||
@ -418,7 +414,7 @@ const synth = (options) => new Synth(options);
|
||||
Shortcut for Tone.Destination. Intended to be used with Tone's .chain:
|
||||
|
||||
<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))`}
|
||||
/>
|
||||
|
||||
@ -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:
|
||||
|
||||
<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))`}
|
||||
/>
|
||||
|
||||
@ -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:
|
||||
|
||||
<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))`}
|
||||
/>
|
||||
|
||||
@ -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:
|
||||
|
||||
<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))`}
|
||||
/>
|
||||
|
||||
@ -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:
|
||||
|
||||
<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))`}
|
||||
/>
|
||||
|
||||
@ -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:
|
||||
|
||||
<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))`}
|
||||
/>
|
||||
|
||||
@ -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:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".m.
|
||||
synth('<sawtooth8 square8>'.m).slow(4)`}
|
||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~"
|
||||
.synth("<sawtooth8 square8>").slow(4)`}
|
||||
/>
|
||||
|
||||
#### adsr(attack, decay?, sustain?, release?)
|
||||
@ -491,8 +487,8 @@ synth('<sawtooth8 square8>'.m).slow(4)`}
|
||||
Chainable Envelope helper:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"[c5 c5 bb4 c5] [~ g4 ~ g4] [c5 f5 e5 c5] ~".m.slow(4).
|
||||
synth('sawtooth16').adsr(0,.1,0,0)`}
|
||||
tune={`"[c5 c5 bb4 c5] [~ g4 ~ g4] [c5 f5 e5 c5] ~".slow(4)
|
||||
.synth('sawtooth16').adsr(0,.1,0,0)`}
|
||||
/>
|
||||
|
||||
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:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".m.
|
||||
synth('sawtooth16').filter('[500 2000]*8'.m).slow(4)`}
|
||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~"
|
||||
.synth('sawtooth16').filter("[500 2000]*8").slow(4)`}
|
||||
/>
|
||||
|
||||
#### gain(value)
|
||||
@ -511,8 +507,8 @@ synth('sawtooth16').filter('[500 2000]*8'.m).slow(4)`}
|
||||
Patternified gain:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~".m.
|
||||
synth('sawtooth16').gain('[.2 .8]*8'.m).slow(4)`}
|
||||
tune={`"[c4 c4 bb3 c4] [~ g3 ~ g3] [c4 f4 e4 c4] ~"
|
||||
.synth('sawtooth16').gain("[.2 .8]*8").slow(4)`}
|
||||
/>
|
||||
|
||||
#### autofilter(value)
|
||||
@ -520,8 +516,8 @@ synth('sawtooth16').gain('[.2 .8]*8'.m).slow(4)`}
|
||||
Patternified autofilter:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"c2 c3".m.
|
||||
synth('sawtooth16').autofilter('<1 4 8>'.m)`}
|
||||
tune={`"c2 c3"
|
||||
.synth('sawtooth16').autofilter("<1 4 8>"")`}
|
||||
/>
|
||||
|
||||
## 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:
|
||||
|
||||
<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.
|
||||
|
||||
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)
|
||||
|
||||
Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like scaleTranpose.
|
||||
|
||||
<MiniRepl
|
||||
tune={`"0 2 4 6 4 2".m
|
||||
tune={`"0 2 4 6 4 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:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"-8 [2,4,6]".m
|
||||
tune={`"-8 [2,4,6]"
|
||||
.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?)
|
||||
|
||||
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.
|
||||
|
||||
@ -575,16 +571,15 @@ TODO: use voicing collection as first param + patternify.
|
||||
|
||||
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:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"<C^7 A7b13 Dm7 G7>".m.edit(
|
||||
x => x.voicings(['d3','g4']).groove('~ x'.m),
|
||||
tune={`"<C^7 A7b13 Dm7 G7>".edit(
|
||||
x => x.voicings(['d3','g4']).groove("~ x"),
|
||||
x => x.rootNotes(2).tone(synth(osc('sawtooth4')).chain(out))
|
||||
)`}
|
||||
height={150}
|
||||
/>
|
||||
|
||||
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.
|
||||
|
||||
<!--<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()`}
|
||||
/>-->
|
||||
|
||||
|
||||
@ -17,8 +17,10 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent }: any) {
|
||||
const [activeCode, setActiveCode] = useState<string>();
|
||||
const [log, setLog] = useState('');
|
||||
const [error, setError] = useState<Error>();
|
||||
const [hash, setHash] = useState('');
|
||||
const [pattern, setPattern] = useState<Pattern>();
|
||||
const dirty = code !== activeCode;
|
||||
const dirty = code !== activeCode || error;
|
||||
const generateHash = () => encodeURIComponent(btoa(code));
|
||||
const activateCode = (_code = code) => {
|
||||
!cycle.started && cycle.start();
|
||||
broadcast({ type: 'start', from: id });
|
||||
@ -32,11 +34,12 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent }: any) {
|
||||
if (autolink) {
|
||||
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
||||
}
|
||||
setHash(generateHash());
|
||||
setError(undefined);
|
||||
setActiveCode(_code);
|
||||
} catch (err: any) {
|
||||
err.message = 'evaluation error: ' + err.message;
|
||||
console.warn(err)
|
||||
console.warn(err);
|
||||
setError(err);
|
||||
}
|
||||
};
|
||||
@ -153,6 +156,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent }: any) {
|
||||
activateCode,
|
||||
activeCode,
|
||||
pushLog,
|
||||
hash,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -620,6 +620,9 @@ class Pattern {
|
||||
edit(...funcs) {
|
||||
return stack(...funcs.map(func => func(this)));
|
||||
}
|
||||
pipe(func) {
|
||||
return func(this);
|
||||
}
|
||||
|
||||
_bypass(on) {
|
||||
on = Boolean(parseInt(on));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user