mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 13:48:34 +00:00
hot mode + tone synth experiments
This commit is contained in:
parent
89ee94e953
commit
20cbaf913a
192
package-lock.json
generated
192
package-lock.json
generated
@ -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
45
repl/public/hot.js
Normal 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);
|
||||
@ -27,7 +27,7 @@ export default {
|
||||
},
|
||||
packageOptions: {
|
||||
/* ... */
|
||||
knownEntrypoints: ['fraction.js'],
|
||||
knownEntrypoints: ['fraction.js', 'codemirror'],
|
||||
},
|
||||
devOptions: {
|
||||
tailwindConfig: './tailwind.config.js',
|
||||
|
||||
157
repl/src/App.tsx
157
repl/src/App.tsx
@ -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}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
96
repl/src/tone.ts
Normal 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));
|
||||
};
|
||||
@ -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 ~"))`;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"include": ["src", "types"],
|
||||
"include": ["src", "types", "public/hot.js"],
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"module": "esnext",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user