/* App.js - Copyright (C) 2022 Strudel contributors - see 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 . */ import controls from '@strudel.cycles/core/controls.mjs'; import { evalScope, evaluate } from '@strudel.cycles/eval'; import { CodeMirror, cx, useHighlighting, useRepl, useWebMidi } from '@strudel.cycles/react'; import { getDefaultSynth, cleanupDraw, cleanupUi, Tone } from '@strudel.cycles/tone'; import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'; import './App.css'; import logo from './logo.svg'; import * as tunes from './tunes.mjs'; evalScope( Tone, controls, import('@strudel.cycles/core'), import('@strudel.cycles/tone'), import('@strudel.cycles/tonal'), import('@strudel.cycles/mini'), import('@strudel.cycles/midi'), import('@strudel.cycles/xen'), import('@strudel.cycles/webaudio'), ); const initialUrl = window.location.href; const codeParam = window.location.href.split('#')[1]; let decoded; try { decoded = atob(decodeURIComponent(codeParam || '')); } catch (err) { console.warn('failed to decode', err); } function getRandomTune() { const allTunes = Object.values(tunes); const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)]; return randomItem(allTunes); } const randomTune = getRandomTune(); const defaultSynth = getDefaultSynth(); const isEmbedded = window.location !== window.parent.location; function App() { // const [editor, setEditor] = useState(); const [view, setView] = useState(); const { setCode, setPattern, error, code, cycle, dirty, log, togglePlay, activeCode, setActiveCode, activateCode, pattern, pushLog, pending, } = useRepl({ tune: decoded || randomTune, defaultSynth, }); const logBox = useRef(); // scroll log box to bottom when log changes useLayoutEffect(() => { if (logBox.current) { 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 = async (e) => { if (e.ctrlKey || e.altKey) { if (e.code === 'Enter') { await activateCode(); e.preventDefault(); } else if (e.code === 'Period') { cycle.stop(); e.preventDefault(); } } }; window.addEventListener('keydown', handleKeyPress); return () => window.removeEventListener('keydown', handleKeyPress); }, [pattern, code, activateCode, cycle]); useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes('strudel disable-highlighting') }); useWebMidi({ ready: useCallback( ({ outputs }) => { pushLog(`WebMidi ready! Just add .midi(${outputs.map((o) => `'${o.name}'`).join(' | ')}) to the pattern. `); }, [pushLog], ), connected: useCallback( ({ outputs }) => { pushLog(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`); }, [pushLog], ), disconnected: useCallback( ({ outputs }) => { pushLog(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`); }, [pushLog], ), }); return (
{/* onCursor={markParens} */} {!cycle.started ? `press ctrl+enter to play\n` : dirty ? `ctrl+enter to update\n` : 'no changes\n'} {error && (
{error?.message || 'unknown error'}
)}
{!isEmbedded && (