started refac repl to new scheduler + transpiler

This commit is contained in:
Felix Roos
2022-11-10 12:07:36 +01:00
parent c99d957bc8
commit 14c2da4fa2
17 changed files with 5650 additions and 697 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ import { controls, evalScope } from '@strudel.cycles/core';
evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tone'),
// import('@strudel.cycles/tone'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'),
+6 -5
View File
@@ -1,6 +1,5 @@
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
import { useInView } from 'react-hook-inview';
import useRepl from '../hooks/useRepl.mjs';
import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs';
import CodeMirror6, { flash } from './CodeMirror6';
@@ -8,10 +7,12 @@ import 'tailwindcss/tailwind.css';
import './style.css';
import styles from './MiniRepl.module.css';
import { Icon } from './Icon';
import { Tone } from '@strudel.cycles/tone';
import { getAudioContext } from '@strudel.cycles/webaudio';
// import { Tone } from '@strudel.cycles/tone';
export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableKeyboard }) {
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
return <p>TODO</p>;
/* const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
useRepl({
tune,
autolink: false,
@@ -35,7 +36,7 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
view,
pattern,
active: cycle.started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => Tone.getTransport().seconds,
getTime: () => getAudioContext().seconds,
});
// set active pattern on ctrl+enter
@@ -75,5 +76,5 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</div>
</div>
);
); */
}
-86
View File
@@ -1,86 +0,0 @@
/*
useCycle.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useCycle.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useEffect, useState } from 'react';
import { Tone } from '@strudel.cycles/tone';
import { State, TimeSpan } from '@strudel.cycles/core';
/* export declare interface UseCycleProps {
onEvent: ToneEventCallback<any>;
onQuery?: (state: State) => Hap[];
onSchedule?: (events: Hap[], cycle: number) => void;
onDraw?: ToneEventCallback<any>;
ready?: boolean; // if false, query will not be called on change props
} */
// function useCycle(props: UseCycleProps) {
function useCycle(props) {
// onX must use useCallback!
const { onEvent, onQuery, onSchedule, ready = true, onDraw } = props;
const [started, setStarted] = useState(false);
const cycleDuration = 1;
const activeCycle = () => Math.floor(Tone.getTransport().seconds / cycleDuration);
// pull events with onQuery + count up to next cycle
const query = (cycle = activeCycle()) => {
const timespan = new TimeSpan(cycle, cycle + 1);
const events = onQuery?.(new State(timespan)) || [];
onSchedule?.(events, cycle);
// cancel events after current query. makes sure no old events are player for rescheduled cycles
// console.log('schedule', cycle);
// query next cycle in the middle of the current
const cancelFrom = timespan.begin.valueOf();
Tone.getTransport().cancel(cancelFrom);
// const queryNextTime = (cycle + 1) * cycleDuration - 0.1;
const queryNextTime = (cycle + 1) * cycleDuration - 0.5;
// if queryNextTime would be before current time, execute directly (+0.1 for safety that it won't miss)
const t = Math.max(Tone.getTransport().seconds, queryNextTime) + 0.1;
Tone.getTransport().schedule(() => {
query(cycle + 1);
}, t);
// schedule events for next cycle
events
?.filter((event) => event.part.begin.equals(event.whole?.begin))
.forEach((event) => {
Tone.getTransport().schedule((time) => {
onEvent(time, event, Tone.getContext().currentTime);
Tone.Draw.schedule(() => {
// do drawing or DOM manipulation here
onDraw?.(time, event);
}, time);
}, event.part.begin.valueOf());
});
};
useEffect(() => {
ready && query();
}, [onEvent, onSchedule, onQuery, onDraw, ready]);
const start = async () => {
setStarted(true);
await Tone.start();
Tone.getTransport().start('+0.1');
};
const stop = () => {
Tone.getTransport().pause();
setStarted(false);
};
const toggle = () => (started ? stop() : start());
return {
start,
stop,
onEvent,
started,
setStarted,
toggle,
query,
activeCycle,
};
}
export default useCycle;
-150
View File
@@ -1,150 +0,0 @@
/*
useRepl.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useRepl.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useCallback, useState, useMemo } from 'react';
import { evaluate } from '@strudel.cycles/eval';
import useCycle from './useCycle.mjs';
import usePostMessage from './usePostMessage.mjs';
import { webaudioOutputTrigger } from '@strudel.cycles/webaudio';
let s4 = () => {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
};
const generateHash = (code) => encodeURIComponent(btoa(code));
function useRepl({ tune, autolink = true, onEvent, onDraw: onDrawProp }) {
const id = useMemo(() => s4(), []);
const [code, setCode] = useState(tune);
const [activeCode, setActiveCode] = useState();
const [log, setLog] = useState('');
const [error, setError] = useState();
const [pending, setPending] = useState(false);
const [hash, setHash] = useState('');
const [pattern, setPattern] = useState();
const dirty = useMemo(() => code !== activeCode || error, [code, activeCode, error]);
const pushLog = useCallback((message) => setLog((log) => log + `${log ? '\n\n' : ''}${message}`), []);
// below block allows disabling the highlighting by including "strudel disable-highlighting" in the code (as comment)
const onDraw = useMemo(() => {
if (activeCode && !activeCode.includes('strudel disable-highlighting')) {
return (time, event) => onDrawProp?.(time, event, activeCode);
}
}, [activeCode, onDrawProp]);
const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]);
const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]);
// cycle hook to control scheduling
const cycle = useCycle({
onDraw,
onEvent: useCallback(
(time, event, currentTime) => {
try {
onEvent?.(event);
if (event.context.logs?.length) {
event.context.logs.forEach(pushLog);
}
const { onTrigger = webaudioOutputTrigger } = event.context;
onTrigger(time, event, currentTime, 1 /* cps */);
} catch (err) {
console.warn(err);
err.message = 'unplayable event: ' + err?.message;
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
}
},
[onEvent, pushLog],
),
onQuery: useCallback(
(state) => {
try {
return pattern?.query(state) || [];
} catch (err) {
console.warn(err);
err.message = 'query error: ' + err.message;
setError(err);
return [];
}
},
[pattern],
),
onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), []),
ready: !!pattern && !!activeCode,
});
const broadcast = usePostMessage(({ data: { from, type } }) => {
if (type === 'start' && from !== id) {
// console.log('message', from, type);
cycle.setStarted(false);
setActiveCode(undefined);
}
});
const activateCode = useCallback(
async (_code = code) => {
if (activeCode && !dirty) {
setError(undefined);
cycle.start();
return;
}
try {
setPending(true);
const parsed = await evaluate(_code);
cycle.start();
broadcast({ type: 'start', from: id });
setPattern(() => parsed.pattern);
if (autolink) {
window.location.hash = '#' + encodeURIComponent(btoa(code));
}
setHash(generateHash(code));
setError(undefined);
setActiveCode(_code);
setPending(false);
} catch (err) {
err.message = 'evaluation error: ' + err.message;
console.warn(err);
setError(err);
}
},
[activeCode, dirty, code, cycle, autolink, id, broadcast],
);
// logs events of cycle
const logCycle = (_events, cycle) => {
if (_events.length) {
// pushLog(`# cycle ${cycle}\n` + _events.map((e: any) => e.show()).join('\n'));
}
};
const togglePlay = () => {
if (!cycle.started) {
activateCode();
} else {
cycle.stop();
}
};
return {
hideHeader,
hideConsole,
pending,
code,
setCode,
pattern,
error,
cycle,
setPattern,
dirty,
log,
togglePlay,
setActiveCode,
activateCode,
activeCode,
pushLog,
hash,
};
}
export default useRepl;
+14 -1
View File
@@ -8,7 +8,9 @@ function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = fals
const [evalError, setEvalError] = useState();
const [activeCode, setActiveCode] = useState(code);
const [pattern, setPattern] = useState();
const [started, setStarted] = useState(false);
const isDirty = code !== activeCode;
// TODO: make sure this hook reruns when scheduler.started changes
const { scheduler, evaluate: _evaluate } = useMemo(
() =>
repl({
@@ -23,6 +25,7 @@ function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = fals
setPattern(_pattern);
setEvalError();
},
onToggle: (v) => setStarted(v),
onEvalError: setEvalError,
}),
[defaultOutput, interval, getTime],
@@ -32,12 +35,22 @@ function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = fals
const inited = useRef();
useEffect(() => {
if (!inited.current && evalOnMount && code) {
console.log('eval on mount');
inited.current = true;
evaluate();
}
}, [evaluate, evalOnMount, code]);
return { schedulerError, scheduler, evalError, evaluate, activeCode, isDirty, pattern };
const togglePlay = async () => {
if (started) {
scheduler.pause();
// scheduler.stop();
} else {
await evaluate();
}
};
return { schedulerError, scheduler, evalError, evaluate, activeCode, isDirty, pattern, started, togglePlay };
}
export default useStrudel;
-2
View File
@@ -2,10 +2,8 @@
export { default as CodeMirror, flash } from './components/CodeMirror6';
export * from './components/MiniRepl';
export { default as useCycle } from './hooks/useCycle';
export { default as useHighlighting } from './hooks/useHighlighting';
export { default as usePostMessage } from './hooks/usePostMessage';
export { default as useRepl } from './hooks/useRepl';
export { default as useStrudel } from './hooks/useStrudel';
export { default as useKeydown } from './hooks/useKeydown';
export { default as cx } from './cx';