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

32
package-lock.json generated
View File

@ -12495,10 +12495,10 @@
}, },
"packages/midi": { "packages/midi": {
"name": "@strudel.cycles/midi", "name": "@strudel.cycles/midi",
"version": "0.3.2", "version": "0.3.3",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@strudel.cycles/tone": "^0.3.2", "@strudel.cycles/tone": "^0.3.3",
"tone": "^14.7.77", "tone": "^14.7.77",
"webmidi": "^3.0.21" "webmidi": "^3.0.21"
} }
@ -12519,12 +12519,12 @@
}, },
"packages/mini": { "packages/mini": {
"name": "@strudel.cycles/mini", "name": "@strudel.cycles/mini",
"version": "0.3.2", "version": "0.3.3",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.3.2", "@strudel.cycles/core": "^0.3.2",
"@strudel.cycles/eval": "^0.3.2", "@strudel.cycles/eval": "^0.3.2",
"@strudel.cycles/tone": "^0.3.2" "@strudel.cycles/tone": "^0.3.3"
}, },
"devDependencies": { "devDependencies": {
"peggy": "^2.0.1" "peggy": "^2.0.1"
@ -12540,13 +12540,13 @@
}, },
"packages/react": { "packages/react": {
"name": "@strudel.cycles/react", "name": "@strudel.cycles/react",
"version": "0.3.2", "version": "0.3.3",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@codemirror/lang-javascript": "^6.1.1", "@codemirror/lang-javascript": "^6.1.1",
"@strudel.cycles/core": "^0.3.2", "@strudel.cycles/core": "^0.3.2",
"@strudel.cycles/eval": "^0.3.2", "@strudel.cycles/eval": "^0.3.2",
"@strudel.cycles/tone": "^0.3.2", "@strudel.cycles/tone": "^0.3.3",
"@uiw/codemirror-themes": "^4.12.4", "@uiw/codemirror-themes": "^4.12.4",
"@uiw/react-codemirror": "^4.12.4", "@uiw/react-codemirror": "^4.12.4",
"react-hook-inview": "^4.5.0" "react-hook-inview": "^4.5.0"
@ -12621,11 +12621,11 @@
}, },
"packages/soundfonts": { "packages/soundfonts": {
"name": "@strudel.cycles/soundfonts", "name": "@strudel.cycles/soundfonts",
"version": "0.3.2", "version": "0.3.3",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.3.2", "@strudel.cycles/core": "^0.3.2",
"@strudel.cycles/webaudio": "^0.3.2", "@strudel.cycles/webaudio": "^0.3.3",
"sfumato": "^0.1.2", "sfumato": "^0.1.2",
"soundfont2": "^0.4.0" "soundfont2": "^0.4.0"
}, },
@ -12653,7 +12653,7 @@
}, },
"packages/tonal": { "packages/tonal": {
"name": "@strudel.cycles/tonal", "name": "@strudel.cycles/tonal",
"version": "0.3.2", "version": "0.3.3",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.3.2", "@strudel.cycles/core": "^0.3.2",
@ -12678,7 +12678,7 @@
}, },
"packages/tone": { "packages/tone": {
"name": "@strudel.cycles/tone", "name": "@strudel.cycles/tone",
"version": "0.3.2", "version": "0.3.3",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.3.2", "@strudel.cycles/core": "^0.3.2",
@ -12709,7 +12709,7 @@
}, },
"packages/webaudio": { "packages/webaudio": {
"name": "@strudel.cycles/webaudio", "name": "@strudel.cycles/webaudio",
"version": "0.3.2", "version": "0.3.3",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@strudel.cycles/core": "^0.3.2" "@strudel.cycles/core": "^0.3.2"
@ -14427,7 +14427,7 @@
"@strudel.cycles/midi": { "@strudel.cycles/midi": {
"version": "file:packages/midi", "version": "file:packages/midi",
"requires": { "requires": {
"@strudel.cycles/tone": "^0.3.2", "@strudel.cycles/tone": "^0.3.3",
"tone": "^14.7.77", "tone": "^14.7.77",
"webmidi": "^3.0.21" "webmidi": "^3.0.21"
}, },
@ -14448,7 +14448,7 @@
"requires": { "requires": {
"@strudel.cycles/core": "^0.3.2", "@strudel.cycles/core": "^0.3.2",
"@strudel.cycles/eval": "^0.3.2", "@strudel.cycles/eval": "^0.3.2",
"@strudel.cycles/tone": "^0.3.2", "@strudel.cycles/tone": "^0.3.3",
"peggy": "^2.0.1" "peggy": "^2.0.1"
} }
}, },
@ -14464,7 +14464,7 @@
"@codemirror/lang-javascript": "^6.1.1", "@codemirror/lang-javascript": "^6.1.1",
"@strudel.cycles/core": "^0.3.2", "@strudel.cycles/core": "^0.3.2",
"@strudel.cycles/eval": "^0.3.2", "@strudel.cycles/eval": "^0.3.2",
"@strudel.cycles/tone": "^0.3.2", "@strudel.cycles/tone": "^0.3.3",
"@types/react": "^17.0.2", "@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2", "@types/react-dom": "^17.0.2",
"@uiw/codemirror-themes": "^4.12.4", "@uiw/codemirror-themes": "^4.12.4",
@ -14520,7 +14520,7 @@
"version": "file:packages/soundfonts", "version": "file:packages/soundfonts",
"requires": { "requires": {
"@strudel.cycles/core": "^0.3.2", "@strudel.cycles/core": "^0.3.2",
"@strudel.cycles/webaudio": "^0.3.2", "@strudel.cycles/webaudio": "^0.3.3",
"node-fetch": "^3.2.6", "node-fetch": "^3.2.6",
"sfumato": "^0.1.2", "sfumato": "^0.1.2",
"soundfont2": "^0.4.0" "soundfont2": "^0.4.0"
@ -20344,7 +20344,7 @@
"@codemirror/lang-javascript": "^6.1.1", "@codemirror/lang-javascript": "^6.1.1",
"@strudel.cycles/core": "^0.3.2", "@strudel.cycles/core": "^0.3.2",
"@strudel.cycles/eval": "^0.3.2", "@strudel.cycles/eval": "^0.3.2",
"@strudel.cycles/tone": "^0.3.2", "@strudel.cycles/tone": "^0.3.3",
"@types/react": "^17.0.2", "@types/react": "^17.0.2",
"@types/react-dom": "^17.0.2", "@types/react-dom": "^17.0.2",
"@uiw/codemirror-themes": "^4.12.4", "@uiw/codemirror-themes": "^4.12.4",

View File

@ -13,8 +13,9 @@ export class Cyclist {
cps = 1; // TODO cps = 1; // TODO
getTime; getTime;
phase = 0; phase = 0;
constructor({ interval, onTrigger, onError, getTime, latency = 0.1 }) { constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
this.getTime = getTime; this.getTime = getTime;
this.onToggle = onToggle;
const round = (x) => Math.round(x * 1000) / 1000; const round = (x) => Math.round(x * 1000) / 1000;
this.clock = createClock( this.clock = createClock(
getTime, getTime,
@ -28,9 +29,7 @@ export class Cyclist {
const time = getTime(); const time = getTime();
try { try {
const haps = this.pattern.queryArc(begin, end); // get Haps const haps = this.pattern.queryArc(begin, end); // get Haps
// console.log('haps', haps.map((hap) => hap.value.n).join(' '));
haps.forEach((hap) => { haps.forEach((hap) => {
// console.log('hap', hap.value.n, hap.part.begin);
if (hap.part.begin.equals(hap.whole.begin)) { if (hap.part.begin.equals(hap.whole.begin)) {
const deadline = hap.whole.begin + this.origin - time + latency; const deadline = hap.whole.begin + this.origin - time + latency;
const duration = hap.duration * 1; const duration = hap.duration * 1;
@ -48,22 +47,26 @@ export class Cyclist {
getPhase() { getPhase() {
return this.phase; return this.phase;
} }
setStarted(v) {
this.started = v;
this.onToggle?.(v);
}
start() { start() {
if (!this.pattern) { if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.'); throw new Error('Scheduler: no pattern set! call .setPattern first.');
} }
this.clock.start(); this.clock.start();
this.started = true; this.setStarted(true);
} }
pause() { pause() {
this.clock.stop(); this.clock.stop();
delete this.origin; // delete this.origin;
this.started = false; this.setStarted(false);
} }
stop() { stop() {
delete this.origin; delete this.origin;
this.clock.stop(); this.clock.stop();
this.started = false; this.setStarted(false);
} }
setPattern(pat, autostart = false) { setPattern(pat, autostart = false) {
this.pattern = pat; this.pattern = pat;

View File

@ -1,8 +1,17 @@
import { Cyclist } from './cyclist.mjs'; import { Cyclist } from './cyclist.mjs';
import { evaluate as _evaluate } from './evaluate.mjs'; import { evaluate as _evaluate } from './evaluate.mjs';
export function repl({ interval, defaultOutput, onSchedulerError, onEvalError, onEval, getTime, transpiler }) { export function repl({
const scheduler = new Cyclist({ interval, onTrigger: defaultOutput, onError: onSchedulerError, getTime }); interval,
defaultOutput,
onSchedulerError,
onEvalError,
onEval,
getTime,
transpiler,
onToggle,
}) {
const scheduler = new Cyclist({ interval, onTrigger: defaultOutput, onError: onSchedulerError, getTime, onToggle });
const evaluate = async (code) => { const evaluate = async (code) => {
if (!code) { if (!code) {
throw new Error('no code to evaluate'); throw new Error('no code to evaluate');

View File

@ -31,10 +31,11 @@ function createClock(
}; };
let intervalID; let intervalID;
const start = () => { const start = () => {
clear(); // just in case start was called more than once
onTick(); onTick();
intervalID = setInterval(onTick, interval * 1000); intervalID = setInterval(onTick, interval * 1000);
}; };
const clear = () => clearInterval(intervalID); const clear = () => intervalID !== undefined && clearInterval(intervalID);
const pause = () => clear(); const pause = () => clear();
const stop = () => { const stop = () => {
tick = 0; tick = 0;

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/>. 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 * as _WebMidi from 'webmidi';
import { Pattern, isPattern } from '@strudel.cycles/core'; import { Pattern, isPattern, isNote } from '@strudel.cycles/core';
import { Tone } from '@strudel.cycles/tone'; import { getAudioContext } from '@strudel.cycles/webaudio';
// if you use WebMidi from outside of this package, make sure to import that instance: // if you use WebMidi from outside of this package, make sure to import that instance:
export const { WebMidi } = _WebMidi; export const { WebMidi } = _WebMidi;
@ -68,7 +68,7 @@ Pattern.prototype.midi = function (output, channel = 1) {
); );
} }
// console.log('midi', value, output); // console.log('midi', value, output);
const timingOffset = WebMidi.time - Tone.getContext().currentTime * 1000; const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
time = time * 1000 + timingOffset; time = time * 1000 + timingOffset;
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000; // const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
// await enableWebMidi() // await enableWebMidi()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -17,6 +17,7 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"watch": "vite build --watch",
"preview": "vite preview" "preview": "vite preview"
}, },
"repository": { "repository": {

View File

@ -6,7 +6,7 @@ import { controls, evalScope } from '@strudel.cycles/core';
evalScope( evalScope(
controls, controls,
import('@strudel.cycles/core'), import('@strudel.cycles/core'),
import('@strudel.cycles/tone'), // import('@strudel.cycles/tone'),
import('@strudel.cycles/tonal'), import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'), import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'), import('@strudel.cycles/midi'),

View File

@ -1,6 +1,5 @@
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react'; import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
import { useInView } from 'react-hook-inview'; import { useInView } from 'react-hook-inview';
import useRepl from '../hooks/useRepl.mjs';
import cx from '../cx'; import cx from '../cx';
import useHighlighting from '../hooks/useHighlighting.mjs'; import useHighlighting from '../hooks/useHighlighting.mjs';
import CodeMirror6, { flash } from './CodeMirror6'; import CodeMirror6, { flash } from './CodeMirror6';
@ -8,10 +7,12 @@ 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 { 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 }) { 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({ useRepl({
tune, tune,
autolink: false, autolink: false,
@ -35,7 +36,7 @@ export function MiniRepl({ tune, hideOutsideView = false, init, onEvent, enableK
view, view,
pattern, pattern,
active: cycle.started && !activeCode?.includes('strudel disable-highlighting'), active: cycle.started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => Tone.getTransport().seconds, getTime: () => getAudioContext().seconds,
}); });
// set active pattern on ctrl+enter // 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} />} {show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
</div> </div>
</div> </div>
); ); */
} }

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;

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;

View File

@ -8,7 +8,9 @@ function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = fals
const [evalError, setEvalError] = useState(); const [evalError, setEvalError] = useState();
const [activeCode, setActiveCode] = useState(code); const [activeCode, setActiveCode] = useState(code);
const [pattern, setPattern] = useState(); const [pattern, setPattern] = useState();
const [started, setStarted] = useState(false);
const isDirty = code !== activeCode; const isDirty = code !== activeCode;
// TODO: make sure this hook reruns when scheduler.started changes
const { scheduler, evaluate: _evaluate } = useMemo( const { scheduler, evaluate: _evaluate } = useMemo(
() => () =>
repl({ repl({
@ -23,6 +25,7 @@ function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = fals
setPattern(_pattern); setPattern(_pattern);
setEvalError(); setEvalError();
}, },
onToggle: (v) => setStarted(v),
onEvalError: setEvalError, onEvalError: setEvalError,
}), }),
[defaultOutput, interval, getTime], [defaultOutput, interval, getTime],
@ -32,12 +35,22 @@ function useStrudel({ defaultOutput, interval, getTime, code, evalOnMount = fals
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(); evaluate();
} }
}, [evaluate, evalOnMount, code]); }, [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; export default useStrudel;

View File

@ -2,10 +2,8 @@
export { default as CodeMirror, flash } from './components/CodeMirror6'; export { default as CodeMirror, flash } from './components/CodeMirror6';
export * from './components/MiniRepl'; export * from './components/MiniRepl';
export { default as useCycle } from './hooks/useCycle';
export { default as useHighlighting } from './hooks/useHighlighting'; export { default as useHighlighting } from './hooks/useHighlighting';
export { default as usePostMessage } from './hooks/usePostMessage'; export { default as usePostMessage } from './hooks/usePostMessage';
export { default as useRepl } from './hooks/useRepl';
export { default as useStrudel } from './hooks/useStrudel'; export { default as useStrudel } from './hooks/useStrudel';
export { default as useKeydown } from './hooks/useKeydown'; export { default as useKeydown } from './hooks/useKeydown';
export { default as cx } from './cx'; export { default as cx } from './cx';

View File

@ -24,3 +24,17 @@ cd repl
npm run build # <- builds repl + tutorial to ../docs npm run build # <- builds repl + tutorial to ../docs
npm run static # <- test static build npm run static # <- test static build
``` ```
## Refactoring Notes
currently broken / buggy:
- MiniREPL
- repl log section
- hideHeader flag
- pending flag
- web midi
- draw / pianoroll
- pause does stop
- random button triggers start
- highlighting seems too late (off by latency ?)

View File

@ -4,9 +4,9 @@ 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/>. 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 { evaluate } from '@strudel.cycles/eval'; // import { evaluate } from '@strudel.cycles/eval';
import { CodeMirror, cx, flash, useHighlighting, useRepl, useWebMidi } from '@strudel.cycles/react'; import { CodeMirror, cx, flash, useHighlighting, useWebMidi } from '@strudel.cycles/react';
import { cleanupDraw, cleanupUi, Tone } from '@strudel.cycles/tone'; // import { cleanupDraw, cleanupUi, Tone } from '@strudel.cycles/tone';
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import './App.css'; import './App.css';
import logo from './logo.svg'; import logo from './logo.svg';
@ -17,6 +17,8 @@ import { resetLoadedSamples, getAudioContext } from '@strudel.cycles/webaudio';
import { controls, evalScope } from '@strudel.cycles/core'; import { controls, evalScope } from '@strudel.cycles/core';
import { createClient } from '@supabase/supabase-js'; import { createClient } from '@supabase/supabase-js';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { useStrudel } from '@strudel.cycles/react';
import { webaudioOutput } from '@strudel.cycles/webaudio';
// Create a single supabase client for interacting with your database // Create a single supabase client for interacting with your database
const supabase = createClient( const supabase = createClient(
@ -25,11 +27,11 @@ const supabase = createClient(
); );
evalScope( evalScope(
Tone, // Tone,
controls, // sadly, this cannot be exported from core direclty controls, // sadly, this cannot be exported from core direclty
{ WebDirt }, { WebDirt },
import('@strudel.cycles/core'), import('@strudel.cycles/core'),
import('@strudel.cycles/tone'), // import('@strudel.cycles/tone'),
import('@strudel.cycles/tonal'), import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'), import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'), import('@strudel.cycles/midi'),
@ -42,6 +44,11 @@ evalScope(
prebake(); prebake();
const pushLog = console.log;
const hideHeader = false;
const pending = false;
const getTime = () => getAudioContext().currentTime;
async function initCode() { async function initCode() {
// load code from url hash (either short hash from database or decode long hash) // load code from url hash (either short hash from database or decode long hash)
try { try {
@ -83,38 +90,28 @@ 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 {
setCode, const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern, started, togglePlay } =
setPattern, useStrudel({
error, code,
code, defaultOutput: webaudioOutput,
cycle, getTime,
dirty, });
log, const error = schedulerError || evalError;
togglePlay,
activeCode,
setActiveCode,
activateCode,
pattern,
pushLog,
pending,
hideHeader,
hideConsole,
} = useRepl({
tune: '// LOADING...',
});
useEffect(() => { useEffect(() => {
initCode().then((decoded) => setCode(decoded || randomTune)); initCode().then((decoded) => setCode(decoded || randomTune));
}, []); }, []);
const logBox = useRef(); const logBox = useRef();
// scroll log box to bottom when log changes // scroll log box to bottom when log changes
useLayoutEffect(() => {
/* useLayoutEffect(() => {
if (logBox.current) { if (logBox.current) {
logBox.current.scrollTop = logBox.current?.scrollHeight; logBox.current.scrollTop = logBox.current?.scrollHeight;
} }
}, [log]); }, [log]); */
// set active pattern on ctrl+enter // set active pattern on ctrl+enter
useLayoutEffect(() => { useLayoutEffect(() => {
@ -124,25 +121,27 @@ 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') {
cycle.stop(); scheduler.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, cycle, view]); }, [pattern /* , code */, /* activateCode, */ scheduler, view]);
useHighlighting({ useHighlighting({
view, view,
pattern, pattern,
active: cycle.started && !activeCode?.includes('strudel disable-highlighting'), active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => Tone.getTransport().seconds, getTime: () => scheduler.phase,
// getTime: () => Tone.getTransport().seconds,
}); });
useWebMidi({ /* useWebMidi({
ready: useCallback( ready: useCallback(
({ outputs }) => { ({ outputs }) => {
pushLog(`WebMidi ready! Just add .midi(${outputs.map((o) => `'${o.name}'`).join(' | ')}) to the pattern. `); pushLog(`WebMidi ready! Just add .midi(${outputs.map((o) => `'${o.name}'`).join(' | ')}) to the pattern. `);
@ -161,7 +160,7 @@ function App() {
}, },
[pushLog], [pushLog],
), ),
}); }); */
return ( return (
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
@ -187,7 +186,7 @@ function App() {
> >
{!pending ? ( {!pending ? (
<span className={cx('flex items-center', isEmbedded ? 'w-16' : 'w-16')}> <span className={cx('flex items-center', isEmbedded ? 'w-16' : 'w-16')}>
{cycle.started ? ( {started ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path <path
fillRule="evenodd" fillRule="evenodd"
@ -204,7 +203,7 @@ function App() {
/> />
</svg> </svg>
)} )}
{cycle.started ? 'pause' : 'play'} {started ? 'pause' : 'play'}
</span> </span>
) : ( ) : (
<>loading...</> <>loading...</>
@ -212,13 +211,13 @@ function App() {
</button> </button>
<button <button
onClick={() => { onClick={() => {
dirty && activateCode(); isDirty && activateCode();
pushLog('Code updated! Tip: You can also update the code by pressing ctrl+enter.'); pushLog('Code updated! Tip: You can also update the code by pressing ctrl+enter.');
}} }}
className={cx( className={cx(
'hover:bg-gray-300', 'hover:bg-gray-300',
!isEmbedded ? 'p-2' : 'px-2', !isEmbedded ? 'p-2' : 'px-2',
!dirty || !activeCode ? 'opacity-50' : '', !isDirty || !activeCode ? 'opacity-50' : '',
)} )}
> >
🔄 update 🔄 update
@ -230,8 +229,8 @@ function App() {
const _code = getRandomTune(); const _code = getRandomTune();
// console.log('tune', _code); // uncomment this to debug when random code fails // console.log('tune', _code); // uncomment this to debug when random code fails
setCode(_code); setCode(_code);
cleanupDraw(); /* cleanupDraw();
cleanupUi(); cleanupUi(); */
resetLoadedSamples(); resetLoadedSamples();
await prebake(); // declare default samples await prebake(); // declare default samples
const parsed = await evaluate(_code); const parsed = await evaluate(_code);
@ -307,7 +306,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">
{!cycle.started ? `press ctrl+enter to play\n` : dirty ? `ctrl+enter to update\n` : 'no changes\n'} {!started ? `press ctrl+enter to play\n` : isDirty ? `ctrl+enter to update\n` : 'no changes\n'}
</span> </span>
{error && ( {error && (
<div <div
@ -320,7 +319,7 @@ function App() {
</div> </div>
)} )}
</div> </div>
{!isEmbedded && !hideConsole && ( {/* !isEmbedded && !hideConsole && (
<textarea <textarea
className="z-[10] h-16 border-0 text-xs bg-[transparent] border-t border-slate-600 resize-none" className="z-[10] h-16 border-0 text-xs bg-[transparent] border-t border-slate-600 resize-none"
value={log} value={log}
@ -328,7 +327,7 @@ function App() {
ref={logBox} ref={logBox}
style={{ fontFamily: 'monospace' }} style={{ fontFamily: 'monospace' }}
/> />
)} ) */}
</section> </section>
{/* !isEmbedded && ( {/* !isEmbedded && (
<button className="fixed right-4 bottom-2 z-[11]" onClick={() => playStatic(code)}> <button className="fixed right-4 bottom-2 z-[11]" onClick={() => playStatic(code)}>

View File

@ -10,7 +10,7 @@ fetch('https://strudel.tidalcycles.org/EmuSP12.json')
evalScope( evalScope(
controls, controls,
import('@strudel.cycles/core'), import('@strudel.cycles/core'),
import('@strudel.cycles/tone'), // import('@strudel.cycles/tone'),
import('@strudel.cycles/tonal'), import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'), import('@strudel.cycles/mini'),
import('@strudel.cycles/midi'), import('@strudel.cycles/midi'),