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
+10 -7
View File
@@ -13,8 +13,9 @@ export class Cyclist {
cps = 1; // TODO
getTime;
phase = 0;
constructor({ interval, onTrigger, onError, getTime, latency = 0.1 }) {
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
this.getTime = getTime;
this.onToggle = onToggle;
const round = (x) => Math.round(x * 1000) / 1000;
this.clock = createClock(
getTime,
@@ -28,9 +29,7 @@ export class Cyclist {
const time = getTime();
try {
const haps = this.pattern.queryArc(begin, end); // get Haps
// console.log('haps', haps.map((hap) => hap.value.n).join(' '));
haps.forEach((hap) => {
// console.log('hap', hap.value.n, hap.part.begin);
if (hap.part.begin.equals(hap.whole.begin)) {
const deadline = hap.whole.begin + this.origin - time + latency;
const duration = hap.duration * 1;
@@ -48,22 +47,26 @@ export class Cyclist {
getPhase() {
return this.phase;
}
setStarted(v) {
this.started = v;
this.onToggle?.(v);
}
start() {
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
this.clock.start();
this.started = true;
this.setStarted(true);
}
pause() {
this.clock.stop();
delete this.origin;
this.started = false;
// delete this.origin;
this.setStarted(false);
}
stop() {
delete this.origin;
this.clock.stop();
this.started = false;
this.setStarted(false);
}
setPattern(pat, autostart = false) {
this.pattern = pat;
+11 -2
View File
@@ -1,8 +1,17 @@
import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs';
export function repl({ interval, defaultOutput, onSchedulerError, onEvalError, onEval, getTime, transpiler }) {
const scheduler = new Cyclist({ interval, onTrigger: defaultOutput, onError: onSchedulerError, getTime });
export function repl({
interval,
defaultOutput,
onSchedulerError,
onEvalError,
onEval,
getTime,
transpiler,
onToggle,
}) {
const scheduler = new Cyclist({ interval, onTrigger: defaultOutput, onError: onSchedulerError, getTime, onToggle });
const evaluate = async (code) => {
if (!code) {
throw new Error('no code to evaluate');
+2 -1
View File
@@ -31,10 +31,11 @@ function createClock(
};
let intervalID;
const start = () => {
clear(); // just in case start was called more than once
onTick();
intervalID = setInterval(onTick, interval * 1000);
};
const clear = () => clearInterval(intervalID);
const clear = () => intervalID !== undefined && clearInterval(intervalID);
const pause = () => clear();
const stop = () => {
tick = 0;
+4 -4
View File
@@ -4,10 +4,10 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
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 { isNote } from 'tone';
import * as _WebMidi from 'webmidi';
import { Pattern, isPattern } from '@strudel.cycles/core';
import { Tone } from '@strudel.cycles/tone';
import { Pattern, isPattern, isNote } from '@strudel.cycles/core';
import { getAudioContext } from '@strudel.cycles/webaudio';
// if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi;
@@ -68,7 +68,7 @@ Pattern.prototype.midi = function (output, channel = 1) {
);
}
// console.log('midi', value, output);
const timingOffset = WebMidi.time - Tone.getContext().currentTime * 1000;
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
time = time * 1000 + timingOffset;
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
// await enableWebMidi()
+23 -3
View File
File diff suppressed because one or more lines are too long
+5505 -375
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -17,6 +17,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"watch": "vite build --watch",
"preview": "vite preview"
},
"repository": {
+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';