fix mini repl + improve repl api

This commit is contained in:
Felix Roos 2022-11-10 16:36:51 +01:00
parent 14c2da4fa2
commit c7c90b0647
9 changed files with 1413 additions and 1267 deletions

View file

@ -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) {

View file

@ -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 };
} }

View file

@ -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;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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>
); */ );
} }

View file

@ -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;

View file

@ -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)

View file

@ -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