import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import * as Tone from 'tone'; import CodeMirror from './CodeMirror'; import cx from './cx'; import { evaluate } from './evaluate'; import logo from './logo.svg'; import { useWebMidi } from './midi'; import * as tunes from './tunes'; import useRepl from './useRepl'; // TODO: use https://www.npmjs.com/package/@monaco-editor/react const [_, codeParam] = window.location.href.split('#'); let decoded; try { decoded = atob(decodeURIComponent(codeParam || '')); } catch (err) { console.warn('failed to decode', err); } const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.Destination); defaultSynth.set({ oscillator: { type: 'triangle' }, envelope: { release: 0.01, }, }); function getRandomTune() { const allTunes = Object.values(tunes); const randomItem = (arr: any[]) => arr[Math.floor(Math.random() * arr.length)]; return randomItem(allTunes); } const randomTune = getRandomTune(); function App() { const [editor, setEditor] = useState(); const doc = useMemo(() => editor?.getDoc(), [editor]); const { setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activateCode, pattern, pushLog } = useRepl({ tune: decoded || randomTune, defaultSynth, onEvent: useCallback( (event) => { const locs = event.value.locations; if (!locs) { return; } // mark active event const marks = locs.map(({ start, end }) => doc.markText( { line: start.line - 1, ch: start.column }, { line: end.line - 1, ch: end.column }, { css: 'background-color: gray;' } ) ); //Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler... setTimeout(() => { marks.forEach((mark) => mark.clear()); // }, '+' + event.duration * 0.5); }, event.duration * 0.9 * 1000); }, [doc] ), }); const logBox = useRef(); // scroll log box to bottom when log changes useLayoutEffect(() => { logBox.current.scrollTop = logBox.current?.scrollHeight; }, [log]); // set active pattern on ctrl+enter useLayoutEffect(() => { // TODO: make sure this is only fired when editor has focus const handleKeyPress = (e: any) => { if (e.ctrlKey || e.altKey) { switch (e.code) { case 'Enter': activateCode(); !cycle.started && cycle.start(); break; case 'Period': cycle.stop(); } } }; document.addEventListener('keypress', handleKeyPress); return () => document.removeEventListener('keypress', handleKeyPress); }, [pattern, code]); useWebMidi({ ready: useCallback(({ outputs }) => { pushLog(`WebMidi ready! Just add .midi(${outputs.map((o) => `"${o.name}"`).join(' | ')}) to the pattern. `); }, []), connected: useCallback(({ outputs }) => { pushLog(`Midi device connected! Available: ${outputs.map((o) => `"${o.name}"`).join(', ')}`); }, []), disconnected: useCallback(({ outputs }) => { pushLog(`Midi device disconnected! Available: ${outputs.map((o) => `"${o.name}"`).join(', ')}`); }, []), }); return (
logo

Strudel REPL

setCode(value)} /> {!cycle.started ? `press ctrl+enter to play\n` : dirty ? `ctrl+enter to update\n` : 'no changes\n'}
{error && (
{error?.message || 'unknown error'}
)}