hot mode + tone synth experiments

This commit is contained in:
Felix Roos 2022-02-08 23:01:01 +01:00
parent 89ee94e953
commit 20cbaf913a
9 changed files with 502 additions and 80 deletions

192
package-lock.json generated
View File

@ -9,7 +9,12 @@
"version": "1.0.2",
"license": "GPL-3.0-or-later",
"dependencies": {
"fraction.js": "^4.1.2"
"automation-events": "^4.0.12",
"fraction.js": "^4.1.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"standardized-audio-context": "^25.3.20",
"tone": "^14.7.77"
},
"devDependencies": {
"mocha": "^9.1.4",
@ -122,6 +127,17 @@
"node": ">=4"
}
},
"node_modules/@babel/runtime": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.0.tgz",
"integrity": "sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@gar/promisify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz",
@ -689,6 +705,18 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
},
"node_modules/automation-events": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/automation-events/-/automation-events-4.0.13.tgz",
"integrity": "sha512-SYHkG0A0x+JwLBHSexZhlv9mcYlvFpen9S2zTNjchfp5EDSbrs3Fm9Teje0PWpUNTV5W1C/kEPdZ4RTrGOm7Hg==",
"dependencies": {
"@babel/runtime": "^7.17.0",
"tslib": "^2.3.1"
},
"engines": {
"node": ">=12.20.1"
}
},
"node_modules/aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@ -2513,8 +2541,7 @@
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"version": "4.1.0",
@ -2688,6 +2715,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
@ -3248,7 +3286,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -3860,6 +3897,31 @@
"safe-buffer": "^5.1.0"
}
},
"node_modules/react": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"scheduler": "^0.20.2"
},
"peerDependencies": {
"react": "17.0.2"
}
},
"node_modules/read-cmd-shim": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz",
@ -3924,6 +3986,11 @@
"node": ">=8.10.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
"node_modules/request": {
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
@ -4095,6 +4162,15 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"node_modules/scheduler": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"node_modules/semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
@ -4352,6 +4428,16 @@
"node": ">= 8"
}
},
"node_modules/standardized-audio-context": {
"version": "25.3.20",
"resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.20.tgz",
"integrity": "sha512-c6eMQXmN7iDS7ROuSqOrHQhxpazerJSnRHEJiKD8YkruZBTt/a5E7zmk+KkStoi0dohFAod8wvwWxc7S1gmdig==",
"dependencies": {
"@babel/runtime": "^7.17.0",
"automation-events": "^4.0.12",
"tslib": "^2.3.1"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@ -4506,6 +4592,15 @@
"node": ">=8.0"
}
},
"node_modules/tone": {
"version": "14.7.77",
"resolved": "https://registry.npmjs.org/tone/-/tone-14.7.77.tgz",
"integrity": "sha512-tCfK73IkLHyzoKUvGq47gyDyxiKLFvKiVCOobynGgBB9Dl0NkxTM2p+eRJXyCYrjJwy9Y0XCMqD3uOYsYt2Fdg==",
"dependencies": {
"standardized-audio-context": "^25.1.8",
"tslib": "^2.0.1"
}
},
"node_modules/tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
@ -4528,8 +4623,7 @@
"node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
@ -4984,6 +5078,14 @@
}
}
},
"@babel/runtime": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.0.tgz",
"integrity": "sha512-etcO/ohMNaNA2UBdaXBBSX/3aEzFMRrVfaPv8Ptc0k+cWpWW0QFiGZ2XnVqQZI1Cf734LbPGmqBKWESfW4x/dQ==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"@gar/promisify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz",
@ -5459,6 +5561,15 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
},
"automation-events": {
"version": "4.0.13",
"resolved": "https://registry.npmjs.org/automation-events/-/automation-events-4.0.13.tgz",
"integrity": "sha512-SYHkG0A0x+JwLBHSexZhlv9mcYlvFpen9S2zTNjchfp5EDSbrs3Fm9Teje0PWpUNTV5W1C/kEPdZ4RTrGOm7Hg==",
"requires": {
"@babel/runtime": "^7.17.0",
"tslib": "^2.3.1"
}
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@ -6861,8 +6972,7 @@
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"js-yaml": {
"version": "4.1.0",
@ -7003,6 +7113,14 @@
"is-unicode-supported": "^0.1.0"
}
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
@ -7423,8 +7541,7 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"once": {
"version": "1.4.0",
@ -7873,6 +7990,25 @@
"safe-buffer": "^5.1.0"
}
},
"react": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"react-dom": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"scheduler": "^0.20.2"
}
},
"read-cmd-shim": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz",
@ -7933,6 +8069,11 @@
"picomatch": "^2.2.1"
}
},
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
"request": {
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
@ -8053,6 +8194,15 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
"scheduler": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
@ -8255,6 +8405,16 @@
"minipass": "^3.1.1"
}
},
"standardized-audio-context": {
"version": "25.3.20",
"resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.20.tgz",
"integrity": "sha512-c6eMQXmN7iDS7ROuSqOrHQhxpazerJSnRHEJiKD8YkruZBTt/a5E7zmk+KkStoi0dohFAod8wvwWxc7S1gmdig==",
"requires": {
"@babel/runtime": "^7.17.0",
"automation-events": "^4.0.12",
"tslib": "^2.3.1"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@ -8371,6 +8531,15 @@
"is-number": "^7.0.0"
}
},
"tone": {
"version": "14.7.77",
"resolved": "https://registry.npmjs.org/tone/-/tone-14.7.77.tgz",
"integrity": "sha512-tCfK73IkLHyzoKUvGq47gyDyxiKLFvKiVCOobynGgBB9Dl0NkxTM2p+eRJXyCYrjJwy9Y0XCMqD3uOYsYt2Fdg==",
"requires": {
"standardized-audio-context": "^25.1.8",
"tslib": "^2.0.1"
}
},
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
@ -8390,8 +8559,7 @@
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"tunnel-agent": {
"version": "0.6.0",

45
repl/public/hot.js Normal file
View File

@ -0,0 +1,45 @@
// this file can be used to livecode from the comfort of your editor.
// just export a pattern from export default
// enable hot mode by pressing "toggle hot mode" on the top right of the repl
import { mini, h } from '../src/parse';
import { sequence, pure, reify, slowcat, fastcat, cat, stack, silence } from '../../strudel.mjs';
import { gain, filter } from '../src/tone';
export default stack(
sequence(
mini(
'e5 [b4 c5] d5 [c5 b4]',
'a4 [a4 c5] e5 [d5 c5]',
'b4 [~ c5] d5 e5',
'c5 a4 a4 ~',
'[~ d5] [~ f5] a5 [g5 f5]',
'e5 [~ c5] e5 [d5 c5]',
'b4 [b4 c5] d5 e5',
'c5 a4 a4 ~'
)
.synth({
oscillator: { type: 'sine' },
envelope: { attack: 0.1 },
})
.rev()
),
sequence(
mini(
'e2 e3 e2 e3 e2 e3 e2 e3',
'a2 a3 a2 a3 a2 a3 a2 a3',
'g#2 g#3 g#2 g#3 e2 e3 e2 e3',
'a2 a3 a2 a3 a2 a3 b1 c2',
'd2 d3 d2 d3 d2 d3 d2 d3',
'c2 c3 c2 c3 c2 c3 c2 c3',
'b1 b2 b1 b2 e2 e3 e2 e3',
'a1 a2 a1 a2 a1 a2 a1 a2'
)
.synth({
oscillator: { type: 'sawtooth' },
envelope: { attack: 0.1 },
})
.chain(gain(0.7), filter(2000))
.rev()
)
).slow(16);

View File

@ -27,7 +27,7 @@ export default {
},
packageOptions: {
/* ... */
knownEntrypoints: ['fraction.js'],
knownEntrypoints: ['fraction.js', 'codemirror'],
},
devOptions: {
tailwindConfig: './tailwind.config.js',

View File

@ -1,22 +1,27 @@
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import logo from './logo.svg';
import * as strudel from '../../strudel.mjs';
import cx from './cx';
import * as Tone from 'tone';
import useCycle from './useCycle';
import type { Hap, Pattern } from './types';
import type { Pattern } from './types';
import * as tunes from './tunes';
import * as krill from './parse';
import * as parser from './parse';
import CodeMirror from './CodeMirror';
import hot from '../public/hot';
const { tetris, tetrisMini, tetrisHaskell } = tunes;
const { tetris, tetrisRev } = tunes;
const { parse } = parser;
const { sequence, pure, reify, slowcat, fastcat, cat, stack, silence } = strudel; // make available to eval
const { mini, h } = krill; // for eval (direct import wont work somehow)
const parse = (code: string): Pattern => eval(code);
const getHotCode = async () => {
return fetch('/hot.js')
.then((res) => res.text())
.then((src) => {
return src.split('export default').slice(-1)[0].trim();
});
};
const synth = new Tone.PolySynth().toDestination();
synth.set({
const defaultSynth = new Tone.PolySynth().toDestination();
defaultSynth.set({
oscillator: { type: 'triangle' },
envelope: {
release: 0.01,
@ -25,11 +30,13 @@ synth.set({
function App() {
const [mode, setMode] = useState<string>('javascript');
const [code, setCode] = useState<string>(tetrisHaskell);
const [code, setCode] = useState<string>(tetrisRev);
const [log, setLog] = useState('');
const logBox = useRef<any>();
const [error, setError] = useState<Error>();
const [pattern, setPattern] = useState<Pattern>();
const [activePattern, setActivePattern] = useState<Pattern>();
const [isHot, setIsHot] = useState(false); // set to true to enable live coding in hot.js, using dev server
// logs events of cycle
const logCycle = (_events: any, cycle: any) => {
if (_events.length) {
@ -39,81 +46,137 @@ function App() {
// cycle hook to control scheduling
const cycle = useCycle({
onEvent: useCallback((time, event) => {
// console.log('event', event, time);
synth.triggerAttackRelease(event.value, event.duration, time);
try {
if (typeof event.value === 'string') {
defaultSynth.triggerAttackRelease(event.value, event.duration, time);
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
} else {
const { onTrigger } = event.value;
onTrigger(time, event);
}
setError(undefined);
} catch (err: any) {
console.warn(err);
err.message = 'unplayable event: ' + err?.message;
setError(err);
}
}, []),
onQuery: useCallback(
(span) => {
try {
return pattern?.query(span) || [];
return activePattern?.query(span) || [];
} catch (err: any) {
setError(err);
return [];
}
},
[pattern]
[activePattern]
),
onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), [pattern]),
ready: !!pattern,
onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), [activePattern]),
ready: !!activePattern,
});
// set active pattern on ctrl+enter
useLayoutEffect(() => {
const handleKeyPress = (e: any) => {
if (e.ctrlKey && e.code === 'Enter') {
setActivePattern(() => pattern);
!cycle.started && cycle.start();
}
};
document.addEventListener('keypress', handleKeyPress);
return () => document.removeEventListener('keypress', handleKeyPress);
}, [pattern]);
// parse pattern when code changes
useEffect(() => {
try {
let _pattern: Pattern;
try {
_pattern = h(code);
setMode('pegjs'); // haskell mode does not recognize quotes, pegjs looks ok by accident..
} catch (err) {
setMode('javascript');
// code is not haskell like
_pattern = parse(code);
if (_pattern?.constructor?.name !== 'Pattern') {
const message = `got "${typeof _pattern}" instead of pattern`;
throw new Error(message + (typeof _pattern === 'function' ? ', did you forget to call a function?' : '.'));
}
let _code = code;
// handle hot mode
if (isHot) {
if (typeof hot !== 'string') {
getHotCode().then((_code) => {
setCode(_code);
setMode('javascript');
}); // if using HMR, just use changed file
setActivePattern(hot);
return;
} else {
_code = hot;
setCode(_code);
}
setPattern(() => _pattern); // need arrow function here! otherwise if user returns a function, react will think it's a state reducer
}
// normal mode
try {
const parsed = parse(_code);
// need arrow function here! otherwise if user returns a function, react will think it's a state reducer
// only first time, then need ctrl+enter
pattern;
setPattern(() => parsed.pattern);
if (!activePattern || isHot) {
setActivePattern(() => parsed.pattern);
}
setMode(parsed.mode);
setError(undefined);
} catch (err: any) {
console.warn(err);
setError(err);
}
}, [code]);
}, [code, isHot]);
// scroll log box to bottom when log changes
useLayoutEffect(() => {
logBox.current.scrollTop = logBox.current?.scrollHeight;
}, [log]);
return (
<div className="h-screen bg-slate-900 flex flex-col">
<header className="flex-none w-full h-16 px-2 flex items-center space-x-2 border-b border-gray-200 bg-white">
<img src={logo} className="Tidal-logo w-16 h-16" alt="logo" />
<h1 className="text-2xl">Strudel REPL</h1>
<header className="flex-none w-full h-16 px-2 flex border-b border-gray-200 bg-white justify-between">
<div className="flex items-center space-x-2">
<img src={logo} className="Tidal-logo w-16 h-16" alt="logo" />
<h1 className="text-2xl">Strudel REPL</h1>
</div>
{window.location.href.includes('http://localhost:8080') && (
<button
onClick={() => {
if (isHot || confirm('Really switch? You might loose your current pattern..')) {
setIsHot((h) => !h);
}
}}
>
{isHot ? '🔥' : ' '} toggle hot mode
</button>
)}
</header>
<section className="grow flex flex-col p-2 text-gray-100">
<div className="grow relative">
<div className={cx('h-full bg-slate-600', error ? 'focus:ring-red-500' : 'focus:ring-slate-800')}>
<div className={cx('h-full bg-[#2A3236]', error ? 'focus:ring-red-500' : 'focus:ring-slate-800')}>
<CodeMirror
value={code}
readOnly={isHot}
options={{
mode,
theme: 'material',
lineNumbers: true,
}}
onChange={(_: any, __: any, value: any) => {
setLog((log) => log + `${log ? '\n\n' : ''}✏️ edit\n${code}\n${value}`);
setCode(value);
if (!isHot) {
// setLog((log) => log + `${log ? '\n\n' : ''}✏️ edit\n${code}\n${value}`);
setCode(value);
}
}}
/>
<span className="p-4 absolute bottom-0 left-0 text-xs whitespace-pre">
{!cycle.started
? `press ctrl+enter to play\n`
: !isHot && activePattern !== pattern
? `ctrl+enter to update\n`
: 'no changes\n'}
{!isHot && <>{{ pegjs: 'mini' }[mode] || mode} mode</>}
{isHot && '🔥 hot mode: go to hot.js to edit pattern, then save'}
</span>
</div>
{error && <div className="absolute right-2 bottom-2 text-red-500">{error?.message || 'unknown error'}</div>}
{/* <textarea
className={cx('w-full h-64 bg-slate-600', error ? 'focus:ring-red-500' : 'focus:ring-slate-800')}
value={code}
onChange={(e) => {
setLog((log) => log + `${log ? '\n\n' : ''}✏️ edit\n${code}\n${e.target.value}`);
setCode(e.target.value);
}}
/> */}
</div>
<button
className="flex-none w-full border border-gray-700 p-2 bg-slate-700 hover:bg-slate-500"
@ -122,7 +185,7 @@ function App() {
{cycle.started ? 'pause' : 'play'}
</button>
<textarea
className="grow bg-[#283237] border-0"
className="grow bg-[#283237] border-0 text-xs"
value={log}
readOnly
ref={logBox}

View File

@ -6,7 +6,7 @@ ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root'),
document.getElementById('root')
);
// Hot Module Replacement (HMR) - Remove this snippet to remove HMR.

View File

@ -1,8 +1,13 @@
import * as krill from '../krill-parser';
import * as strudel from '../../strudel.mjs';
import { Scale, Note, Interval } from '@tonaljs/tonal';
import './tone';
import * as toneStuff from './tone';
const { sequence, stack, silence, Fraction, pure } = strudel;
// even if some functions are not used, we need them to be available in eval
const { pure, stack, slowcat, fastcat, cat, sequence, polymeter, pm, polyrhythm, pr, /* reify, */ silence, Fraction } =
strudel;
const { autofilter, filter, gain } = toneStuff;
function reify(thing: any) {
if (thing?.constructor?.name === 'Pattern') {
@ -99,3 +104,21 @@ export const h = (string: string) => {
// console.log('ast', ast);
return patternifyAST(ast);
};
export const parse: any = (code: string) => {
let _pattern;
let mode;
try {
_pattern = h(code);
mode = 'pegjs';
} catch (err) {
// code is not haskell like
mode = 'javascript';
_pattern = eval(code);
if (_pattern?.constructor?.name !== 'Pattern') {
const message = `got "${typeof _pattern}" instead of pattern`;
throw new Error(message + (typeof _pattern === 'function' ? ', did you forget to call a function?' : '.'));
}
}
return { mode, pattern: _pattern };
};

96
repl/src/tone.ts Normal file
View File

@ -0,0 +1,96 @@
import { Pattern as _Pattern } from '../../strudel.mjs';
import { AutoFilter, Destination, Filter, Gain, Transport, Synth } from 'tone';
const Pattern = _Pattern as any;
const getTrigger = (getChain: any, value: any) => (time: number, event: any) => {
const chain = getChain(time); // make sure this returns a node that is connected toDestination
chain.triggerAttackRelease(value, event.duration, time);
Transport.scheduleOnce(() => {
chain.dispose(); // mark for garbage collection
}, '+' + event.duration * 2);
};
Pattern.prototype._synth = function (type: any = 'triangle') {
return this.fmap((value: any) => {
value = typeof value !== 'object' && !Array.isArray(value) ? { value } : value;
const instrumentConfig: any = {
oscillator: { type },
envelope: { attack: 0.01, decay: 0.01, sustain: 0.6, release: 0.01 },
};
const getInstrument = () => {
const instrument = new Synth();
instrument.set(instrumentConfig);
return instrument;
};
const onTrigger = getTrigger(() => getInstrument().toDestination(), value.value);
return { ...value, getInstrument, instrumentConfig, onTrigger };
});
};
Pattern.prototype.synth = function (type: any = 'triangle') {
return this._patternify(Pattern.prototype._synth)(type);
};
Pattern.prototype.adsr = function (attack = 0.01, decay = 0.01, sustain = 0.6, release = 0.01) {
return this.fmap((value: any) => {
if (!value?.getInstrument) {
throw new Error('cannot chain adsr: need instrument first (like synth)');
}
const instrumentConfig = { ...value.instrumentConfig, envelope: { attack, decay, sustain, release } };
const getInstrument = () => {
const instrument = value.getInstrument();
instrument.set(instrumentConfig);
return instrument;
};
const onTrigger = getTrigger(() => getInstrument().toDestination(), value.value);
return { ...value, getInstrument, instrumentConfig, onTrigger };
});
};
Pattern.prototype.chain = function (...effectGetters: any) {
return this.fmap((value: any) => {
if (!value?.getInstrument) {
throw new Error('cannot chain: need instrument first (like synth)');
}
const chain = (value.chain || []).concat(effectGetters);
const getChain = (time: number) => {
const effects = chain.map((getEffect: any) => getEffect(time));
return value.getInstrument().chain(...effects, Destination);
};
const onTrigger = getTrigger(getChain, value.value);
return { ...value, getChain, onTrigger, chain };
});
};
export const autofilter =
(freq = 1) =>
() =>
new AutoFilter(freq).start();
export const filter =
(freq = 1, q = 1, type: BiquadFilterType = 'lowpass') =>
() =>
new Filter(freq, type); // .Q.setValueAtTime(q, time);
export const gain =
(gain: number = 0.9) =>
() =>
new Gain(gain);
Pattern.prototype._gain = function (g: number) {
return this.chain(gain(g));
};
Pattern.prototype.gain = function (g: number) {
return this._patternify(Pattern.prototype._gain)(g);
};
Pattern.prototype._filter = function (freq: number, q: number, type: BiquadFilterType = 'lowpass') {
return this.chain(filter(freq, q, type));
};
Pattern.prototype.filter = function (freq: number) {
return this._patternify(Pattern.prototype._filter)(freq);
};
Pattern.prototype.autofilter = function (g: number) {
return this.chain(autofilter(g));
};

View File

@ -44,7 +44,44 @@ export const tetris = `stack(
'a1 a2 a1 a2 a1 a2 a1 a2'
)
)
)._slow(16);`;
).slow(16).synth({
oscillator: {type: 'sawtooth'}
})`;
export const tetrisRev = `stack(
sequence(
mini(
'e5 [b4 c5] d5 [c5 b4]',
'a4 [a4 c5] e5 [d5 c5]',
'b4 [~ c5] d5 e5',
'c5 a4 a4 ~',
'[~ d5] [~ f5] a5 [g5 f5]',
'e5 [~ c5] e5 [d5 c5]',
'b4 [b4 c5] d5 e5',
'c5 a4 a4 ~'
).rev()
),
sequence(
mini(
'e2 e3 e2 e3 e2 e3 e2 e3',
'a2 a3 a2 a3 a2 a3 a2 a3',
'g#2 g#3 g#2 g#3 e2 e3 e2 e3',
'a2 a3 a2 a3 a2 a3 b1 c2',
'd2 d3 d2 d3 d2 d3 d2 d3',
'c2 c3 c2 c3 c2 c3 c2 c3',
'b1 b2 b1 b2 e2 e3 e2 e3',
'a1 a2 a1 a2 a1 a2 a1 a2'
).rev()
)
).slow(16).synth('sawtooth').filter(1000).gain(0.6)`;
/*
.synth({
oscillator: {type: 'sawtooth'},
envelope: { attack: 0.1 }
}).filter(1200).gain(0.8)
*/
export const tetrisMini1 = `mini('[[e5 [b4 c5] d5 [c5 b4]] [a4 [a4 c5] e5 [d5 c5]] [b4 [~ c5] d5 e5] [c5 a4 a4 ~] [[~ d5] [~ f5] a5 [g5 f5]] [e5 [~ c5] e5 [d5 c5]] [b4 [b4 c5] d5 e5] [c5 a4 a4 ~]],[[e2 e3 e2 e3 e2 e3 e2 e3] [a2 a3 a2 a3 a2 a3 a2 a3] [g#2 g#3 g#2 g#3 e2 e3 e2 e3] [a2 a3 a2 a3 a2 a3 b1 c2] [d2 d3 d2 d3 d2 d3 d2 d3] [c2 c3 c2 c3 c2 c3 c2 c3] [b1 b2 b1 b2 e2 e3 e2 e3] [a1 a2 a1 a2 a1 a2 a1 a2]]')._slow(16);`;
export const tetrisMini = `mini(\`[[e5 [b4 c5] d5 [c5 b4]]
@ -103,25 +140,15 @@ export const tetrisHaskell = `slow 16 $ "[[e5 [b4 c5] d5 [c5 b4]]
/*
export const tetrisHaskell = `h(\`slow 16 $ "[[e5 [b4 c5] d5 [c5 b4]] [a4 [a4 c5] e5 [d5 c5]] [b4 [~ c5] d5 e5] [c5 a4 a4 ~] [[~ d5] [~ f5] a5 [g5 f5]] [e5 [~ c5] e5 [d5 c5]] [b4 [b4 c5] d5 e5] [c5 a4 a4 ~]], [[e2 e3]*4] [[a2 a3]*4] [[g#2 g#3]*2 [e2 e3]*2] [a2 a3 a2 a3 a2 a3 b1 c2] [[d2 d3]*4] [[c2 c3]*4] [[b1 b2]*2 [e2 e3]*2] [[a1 a2]*4]"\`)`;
*/
// "sequence('c3', 'eb3', sequence('g3', 'f3'))" //
/* `sequence(
stack('c4','eb4','g4'),
stack('bb3','d4','f4'),
stack('ab3','c4','eb4'),
stack('g3','b3','d4')
)._slow(4)`, */ //
export const spanish = `slowcat(
stack('c4','eb4','g4'),
stack('bb3','d4','f4'),
stack('ab3','c4','eb4'),
stack('g3','b3','d4')
)`;
/* `fastcat(
stack('c4','eb4','g4'),
stack('bb3','d4','f4'),
stack('ab3','c4','eb4'),
stack('g3','b3','d4')
)._slow(4)` */ //
// "slow(sequence('c3', 'eb3', sequence('g3', 'f3')), 'g3')" //
// "sequence('c3', 'eb3')._fast(2)" //
export const whirlyStrudel = `mini("[e4 [b2 b3] c4]")
.every(4, x => x.fast(2))
.every(3, x => x.slow(1.5))
.fast(slowcat(1.25,1,1.5))
.every(2, _ => mini("e4 ~ e3 d4 ~"))`;

View File

@ -1,5 +1,5 @@
{
"include": ["src", "types"],
"include": ["src", "types", "public/hot.js"],
"compilerOptions": {
"allowJs": true,
"module": "esnext",