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

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;

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');

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;

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

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": {
"dev": "vite",
"build": "vite build",
"watch": "vite build --watch",
"preview": "vite preview"
},
"repository": {

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'),

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

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

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

View File

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