fix mini repl + improve repl api
This commit is contained in:
parent
14c2da4fa2
commit
c7c90b0647
9 changed files with 1413 additions and 1267 deletions
|
|
@ -23,7 +23,7 @@ export { default as gist } from './gist.js';
|
||||||
/* import * as p from './package.json';
|
/* import * as p from './package.json';
|
||||||
export const version = p.version; */
|
export const version = p.version; */
|
||||||
console.log(
|
console.log(
|
||||||
'%c // 🌀 @strudel.cycles/core loaded 🌀', // keep "//" for runnable snapshot source..
|
'%c 🌀 @strudel.cycles/core loaded 🌀', // keep "//" for runnable snapshot source..
|
||||||
'background-color: black;color:white;padding:4px;border-radius:15px',
|
'background-color: black;color:white;padding:4px;border-radius:15px',
|
||||||
);
|
);
|
||||||
if (globalThis._strudelLoaded) {
|
if (globalThis._strudelLoaded) {
|
||||||
|
|
|
||||||
|
|
@ -6,27 +6,30 @@ export function repl({
|
||||||
defaultOutput,
|
defaultOutput,
|
||||||
onSchedulerError,
|
onSchedulerError,
|
||||||
onEvalError,
|
onEvalError,
|
||||||
onEval,
|
beforeEval,
|
||||||
|
afterEval,
|
||||||
getTime,
|
getTime,
|
||||||
transpiler,
|
transpiler,
|
||||||
onToggle,
|
onToggle,
|
||||||
}) {
|
}) {
|
||||||
const scheduler = new Cyclist({ interval, onTrigger: defaultOutput, onError: onSchedulerError, getTime, onToggle });
|
const scheduler = new Cyclist({ interval, onTrigger: defaultOutput, onError: onSchedulerError, getTime, onToggle });
|
||||||
const evaluate = async (code) => {
|
const evaluate = async (code, autostart = true) => {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error('no code to evaluate');
|
throw new Error('no code to evaluate');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
beforeEval({ code });
|
||||||
const { pattern } = await _evaluate(code, transpiler);
|
const { pattern } = await _evaluate(code, transpiler);
|
||||||
scheduler.setPattern(pattern, true);
|
scheduler.setPattern(pattern, autostart);
|
||||||
onEval?.({
|
afterEval({ code, pattern });
|
||||||
pattern,
|
return pattern;
|
||||||
code,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`eval error: ${err.message}`);
|
console.warn(`eval error: ${err.message}`);
|
||||||
onEvalError?.(err);
|
onEvalError?.(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return { scheduler, evaluate };
|
const stop = () => scheduler.stop();
|
||||||
|
const start = () => scheduler.start();
|
||||||
|
const pause = () => scheduler.pause();
|
||||||
|
return { scheduler, evaluate, start, stop, pause };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ function peg$padEnd(str, targetLength, padString) {
|
||||||
}
|
}
|
||||||
|
|
||||||
peg$SyntaxError.prototype.format = function(sources) {
|
peg$SyntaxError.prototype.format = function(sources) {
|
||||||
var str = "Error: " + this.message;
|
var str = "peg error: " + this.message;
|
||||||
if (this.location) {
|
if (this.location) {
|
||||||
var src = null;
|
var src = null;
|
||||||
var k;
|
var k;
|
||||||
|
|
|
||||||
44
packages/react/dist/index.cjs.js
vendored
44
packages/react/dist/index.cjs.js
vendored
File diff suppressed because one or more lines are too long
2451
packages/react/dist/index.es.js
vendored
2451
packages/react/dist/index.es.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -7,20 +7,33 @@ import 'tailwindcss/tailwind.css';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
import styles from './MiniRepl.module.css';
|
import styles from './MiniRepl.module.css';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||||
// import { Tone } from '@strudel.cycles/tone';
|
import useStrudel from '../hooks/useStrudel.mjs';
|
||||||
|
|
||||||
export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableKeyboard }) {
|
const getTime = () => getAudioContext().currentTime;
|
||||||
return <p>TODO</p>;
|
|
||||||
/* const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
|
export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }) {
|
||||||
useRepl({
|
const {
|
||||||
tune,
|
code,
|
||||||
autolink: false,
|
setCode,
|
||||||
onEvent,
|
evaluate,
|
||||||
});
|
activateCode,
|
||||||
useEffect(() => {
|
error,
|
||||||
init && evaluateOnly();
|
isDirty,
|
||||||
}, [tune, init]);
|
activeCode,
|
||||||
|
pattern,
|
||||||
|
started,
|
||||||
|
scheduler,
|
||||||
|
togglePlay,
|
||||||
|
stop,
|
||||||
|
} = useStrudel({
|
||||||
|
initialCode: tune,
|
||||||
|
defaultOutput: webaudioOutput,
|
||||||
|
getTime,
|
||||||
|
});
|
||||||
|
/* useEffect(() => {
|
||||||
|
init && activateCode();
|
||||||
|
}, [init, activateCode]); */
|
||||||
const [view, setView] = useState();
|
const [view, setView] = useState();
|
||||||
const [ref, isVisible] = useInView({
|
const [ref, isVisible] = useInView({
|
||||||
threshold: 0.01,
|
threshold: 0.01,
|
||||||
|
|
@ -35,8 +48,8 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
||||||
useHighlighting({
|
useHighlighting({
|
||||||
view,
|
view,
|
||||||
pattern,
|
pattern,
|
||||||
active: cycle.started && !activeCode?.includes('strudel disable-highlighting'),
|
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||||
getTime: () => getAudioContext().seconds,
|
getTime: () => scheduler.phase,
|
||||||
});
|
});
|
||||||
|
|
||||||
// set active pattern on ctrl+enter
|
// set active pattern on ctrl+enter
|
||||||
|
|
@ -49,7 +62,7 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
||||||
flash(view);
|
flash(view);
|
||||||
await activateCode();
|
await activateCode();
|
||||||
} else if (e.code === 'Period') {
|
} else if (e.code === 'Period') {
|
||||||
cycle.stop();
|
stop();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,16 +70,16 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
||||||
window.addEventListener('keydown', handleKeyPress, true);
|
window.addEventListener('keydown', handleKeyPress, true);
|
||||||
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
||||||
}
|
}
|
||||||
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
|
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container} ref={ref}>
|
<div className={styles.container} ref={ref}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
<button className={cx(styles.button, cycle.started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
|
<button className={cx(styles.button, started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
|
||||||
<Icon type={cycle.started ? 'pause' : 'play'} />
|
<Icon type={started ? 'pause' : 'play'} />
|
||||||
</button>
|
</button>
|
||||||
<button className={cx(dirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
||||||
<Icon type="refresh" />
|
<Icon type="refresh" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -76,5 +89,5 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
|
||||||
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
|
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
); */
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,18 @@ import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { repl } from '@strudel.cycles/core/repl.mjs';
|
import { repl } from '@strudel.cycles/core/repl.mjs';
|
||||||
import { transpiler } from '@strudel.cycles/transpiler';
|
import { transpiler } from '@strudel.cycles/transpiler';
|
||||||
|
|
||||||
function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = false }) {
|
function useStrudel({ defaultOutput, interval, getTime, evalOnMount = false, initialCode = '' }) {
|
||||||
// scheduler
|
// scheduler
|
||||||
const [schedulerError, setSchedulerError] = useState();
|
const [schedulerError, setSchedulerError] = useState();
|
||||||
const [evalError, setEvalError] = useState();
|
const [evalError, setEvalError] = useState();
|
||||||
|
const [code, setCode] = useState(initialCode);
|
||||||
const [activeCode, setActiveCode] = useState(code);
|
const [activeCode, setActiveCode] = useState(code);
|
||||||
const [pattern, setPattern] = useState();
|
const [pattern, setPattern] = useState();
|
||||||
const [started, setStarted] = useState(false);
|
const [started, setStarted] = useState(false);
|
||||||
const isDirty = code !== activeCode;
|
const isDirty = code !== activeCode;
|
||||||
|
|
||||||
// TODO: make sure this hook reruns when scheduler.started changes
|
// TODO: make sure this hook reruns when scheduler.started changes
|
||||||
const { scheduler, evaluate: _evaluate } = useMemo(
|
const { scheduler, evaluate, start, stop, pause } = useMemo(
|
||||||
() =>
|
() =>
|
||||||
repl({
|
repl({
|
||||||
interval,
|
interval,
|
||||||
|
|
@ -20,37 +22,56 @@ function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = fals
|
||||||
onEvalError: setEvalError,
|
onEvalError: setEvalError,
|
||||||
getTime,
|
getTime,
|
||||||
transpiler,
|
transpiler,
|
||||||
onEval: ({ pattern: _pattern, code }) => {
|
beforeEval: ({ code }) => {
|
||||||
|
setCode(code);
|
||||||
|
},
|
||||||
|
onEvalError: setEvalError,
|
||||||
|
afterEval: ({ pattern: _pattern, code }) => {
|
||||||
setActiveCode(code);
|
setActiveCode(code);
|
||||||
setPattern(_pattern);
|
setPattern(_pattern);
|
||||||
setEvalError();
|
setEvalError();
|
||||||
},
|
},
|
||||||
onToggle: (v) => setStarted(v),
|
onToggle: (v) => setStarted(v),
|
||||||
onEvalError: setEvalError,
|
|
||||||
}),
|
}),
|
||||||
[defaultOutput, interval, getTime],
|
[defaultOutput, interval, getTime],
|
||||||
);
|
);
|
||||||
const evaluate = useCallback(() => _evaluate(code), [_evaluate, code]);
|
const activateCode = useCallback(async (autostart = true) => evaluate(code, autostart), [evaluate, code]);
|
||||||
|
|
||||||
const inited = useRef();
|
const inited = useRef();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!inited.current && evalOnMount && code) {
|
if (!inited.current && evalOnMount && code) {
|
||||||
console.log('eval on mount');
|
|
||||||
inited.current = true;
|
inited.current = true;
|
||||||
evaluate();
|
activateCode();
|
||||||
}
|
}
|
||||||
}, [evaluate, evalOnMount, code]);
|
}, [activateCode, evalOnMount, code]);
|
||||||
|
|
||||||
const togglePlay = async () => {
|
const togglePlay = async () => {
|
||||||
if (started) {
|
if (started) {
|
||||||
scheduler.pause();
|
scheduler.pause();
|
||||||
// scheduler.stop();
|
// scheduler.stop();
|
||||||
} else {
|
} else {
|
||||||
await evaluate();
|
await activateCode();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const error = schedulerError || evalError;
|
||||||
return { schedulerError, scheduler, evalError, evaluate, activeCode, isDirty, pattern, started, togglePlay };
|
return {
|
||||||
|
code,
|
||||||
|
setCode,
|
||||||
|
error,
|
||||||
|
schedulerError,
|
||||||
|
scheduler,
|
||||||
|
evalError,
|
||||||
|
evaluate,
|
||||||
|
activateCode,
|
||||||
|
activeCode,
|
||||||
|
isDirty,
|
||||||
|
pattern,
|
||||||
|
started,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
pause,
|
||||||
|
togglePlay,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useStrudel;
|
export default useStrudel;
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,14 @@ npm run static # <- test static build
|
||||||
|
|
||||||
currently broken / buggy:
|
currently broken / buggy:
|
||||||
|
|
||||||
- MiniREPL
|
- [x] MiniREPL
|
||||||
- repl log section
|
- [ ] repl log section => remove?
|
||||||
- hideHeader flag
|
- [ ] hideHeader flag
|
||||||
- pending flag
|
- [ ] pending flag
|
||||||
- web midi
|
- [ ] web midi
|
||||||
- draw / pianoroll
|
- [ ] draw / pianoroll
|
||||||
- pause does stop
|
- [ ] pause does stop
|
||||||
- random button triggers start
|
- [x] random button triggers start
|
||||||
- highlighting seems too late (off by latency ?)
|
- [?] highlighting seems too late (off by latency ?)
|
||||||
|
- [ ] unexpected ast format without body expression (kalimba)
|
||||||
|
- [ ] highlighting sometimes drops highlights (zeldasRescue first note)
|
||||||
|
|
|
||||||
|
|
@ -90,17 +90,27 @@ const randomTune = getRandomTune();
|
||||||
const isEmbedded = window.location !== window.parent.location;
|
const isEmbedded = window.location !== window.parent.location;
|
||||||
function App() {
|
function App() {
|
||||||
// const [editor, setEditor] = useState();
|
// const [editor, setEditor] = useState();
|
||||||
const [code, setCode] = useState('// LOADING');
|
|
||||||
const [view, setView] = useState();
|
const [view, setView] = useState();
|
||||||
const [lastShared, setLastShared] = useState();
|
const [lastShared, setLastShared] = useState();
|
||||||
|
|
||||||
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern, started, togglePlay } =
|
const {
|
||||||
useStrudel({
|
code,
|
||||||
code,
|
setCode,
|
||||||
defaultOutput: webaudioOutput,
|
scheduler,
|
||||||
getTime,
|
evaluate,
|
||||||
});
|
activateCode,
|
||||||
const error = schedulerError || evalError;
|
error,
|
||||||
|
isDirty,
|
||||||
|
activeCode,
|
||||||
|
pattern,
|
||||||
|
started,
|
||||||
|
togglePlay,
|
||||||
|
stop,
|
||||||
|
} = useStrudel({
|
||||||
|
initialCode: '// LOADING',
|
||||||
|
defaultOutput: webaudioOutput,
|
||||||
|
getTime,
|
||||||
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initCode().then((decoded) => setCode(decoded || randomTune));
|
initCode().then((decoded) => setCode(decoded || randomTune));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -121,17 +131,16 @@ function App() {
|
||||||
if (e.code === 'Enter') {
|
if (e.code === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
flash(view);
|
flash(view);
|
||||||
// await activateCode();
|
await activateCode();
|
||||||
await evaluate();
|
|
||||||
} else if (e.code === 'Period') {
|
} else if (e.code === 'Period') {
|
||||||
scheduler.stop();
|
stop();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', handleKeyPress, true);
|
window.addEventListener('keydown', handleKeyPress, true);
|
||||||
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
||||||
}, [pattern /* , code */, /* activateCode, */ scheduler, view]);
|
}, [activateCode, stop, view]);
|
||||||
|
|
||||||
useHighlighting({
|
useHighlighting({
|
||||||
view,
|
view,
|
||||||
|
|
@ -227,15 +236,12 @@ function App() {
|
||||||
className="hover:bg-gray-300 p-2"
|
className="hover:bg-gray-300 p-2"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const _code = getRandomTune();
|
const _code = getRandomTune();
|
||||||
// console.log('tune', _code); // uncomment this to debug when random code fails
|
/*
|
||||||
setCode(_code);
|
cleanupDraw();
|
||||||
/* cleanupDraw();
|
|
||||||
cleanupUi(); */
|
cleanupUi(); */
|
||||||
resetLoadedSamples();
|
resetLoadedSamples();
|
||||||
await prebake(); // declare default samples
|
await prebake(); // declare default samples
|
||||||
const parsed = await evaluate(_code);
|
await evaluate(_code, false);
|
||||||
setPattern(parsed.pattern);
|
|
||||||
setActiveCode(_code);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
🎲 random
|
🎲 random
|
||||||
|
|
@ -306,7 +312,7 @@ function App() {
|
||||||
{/* onCursor={markParens} */}
|
{/* onCursor={markParens} */}
|
||||||
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
|
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
|
||||||
<span className="z-[20] bg-black rounded-t-md py-1 px-2 fixed bottom-0 right-1 text-xs whitespace-pre text-right pointer-events-none">
|
<span className="z-[20] bg-black rounded-t-md py-1 px-2 fixed bottom-0 right-1 text-xs whitespace-pre text-right pointer-events-none">
|
||||||
{!started ? `press ctrl+enter to play\n` : isDirty ? `ctrl+enter to update\n` : 'no changes\n'}
|
{!started ? `press ctrl+enter to play\n` : isDirty ? `press ctrl+enter to update\n` : 'press ctrl+dot do stop\n'}
|
||||||
</span>
|
</span>
|
||||||
{error && (
|
{error && (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue