mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 22:35:15 -04:00
Compare commits
325 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f57060708 | |||
| 1507724ecd | |||
| 4bc2f64b24 | |||
| 839ba31dfb | |||
| 2341452d91 | |||
| 7ab263b514 | |||
| 45b5b5960f | |||
| 1b8ec1f01c | |||
| c92cb1c096 | |||
| 8f31fbe275 | |||
| aa76419eda | |||
| 21503c0c38 | |||
| d957b19f55 | |||
| 122c4242ce | |||
| e769ed2fd8 | |||
| c722904831 | |||
| c93faa9564 | |||
| ce6495a46d | |||
| 04f2b905af | |||
| b668a2c0d2 | |||
| 5fc8f10602 | |||
| a27f399a9e | |||
| ff5d14fb63 | |||
| 27fb2d2d5b | |||
| 0b9d59bf0e | |||
| 23e059a065 | |||
| 45c7b29a96 | |||
| 3e6c3fda60 | |||
| 1a1363ef77 | |||
| b5c51e231b | |||
| 22711dd7be | |||
| 1ba4d3d5d1 | |||
| 823ab73585 | |||
| 66c444efac | |||
| b688b8921a | |||
| 00550ddab5 | |||
| 42fedec088 | |||
| ff3bc0e8c2 | |||
| dde7dff870 | |||
| 25ffc9cf96 | |||
| eca92cf5b5 | |||
| 372bdf8be7 | |||
| c7c90b0647 | |||
| 14c2da4fa2 | |||
| c99d957bc8 | |||
| 51b14d55ec | |||
| f92d9994df | |||
| 431e1b87bd | |||
| 3ec2fc6030 | |||
| 99f416bf6b | |||
| 89acc4870b | |||
| 314c739c3a | |||
| 30b18fc997 | |||
| 168ac60853 | |||
| dff0c0c3b0 | |||
| ea3e9de7ab | |||
| c93fd900bc | |||
| a761aa9a37 | |||
| f37eb92d93 | |||
| 2c7cc5d325 | |||
| 16659c82d2 | |||
| 4ed3ca4128 | |||
| 9b2899c8fb | |||
| fada7fc9cc | |||
| 2be5bca42b | |||
| b1ebf6367c | |||
| 57e373f578 | |||
| 5c01874a32 | |||
| dd89e11633 | |||
| 4e51f19780 | |||
| 33d37d8dee | |||
| 60ce34af43 | |||
| 2602ff6e34 | |||
| ef5cd36a37 | |||
| 7d47f5aba3 | |||
| 33cb506c71 | |||
| 5fb36acb04 | |||
| 5f985f359b | |||
| 09045d5387 | |||
| 371af755df | |||
| 0485632e22 | |||
| c3d6de2412 | |||
| 36d49b11d9 | |||
| 866277510e | |||
| da8e00eb4e | |||
| cd30b2af3f | |||
| 38b3337d35 | |||
| 84d34aa12b | |||
| 96b7b68900 | |||
| 2a6ffbf5a4 | |||
| 1ac99e260b | |||
| 519062c8d0 | |||
| 78d7cdf662 | |||
| a191b1e811 | |||
| b372161874 | |||
| 5bbec7b17c | |||
| 54b847a8ef | |||
| a40a0f730f | |||
| a677b94297 | |||
| d102528d0a | |||
| 854692f975 | |||
| bdc321df4d | |||
| 230e8e9d4d | |||
| 96c49689ec | |||
| 698f560d56 | |||
| 67ee493e56 | |||
| 4fc7ef698a | |||
| 89434fc3bd | |||
| 6129f9afe8 | |||
| e5f38dbbc6 | |||
| 5534db95bc | |||
| 37c45424fd | |||
| ed44f4ed0c | |||
| f829f35cfa | |||
| 3033acadbb | |||
| 5c2a8a68c0 | |||
| e9544f8f92 | |||
| 1a6743dba9 | |||
| 06d6621c14 | |||
| d7b82edae3 | |||
| 5eae156134 | |||
| d8843e6057 | |||
| cf7c734778 | |||
| 240a8930ef | |||
| 4f3046593a | |||
| a0bd5bdd35 | |||
| e3bc63e4fa | |||
| fbf7c6293f | |||
| e128b02da1 | |||
| 2168dbc6fc | |||
| 97db895354 | |||
| 48fc68bfdb | |||
| 3741ffbb59 | |||
| caf58492f2 | |||
| 0c5bdd0205 | |||
| ae6da0d6ef | |||
| bdbb4cb495 | |||
| 566c358e8d | |||
| 324cffe6df | |||
| dadb8119e8 | |||
| 335d54b5f4 | |||
| 0fd851999f | |||
| 50f88798d8 | |||
| 8033cfc49d | |||
| 882f450ee8 | |||
| 4d60a582fe | |||
| 7ef3d33d0e | |||
| 24328ee8ae | |||
| d45c380389 | |||
| fbb3d144a1 | |||
| 7e7e299bce | |||
| d18116fcbe | |||
| e38513f077 | |||
| 08c4c641ec | |||
| 5d7e46b246 | |||
| dac906a79d | |||
| ecfeac4d63 | |||
| db1fc399a1 | |||
| 60616a5228 | |||
| 699c8d5b60 | |||
| 4264f013af | |||
| ecc4e1803c | |||
| 5c230c2d65 | |||
| 6af681354b | |||
| 5f381cf153 | |||
| 2ed290a214 | |||
| cf11472348 | |||
| 2c14514c56 | |||
| 053e6d8325 | |||
| 603c55fda7 | |||
| 113adcc5cc | |||
| a5bf8f6732 | |||
| c4b482634f | |||
| 4b083d6f81 | |||
| 04973d153b | |||
| 69ce7c78a4 | |||
| b64cf197f2 | |||
| a8b0279853 | |||
| e715e2ba2f | |||
| 985de36647 | |||
| b7b576e38b | |||
| 5018669ded | |||
| c5342d3eb0 | |||
| 0e7a0b5754 | |||
| 933ef02df0 | |||
| 513e0d748d | |||
| 852f01fddd | |||
| 9ebada63e9 | |||
| dbda70eee9 | |||
| 18bd10dfed | |||
| f76d03a0dc | |||
| 86ba5c9c1a | |||
| 5b56fbed97 | |||
| dfdd918ea0 | |||
| df2c7ec1a1 | |||
| 0d179f2042 | |||
| 63c6a9735c | |||
| f7e3f7d187 | |||
| cc54da5980 | |||
| ca82796081 | |||
| d092068bd2 | |||
| 0c1529237c | |||
| 6dbb2cb98a | |||
| ead763b909 | |||
| 415139851e | |||
| 5c99b72e2f | |||
| 82cb6c93cf | |||
| d21734ccf2 | |||
| 91934006d5 | |||
| 014c258c57 | |||
| 5e76bc3f2f | |||
| 7f6940f3a5 | |||
| 8d5682deb0 | |||
| 819abbfa7a | |||
| 227ad332b8 | |||
| 17c83e1a85 | |||
| 5d699496ca | |||
| e4f4fe81ae | |||
| abe28367fb | |||
| cc394d9f74 | |||
| 476b51ee31 | |||
| ec823ade14 | |||
| 0c3777f7d7 | |||
| a679e47c0d | |||
| 1e82735241 | |||
| a09ec6b0b5 | |||
| 0134a50ffa | |||
| 1da005e811 | |||
| 8ab912429b | |||
| 88d12223a5 | |||
| 6c363d9c4b | |||
| 6d3d9659e2 | |||
| 8977c5cef4 | |||
| d3645f6c63 | |||
| dbb057621d | |||
| 3a65bc0650 | |||
| 1eb6ff8c17 | |||
| 505cc01a18 | |||
| fdc4bb455f | |||
| c1a27aa6e4 | |||
| a14c7233d4 | |||
| c786d77b07 | |||
| e4913dfb73 | |||
| dfd33bab8e | |||
| 19b8f073b1 | |||
| b38b16f70e | |||
| dd69e4caff | |||
| 762a17d949 | |||
| d90cb23f22 | |||
| 1ccc391c8e | |||
| cafcead62e | |||
| 71ca46b21b | |||
| acf339ad13 | |||
| de4726ec8d | |||
| e58aff41f9 | |||
| 3c8acf9e0e | |||
| 782e8522c5 | |||
| dd4464cb08 | |||
| 3fdfd12448 | |||
| a11d23ba62 | |||
| 6bab362171 | |||
| 5d28c5c825 | |||
| b9173ebe50 | |||
| 0a1577ff43 | |||
| a616d6fd3e | |||
| 149b2ab9f9 | |||
| 4406dd88d1 | |||
| 48a950faf8 | |||
| 0ffbb5cb2c | |||
| f4c31bcf2c | |||
| f0e412a443 | |||
| ce91d36b7a | |||
| a022e5bb93 | |||
| 8e61673bd6 | |||
| 88e39d22f1 | |||
| 1441374f6e | |||
| d1cedaa3e8 | |||
| 5b721f6453 | |||
| a5f05b4715 | |||
| 96f49f117a | |||
| 88e1211762 | |||
| 59e1664223 | |||
| 42922a0d75 | |||
| 4f8a2a0900 | |||
| 9a2a26e904 | |||
| 067c3433b6 | |||
| d0b07c4890 | |||
| 24f8f48a2f | |||
| acafcc9888 | |||
| ec4cfe6f86 | |||
| 7b0c80c8be | |||
| 29029714ee | |||
| cd1203d1e2 | |||
| 4dfc34515c | |||
| 11a9f3ced3 | |||
| 5be1a7000c | |||
| 9059c1b850 | |||
| ec60fe42be | |||
| 275931969e | |||
| e37e0f189a | |||
| 8103943396 | |||
| 9497c99e9e | |||
| 2172d1a767 | |||
| e72c47869a | |||
| 5ea2124e91 | |||
| 0c08919bfa | |||
| a7562c3803 | |||
| 5f4d8f6862 | |||
| 9c7d0951b7 | |||
| 75e57c75cf | |||
| 04502b05d4 | |||
| e3e3a8404a | |||
| 48bf17614f | |||
| 1941b1ff65 | |||
| 46aa3d275a | |||
| d667e790b9 | |||
| bc345b569b | |||
| ee84b4e013 | |||
| e77129fa51 | |||
| 591855919e | |||
| 7af6188a8a | |||
| 1cd31cfce9 | |||
| 227dda921c | |||
| bdf2047ea1 | |||
| 0522996b7f |
@@ -0,0 +1,46 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
deployments: write
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: "npm"
|
||||
- name: Install Dependencies
|
||||
run: npm ci && cd repl && npm ci && cd ../tutorial && npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v2
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
# Upload entire repository
|
||||
path: "./out"
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v1
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16, 17]
|
||||
node-version: [18]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
+3
-1
@@ -35,4 +35,6 @@ tutorial.rendered.mdx
|
||||
doc.json
|
||||
talk/public/EmuSP12
|
||||
talk/public/samples
|
||||
server/samples/old
|
||||
server/samples/old
|
||||
repl/stats.html
|
||||
coverage
|
||||
Vendored
+4
-1
@@ -2,5 +2,8 @@
|
||||
"cSpell.words": [
|
||||
"subspan",
|
||||
"vals"
|
||||
]
|
||||
],
|
||||
"yaml.schemas": {
|
||||
"https://json.schemastore.org/github-workflow.json": "file:///home/felix/projects/strudel/.github/workflows/deploy.yml"
|
||||
}
|
||||
}
|
||||
+18
-10
@@ -12,7 +12,7 @@ To get in touch with the contributors, either
|
||||
|
||||
## Ask a Question
|
||||
|
||||
If you have any questions about strudel, make sure you've read the
|
||||
If you have any questions about strudel, make sure you've read the
|
||||
[tutorial](https://strudel.tidalcycles.org/tutorial/) to find out if it answers your question.
|
||||
If not, use one of the Communication Channels above!
|
||||
|
||||
@@ -31,10 +31,9 @@ Use one of the Communication Channels listed above.
|
||||
|
||||
## Improve the Tutorial
|
||||
|
||||
If you find some weak spots in the [tutorial](https://strudel.tidalcycles.org/),
|
||||
If you find some weak spots in the [tutorial](https://strudel.tidalcycles.org/),
|
||||
you are welcome to improve them by editing [this file](https://github.com/tidalcycles/strudel/blob/main/tutorial/tutorial.mdx).
|
||||
|
||||
|
||||
This will even work without setting up a development environment, only a github account is required.
|
||||
|
||||
## Propose a Feature
|
||||
@@ -52,11 +51,11 @@ Please check that it has not been reported before.
|
||||
To fix a bug that has been reported,
|
||||
|
||||
1. check that nobody else is already fixing it and respond to the issue to let people know you're on it
|
||||
3. fork the repository
|
||||
4. make sure you've setup the project (see below)
|
||||
5. hopefully fix the bug
|
||||
6. make sure the tests pass
|
||||
7. send a pull request
|
||||
2. fork the repository
|
||||
3. make sure you've setup the project (see below)
|
||||
4. hopefully fix the bug
|
||||
5. make sure the tests pass
|
||||
6. send a pull request
|
||||
|
||||
## Write Tests
|
||||
|
||||
@@ -80,7 +79,7 @@ cd repl && npm i # install repl dependencies
|
||||
npm run start # start repl
|
||||
```
|
||||
|
||||
Those commands might look slightly different for your OS.
|
||||
Those commands might look slightly different for your OS.
|
||||
Please report any problems you've had with the setup instructions!
|
||||
|
||||
## Code Style
|
||||
@@ -97,7 +96,7 @@ If you use VSCode, you can
|
||||
|
||||
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
|
||||
When you run `npm i` on the root folder, [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces) will symlink all packages
|
||||
in the `node_modules` folder. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
|
||||
in the `node_modules` folder. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
|
||||
which allows developing multiple packages at the same time
|
||||
|
||||
## Package Publishing
|
||||
@@ -105,9 +104,18 @@ which allows developing multiple packages at the same time
|
||||
To publish all packages that have been changed since the last release, run:
|
||||
|
||||
```sh
|
||||
npm login
|
||||
npx lerna publish
|
||||
```
|
||||
|
||||
### New Packages
|
||||
|
||||
To add a new package, you have to publish it manually the first time, using:
|
||||
|
||||
```sh
|
||||
cd packages/<package-name> && npm publish --access public
|
||||
```
|
||||
|
||||
## Have Fun
|
||||
|
||||
Remember to have fun, and that this project is driven by the passion of volunteers!
|
||||
|
||||
+1806
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 150 KiB |
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"source": {
|
||||
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
|
||||
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser"
|
||||
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
|
||||
},
|
||||
"plugins": ["plugins/markdown"],
|
||||
"opts": {
|
||||
|
||||
Generated
+3813
-3180
File diff suppressed because it is too large
Load Diff
+6
-3
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"name": "@strudel.cycles/monorepo",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.4",
|
||||
"private": true,
|
||||
"description": "Port of tidalcycles to javascript",
|
||||
"scripts": {
|
||||
"pretest": "cd tutorial && npm run jsdoc-json",
|
||||
"test": "vitest run --version",
|
||||
"test-ui": "vitest --ui",
|
||||
"test-coverage": "vitest --coverage",
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
|
||||
"snapshot": "cd repl && npm run snapshot",
|
||||
@@ -13,7 +15,7 @@
|
||||
"osc": "cd packages/osc && npm run server",
|
||||
"build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build",
|
||||
"preview": "npx serve ./out",
|
||||
"deploy": "gh-pages -d out",
|
||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
|
||||
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
||||
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json"
|
||||
},
|
||||
@@ -39,13 +41,14 @@
|
||||
"homepage": "https://strudel.tidalcycles.org",
|
||||
"devDependencies": {
|
||||
"@vitest/ui": "^0.21.1",
|
||||
"c8": "^7.12.0",
|
||||
"events": "^3.3.0",
|
||||
"gh-pages": "^4.0.0",
|
||||
"happy-dom": "^6.0.4",
|
||||
"jsdoc": "^3.6.10",
|
||||
"jsdoc-json": "^2.0.2",
|
||||
"jsdoc-to-markdown": "^7.1.1",
|
||||
"lerna": "^4.0.0",
|
||||
"rollup-plugin-visualizer": "^5.8.1",
|
||||
"vitest": "^0.21.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Packages
|
||||
|
||||
Each folder represents one of the @strudel.cycles/* packages [published to npm](https://www.npmjs.com/org/strudel.cycles).
|
||||
|
||||
To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs.
|
||||
|
||||
This is a graphical view of all the packages: [full screen](https://raw.githubusercontent.com/tidalcycles/strudel/main/dependencies.svg)
|
||||
|
||||

|
||||
|
||||
Generated with
|
||||
|
||||
```sh
|
||||
npx depcruise --include-only "^packages" -X "node_modules" --output-type dot packages | dot -T svg > dependencygraph.svg
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
examples
|
||||
+47
-32
@@ -11,7 +11,7 @@ const generic_params = [
|
||||
/**
|
||||
* Select a sound / sample by name.
|
||||
*
|
||||
* <details>
|
||||
* <details style={{display:'none'}}>
|
||||
* <summary>show all sounds</summary>
|
||||
*
|
||||
* 808 (6) 808bd (25) 808cy (25) 808hc (5) 808ht (5) 808lc (5) 808lt (5) 808mc (5) 808mt (5) 808oh (5) 808sd (25) 909 (1) ab (12) ade (10) ades2 (9) ades3 (7) ades4 (6) alex (2) alphabet (26) amencutup (32) armora (7) arp (2) arpy (11) auto (11) baa (7) baa2 (7) bass (4) bass0 (3) bass1 (30) bass2 (5) bass3 (11) bassdm (24) bassfoo (3) battles (2) bd (24) bend (4) bev (2) bin (2) birds (10) birds3 (19) bleep (13) blip (2) blue (2) bottle (13) breaks125 (2) breaks152 (1) breaks157 (1) breaks165 (1) breath (1) bubble (8) can (14) casio (3) cb (1) cc (6) chin (4) circus (3) clak (2) click (4) clubkick (5) co (4) coins (1) control (2) cosmicg (15) cp (2) cr (6) crow (4) d (4) db (13) diphone (38) diphone2 (12) dist (16) dork2 (4) dorkbot (2) dr (42) dr2 (6) dr55 (4) dr_few (8) drum (6) drumtraks (13) e (8) east (9) electro1 (13) em2 (6) erk (1) f (1) feel (7) feelfx (8) fest (1) fire (1) flick (17) fm (17) foo (27) future (17) gab (10) gabba (4) gabbaloud (4) gabbalouder (4) glasstap (3) glitch (8) glitch2 (8) gretsch (24) gtr (3) h (7) hand (17) hardcore (12) hardkick (6) haw (6) hc (6) hh (13) hh27 (13) hit (6) hmm (1) ho (6) hoover (6) house (8) ht (16) if (5) ifdrums (3) incoming (8) industrial (32) insect (3) invaders (18) jazz (8) jungbass (20) jungle (13) juno (12) jvbass (13) kicklinn (1) koy (2) kurt (7) latibro (8) led (1) less (4) lighter (33) linnhats (6) lt (16) made (7) made2 (1) mash (2) mash2 (4) metal (10) miniyeah (4) monsterb (6) moog (7) mouth (15) mp3 (4) msg (9) mt (16) mute (28) newnotes (15) noise (1) noise2 (8) notes (15) numbers (9) oc (4) odx (15) off (1) outdoor (6) pad (3) padlong (1) pebbles (1) perc (6) peri (15) pluck (17) popkick (10) print (11) proc (2) procshort (8) psr (30) rave (8) rave2 (4) ravemono (2) realclaps (4) reverbkick (1) rm (2) rs (1) sax (22) sd (2) seawolf (3) sequential (8) sf (18) sheffield (1) short (5) sid (12) sine (6) sitar (8) sn (52) space (18) speakspell (12) speech (7) speechless (10) speedupdown (9) stab (23) stomp (10) subroc3d (11) sugar (2) sundance (6) tabla (26) tabla2 (46) tablex (3) tacscan (22) tech (13) techno (7) tink (5) tok (4) toys (13) trump (11) ul (10) ulgab (5) uxay (3) v (6) voodoo (5) wind (10) wobble (1) world (3) xmas (1) yeah (31)
|
||||
@@ -23,7 +23,7 @@ const generic_params = [
|
||||
* @name s
|
||||
* @param {string | Pattern} sound The sound / pattern of sounds to pick
|
||||
* @example
|
||||
* s("bd hh").osc()
|
||||
* s("bd hh")
|
||||
*
|
||||
*/
|
||||
['s', 's', 'sound'],
|
||||
@@ -40,11 +40,6 @@ const generic_params = [
|
||||
* @example
|
||||
* n("0 1 2 3").s('east').osc()
|
||||
*/
|
||||
// TODO: nOut does not work
|
||||
// TODO: notes don't work as expected
|
||||
// current "workaround" for notes:
|
||||
// s('superpiano').n("<c0 d0 e0 g0>"._asNumber()).osc()
|
||||
// -> .n or .osc (or .superdirt) would need to convert note strings to numbers
|
||||
// also see https://github.com/tidalcycles/strudel/pull/63
|
||||
['f', 'n', 'The note or sample number to choose for a synth or sampleset'],
|
||||
['f', 'note', 'The note or pitch to play a sound or synth with'],
|
||||
@@ -62,12 +57,12 @@ const generic_params = [
|
||||
*/
|
||||
['f', 'accelerate', 'a pattern of numbers that speed up (or slow down) samples while they play.'],
|
||||
/**
|
||||
* Like {@link amp}, but exponential.
|
||||
* Controls the gain by an exponential amount.
|
||||
*
|
||||
* @name gain
|
||||
* @param {number | Pattern} amount gain.
|
||||
* @example
|
||||
* s("bd*8").gain(".7*2 1 .7*2 1 .7 1").osc()
|
||||
* s("hh*8").gain(".4!2 1 .4!2 1 .4 1")
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -100,6 +95,18 @@ const generic_params = [
|
||||
'attack',
|
||||
'a pattern of numbers to specify the attack time (in seconds) of an envelope applied to each sample.',
|
||||
],
|
||||
|
||||
/**
|
||||
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
|
||||
*
|
||||
* @name bank
|
||||
* @param {string | Pattern} bank the name of the bank
|
||||
* @example
|
||||
* s("bd sd").bank('RolandTR909') // = s("RolandTR909_bd RolandTR909_sd")
|
||||
*
|
||||
*/
|
||||
['f', 'bank', 'selects sound bank to use'],
|
||||
|
||||
// TODO: find out how this works?
|
||||
/*
|
||||
* Envelope decay time = the time it takes after the attack time to reach the sustain level.
|
||||
@@ -129,7 +136,7 @@ const generic_params = [
|
||||
* @name bandf
|
||||
* @param {number | Pattern} frequency center frequency
|
||||
* @example
|
||||
* s("bd sd").bandf("<1000 2000 4000 8000>").osc()
|
||||
* s("bd sd,hh*3").bandf("<1000 2000 4000 8000>")
|
||||
*
|
||||
*/
|
||||
['f', 'bandf', 'A pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
|
||||
@@ -140,17 +147,19 @@ const generic_params = [
|
||||
* @name bandq
|
||||
* @param {number | Pattern} q q factor
|
||||
* @example
|
||||
* s("bd sd").bandf(2000).bandq("<.2 .9>").osc()
|
||||
* s("bd sd").bandf(500).bandq("<0 1 2 3>")
|
||||
*
|
||||
*/
|
||||
['f', 'bandq', 'a pattern of anumbers from 0 to 1. Sets the q-factor of the band-pass filter.'],
|
||||
/**
|
||||
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name begin
|
||||
* @param {number | Pattern} amount between 0 and 1, where 1 is the length of the sample
|
||||
* @example
|
||||
* s("rave").begin("<0 .25 .5 .75>").osc()
|
||||
* samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/Dirt-Samples/master/')
|
||||
* s("rave").begin("<0 .25 .5 .75>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -159,12 +168,13 @@ const generic_params = [
|
||||
'a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.',
|
||||
],
|
||||
/**
|
||||
* The same as {@link begin}, but cuts off the end off each sample.
|
||||
* The same as .begin, but cuts off the end off each sample.
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name end
|
||||
* @param {number | Pattern} length 1 = whole sample, .5 = half sample, .25 = quarter sample etc..
|
||||
* @example
|
||||
* s("bd*2,ho*4").end("<.1 .2 .5 1>").osc()
|
||||
* s("bd*2,oh*4").end("<.1 .2 .5 1>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -202,7 +212,7 @@ const generic_params = [
|
||||
* @name crush
|
||||
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
|
||||
* @example
|
||||
* s("<bd sd>,hh*3,jvbass*2").fast(2).crush("<16 8 7 6 5 4 3 2>").osc()
|
||||
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -211,12 +221,12 @@ const generic_params = [
|
||||
'bit crushing, a pattern of numbers from 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).',
|
||||
],
|
||||
/**
|
||||
* fake-resampling for lowering the sample rate
|
||||
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
|
||||
*
|
||||
* @name coarse
|
||||
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
|
||||
* @example
|
||||
* s("xmas").coarse("<1 4 8 16 32>").osc()
|
||||
* s("bd sd,hh*4").coarse("<1 4 8 16 32>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -253,7 +263,7 @@ const generic_params = [
|
||||
* @name cutoff
|
||||
* @param {number | Pattern} frequency audible between 0 and 20000
|
||||
* @example
|
||||
* s("bd,hh*2,<~ sd>").fast(2).cutoff("<4000 2000 1000 500 200 100>").osc()
|
||||
* s("bd sd,hh*3").cutoff("<4000 2000 1000 500 200 100>")
|
||||
*
|
||||
*/
|
||||
// TODO: add lpf synonym
|
||||
@@ -264,7 +274,7 @@ const generic_params = [
|
||||
* @name hcutoff
|
||||
* @param {number | Pattern} frequency audible between 0 and 20000
|
||||
* @example
|
||||
* s("bd,hh*2,<~ sd>").fast(2).hcutoff("<4000 2000 1000 500 200 100>").osc()
|
||||
* s("bd sd,hh*4").hcutoff("<4000 2000 1000 500 200 100>")
|
||||
*
|
||||
*/
|
||||
// TODO: add hpf synonym
|
||||
@@ -274,12 +284,12 @@ const generic_params = [
|
||||
'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the high-pass filter. Also has alias @hpf@',
|
||||
],
|
||||
/**
|
||||
* Applies the cutoff frequency of the high-pass filter.
|
||||
* Applies the resonance of the high-pass filter.
|
||||
*
|
||||
* @name hresonance
|
||||
* @param {number | Pattern} q resonance factor between 0 and 1
|
||||
* @param {number | Pattern} q resonance factor between 0 and 50
|
||||
* @example
|
||||
* s("bd,hh*2,<~ sd>").fast(2).hcutoff(2000).hresonance("<0 .2 .4 .6>").osc()
|
||||
* s("bd sd,hh*4").hcutoff(2000).hresonance("<0 10 20 30>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -292,15 +302,15 @@ const generic_params = [
|
||||
* Applies the cutoff frequency of the low-pass filter.
|
||||
*
|
||||
* @name resonance
|
||||
* @param {number | Pattern} q resonance factor between 0 and 1
|
||||
* @param {number | Pattern} q resonance factor between 0 and 50
|
||||
* @example
|
||||
* s("bd,hh*2,<~ sd>").fast(2).cutoff(2000).resonance("<0 .2 .4 .6>").osc()
|
||||
* s("bd sd,hh*4").cutoff(2000).resonance("<0 10 20 30>")
|
||||
*
|
||||
*/
|
||||
['f', 'resonance', 'a pattern of numbers from 0 to 1. Specifies the resonance of the low-pass filter.'],
|
||||
// TODO: add lpq synonym?
|
||||
/**
|
||||
* Set detune of oscillators. Works only with some synths, see <a target="_blank" href="https://tidalcycles.org/docs/patternlib/tutorials/synthesizers">tidal doc</a>
|
||||
* DJ filter, below 0.5 is low pass filter, above is high pass filter.
|
||||
*
|
||||
* @name djf
|
||||
* @param {number | Pattern} cutoff below 0.5 is low pass filter, above is high pass filter
|
||||
@@ -368,7 +378,7 @@ const generic_params = [
|
||||
* @name fadeTime
|
||||
* @param {number | Pattern} time between 0 and 1
|
||||
* @example
|
||||
* s("ho*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
||||
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -493,7 +503,7 @@ const generic_params = [
|
||||
* @name pan
|
||||
* @param {number | Pattern} pan between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)
|
||||
* @example
|
||||
* s("[bd hh]*2").pan("<.5 1 .5 0>").osc()
|
||||
* s("[bd hh]*2").pan("<.5 1 .5 0>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -581,6 +591,11 @@ const generic_params = [
|
||||
'size',
|
||||
'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.',
|
||||
],
|
||||
[
|
||||
'f',
|
||||
'roomsize',
|
||||
'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.',
|
||||
],
|
||||
// ['f', 'sagogo', ''],
|
||||
// ['f', 'sclap', ''],
|
||||
// ['f', 'sclaves', ''],
|
||||
@@ -591,7 +606,7 @@ const generic_params = [
|
||||
* @name shape
|
||||
* @param {number | Pattern} distortion between 0 and 1
|
||||
* @example
|
||||
* s("bd sd").shape("<0 .2 .4 .6 .8 1>").osc()
|
||||
* s("bd sd,hh*4").shape("<0 .2 .4 .6 .8>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
@@ -648,16 +663,16 @@ const generic_params = [
|
||||
// ['f', 'tomdecay', ''],
|
||||
// ['f', 'vcfegint', ''],
|
||||
// ['f', 'vcoegint', ''],
|
||||
// TODO: Use a rest (~) to override the effect <- vowel
|
||||
/**
|
||||
*
|
||||
* Formant filter to make things sound like vowels.
|
||||
*
|
||||
* @name vowel
|
||||
* @param {string | Pattern} vowel You can use a e i o u. Use a rest (~) to override the effect
|
||||
* @param {string | Pattern} vowel You can use a e i o u.
|
||||
* @example
|
||||
* vowel("a e i [o u]").slow(2)
|
||||
* .n("<[0,7]!4 [2,7]!4>")
|
||||
* .s('supersquare').osc()
|
||||
* note("c2 <eb2 <g2 g1>>").s('sawtooth')
|
||||
* .vowel("<a e i <o u>>")
|
||||
*
|
||||
*/
|
||||
[
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
cyclist.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.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 createClock from './zyklus.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
export class Cyclist {
|
||||
worker;
|
||||
pattern;
|
||||
started = false;
|
||||
cps = 1; // TODO
|
||||
getTime;
|
||||
phase = 0;
|
||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||
this.getTime = getTime;
|
||||
this.onToggle = onToggle;
|
||||
this.latency = latency;
|
||||
const round = (x) => Math.round(x * 1000) / 1000;
|
||||
this.clock = createClock(
|
||||
getTime,
|
||||
(phase, duration, tick) => {
|
||||
if (tick === 0) {
|
||||
this.origin = phase;
|
||||
}
|
||||
const begin = round(phase - this.origin);
|
||||
this.phase = begin - latency;
|
||||
const end = round(begin + duration);
|
||||
const time = getTime();
|
||||
try {
|
||||
const haps = this.pattern.queryArc(begin, end); // get Haps
|
||||
haps.forEach((hap) => {
|
||||
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||
const deadline = hap.whole.begin + this.origin - time + latency;
|
||||
const duration = hap.duration * 1;
|
||||
onTrigger?.(hap, deadline, duration);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger(`[cyclist] error: ${e.message}`);
|
||||
onError?.(e);
|
||||
}
|
||||
}, // called slightly before each cycle
|
||||
interval, // duration of each cycle
|
||||
);
|
||||
}
|
||||
getPhase() {
|
||||
return this.getTime() - this.origin - this.latency;
|
||||
}
|
||||
setStarted(v) {
|
||||
this.started = v;
|
||||
this.onToggle?.(v);
|
||||
}
|
||||
start() {
|
||||
if (!this.pattern) {
|
||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||
}
|
||||
logger('[cyclist] start');
|
||||
this.clock.start();
|
||||
this.setStarted(true);
|
||||
}
|
||||
pause() {
|
||||
logger('[cyclist] pause');
|
||||
this.clock.pause();
|
||||
this.setStarted(false);
|
||||
}
|
||||
stop() {
|
||||
logger('[cyclist] stop');
|
||||
this.clock.stop();
|
||||
this.setStarted(false);
|
||||
}
|
||||
setPattern(pat, autostart = false) {
|
||||
this.pattern = pat;
|
||||
if (autostart && !this.started) {
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
setCps(cps = 1) {
|
||||
this.cps = cps;
|
||||
}
|
||||
log(begin, end, haps) {
|
||||
const onsets = haps.filter((h) => h.hasOnset());
|
||||
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,7 @@ 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 { Tone } from './tone.mjs';
|
||||
import { Pattern } from '@strudel.cycles/core';
|
||||
import { Pattern, getTime } from './index.mjs';
|
||||
|
||||
export const getDrawContext = (id = 'test-canvas') => {
|
||||
let canvas = document.querySelector('#' + id);
|
||||
@@ -28,7 +27,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
||||
let cycle,
|
||||
events = [];
|
||||
const animate = (time) => {
|
||||
const t = Tone.getTransport().seconds;
|
||||
const t = getTime();
|
||||
if (from !== undefined && to !== undefined) {
|
||||
const currentCycle = Math.floor(t);
|
||||
if (cycle !== currentCycle) {
|
||||
@@ -50,9 +49,9 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
||||
return this;
|
||||
};
|
||||
|
||||
export const cleanupDraw = () => {
|
||||
export const cleanupDraw = (clearScreen = true) => {
|
||||
const ctx = getDrawContext();
|
||||
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||
if (window.strudelAnimation) {
|
||||
cancelAnimationFrame(window.strudelAnimation);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import Fraction, { gcd } from './fraction.mjs';
|
||||
* @example
|
||||
* const line = drawLine("0 [1 2 3]", 10); // |0--123|0--123
|
||||
* console.log(line);
|
||||
* silence;
|
||||
*/
|
||||
function drawLine(pat, chars = 60) {
|
||||
let cycle = 0;
|
||||
|
||||
+33
-24
@@ -25,61 +25,70 @@ const euclid = (pulses, steps, rotation = 0) => {
|
||||
* describe a large number of rhythms used in the most important music world traditions.
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name euclid
|
||||
* @param {number} pulses the number of onsets / beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param {number} rotation (optional) offset in steps
|
||||
* @returns Pattern
|
||||
* @example // The Cuban tresillo pattern.
|
||||
* "c3".euclid(3,8)
|
||||
* @example
|
||||
* // The Cuban tresillo pattern.
|
||||
* note("c3").euclid(3,8)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
|
||||
* "c3".euclid(2,5)
|
||||
* note("c3").euclid(2,5)
|
||||
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
|
||||
* "c3".euclid(3,4)
|
||||
* note("c3").euclid(3,4)
|
||||
* @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm.
|
||||
* "c3".euclid(3,5,2)
|
||||
* note("c3").euclid(3,5,2)
|
||||
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
|
||||
* "c3".euclid(3,7)
|
||||
* note("c3").euclid(3,7)
|
||||
* @example // The Cuban tresillo pattern.
|
||||
* "c3".euclid(3,8)
|
||||
* note("c3").euclid(3,8)
|
||||
* @example // Another Ruchenitza Bulgarian folk-dance rhythm.
|
||||
* "c3".euclid(4,7)
|
||||
* note("c3").euclid(4,7)
|
||||
* @example // The Aksak rhythm of Turkey.
|
||||
* "c3".euclid(4,9)
|
||||
* note("c3").euclid(4,9)
|
||||
* @example // The metric pattern used by Frank Zappa in his piece titled Outside Now.
|
||||
* "c3".euclid(4,11)
|
||||
* note("c3").euclid(4,11)
|
||||
* @example // Yields the York-Samai pattern, a popular Arab rhythm.
|
||||
* "c3".euclid(5,6)
|
||||
* note("c3").euclid(5,6)
|
||||
* @example // The Nawakhat pattern, another popular Arab rhythm.
|
||||
* "c3".euclid(5,7)
|
||||
* note("c3").euclid(5,7)
|
||||
* @example // The Cuban cinquillo pattern.
|
||||
* "c3".euclid(5,8)
|
||||
* note("c3").euclid(5,8)
|
||||
* @example // A popular Arab rhythm called Agsag-Samai.
|
||||
* "c3".euclid(5,9)
|
||||
* note("c3").euclid(5,9)
|
||||
* @example // The metric pattern used by Moussorgsky in Pictures at an Exhibition.
|
||||
* "c3".euclid(5,11)
|
||||
* note("c3").euclid(5,11)
|
||||
* @example // The Venda clapping pattern of a South African children’s song.
|
||||
* "c3".euclid(5,12)
|
||||
* note("c3").euclid(5,12)
|
||||
* @example // The Bossa-Nova rhythm necklace of Brazil.
|
||||
* "c3".euclid(5,16)
|
||||
* note("c3").euclid(5,16)
|
||||
* @example // A typical rhythm played on the Bendir (frame drum).
|
||||
* "c3".euclid(7,8)
|
||||
* note("c3").euclid(7,8)
|
||||
* @example // A common West African bell pattern.
|
||||
* "c3".euclid(7,12)
|
||||
* note("c3").euclid(7,12)
|
||||
* @example // A Samba rhythm necklace from Brazil.
|
||||
* "c3".euclid(7,16,14)
|
||||
* note("c3").euclid(7,16,14)
|
||||
* @example // A rhythm necklace used in the Central African Republic.
|
||||
* "c3".euclid(9,16)
|
||||
* note("c3").euclid(9,16)
|
||||
* @example // A rhythm necklace of the Aka Pygmies of Central Africa.
|
||||
* "c3".euclid(11,24,14)
|
||||
* note("c3").euclid(11,24,14)
|
||||
* @example // Another rhythm necklace of the Aka Pygmies of the upper Sangha.
|
||||
* "c3".euclid(13,24,5)
|
||||
* note("c3").euclid(13,24,5)
|
||||
*/
|
||||
Pattern.prototype.euclid = function (pulses, steps, rotation = 0) {
|
||||
return this.struct(euclid(pulses, steps, rotation));
|
||||
};
|
||||
|
||||
/**
|
||||
* Similar to {@link Pattern#euclid}, but each pulse is held until the next pulse, so there will be no gaps.
|
||||
* Similar to `.euclid`, but each pulse is held until the next pulse, so there will be no gaps.
|
||||
* @name euclidLegato
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
|
||||
*/
|
||||
Pattern.prototype.euclidLegato = function (pulses, steps, rotation = 0) {
|
||||
const bin_pat = euclid(pulses, steps, rotation);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
evaluate.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.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 { isPattern, Pattern } from './index.mjs';
|
||||
|
||||
let scoped = false;
|
||||
export const evalScope = async (...args) => {
|
||||
if (scoped) {
|
||||
console.warn('evalScope was called more than once.');
|
||||
}
|
||||
scoped = true;
|
||||
const results = await Promise.allSettled(args);
|
||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||
results.forEach((result, i) => {
|
||||
if (result.status === 'rejected') {
|
||||
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
|
||||
}
|
||||
});
|
||||
Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap());
|
||||
};
|
||||
|
||||
function safeEval(str, options = {}) {
|
||||
const { wrapExpression = true, wrapAsync = true } = options;
|
||||
if (wrapExpression) {
|
||||
str = `{${str}}`;
|
||||
}
|
||||
if (wrapAsync) {
|
||||
str = `(async ()=>${str})()`;
|
||||
}
|
||||
const body = `"use strict";return (${str})`;
|
||||
return Function(body)();
|
||||
}
|
||||
|
||||
export const evaluate = async (code, transpiler) => {
|
||||
if (!scoped) {
|
||||
await evalScope(); // at least scope Pattern.prototype.boostrap
|
||||
}
|
||||
if (transpiler) {
|
||||
code = transpiler(code); // transform syntactically correct js code to semantically usable code
|
||||
}
|
||||
// if no transpiler is given, we expect a single instruction (!wrapExpression)
|
||||
const options = { wrapExpression: !!transpiler };
|
||||
let evaluated = await safeEval(code, options);
|
||||
if (!isPattern(evaluated)) {
|
||||
console.log('evaluated', evaluated);
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||
}
|
||||
return { mode: 'javascript', pattern: evaluated };
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
<div style="position: absolute; bottom: 0; right: 0; padding: 4px; width: 100vw; display: flex">
|
||||
<button id="start" style="font-size: 2em">start</button>
|
||||
<button id="stop" style="font-size: 2em">stop</button>
|
||||
<button id="slower" style="font-size: 2em">slower</button>
|
||||
<button id="faster" style="font-size: 2em">faster</button>
|
||||
</div>
|
||||
<textarea
|
||||
style="font-size: 2em; background: #052b49; color: #fff; height: 100%; width: 100%; outline: none; border: 0"
|
||||
id="text"
|
||||
spellcheck="false"
|
||||
>
|
||||
Loading...</textarea
|
||||
>
|
||||
<script type="module">
|
||||
document.body.style = 'margin: 0';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
const { cat, State, TimeSpan, Scheduler, getPlayableNoteValue, getFreq } = strudel;
|
||||
Object.assign(window, strudel); // add strudel to eval scope
|
||||
|
||||
const ctx = new AudioContext();
|
||||
const scheduler = new Scheduler({
|
||||
// audioContext: getAudioContext(),
|
||||
interval: 0.1,
|
||||
onTrigger: (hap, time, duration) => {
|
||||
const freq = getFrequency(hap);
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = 0.2;
|
||||
osc.frequency.value = freq;
|
||||
osc.type = 'triangle';
|
||||
const onset = ctx.currentTime + time;
|
||||
const offset = onset + duration;
|
||||
const attack = 0.05;
|
||||
const release = 0.05;
|
||||
osc.start(onset);
|
||||
osc.stop(offset + release);
|
||||
|
||||
const g = ctx.createGain();
|
||||
g.gain.setValueAtTime(gain, onset);
|
||||
g.gain.linearRampToValueAtTime(gain, onset + attack);
|
||||
g.gain.setValueAtTime(gain, offset - release);
|
||||
g.gain.linearRampToValueAtTime(0, offset);
|
||||
osc.connect(g);
|
||||
g.connect(ctx.destination);
|
||||
},
|
||||
});
|
||||
|
||||
let initialCode = `stack('c4','e4',cat('g4','a4','b4','a4'))
|
||||
.add(cat(0,1,2,3,4,3,2,1).slow(8))
|
||||
.fast(2)
|
||||
.cps(tri.range(1,8).slow(32))`;
|
||||
|
||||
try {
|
||||
const base64 = decodeURIComponent(window.location.href.split('#')[1]);
|
||||
initialCode = atob(base64);
|
||||
} catch (err) {
|
||||
console.warn('failed to decode', err);
|
||||
}
|
||||
const input = document.getElementById('text');
|
||||
input.value = initialCode;
|
||||
const evaluate = () => {
|
||||
try {
|
||||
const pattern = eval(input.value);
|
||||
scheduler.setPattern(pattern);
|
||||
window.location.hash = '#' + encodeURIComponent(btoa(input.value)); // update url hash
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
};
|
||||
evaluate();
|
||||
input.addEventListener('input', () => evaluate());
|
||||
document.getElementById('start').addEventListener('click', async () => {
|
||||
await ctx.resume();
|
||||
scheduler.start();
|
||||
});
|
||||
document.getElementById('stop').addEventListener('click', () => scheduler.stop());
|
||||
document.getElementById('slower').addEventListener('click', () => scheduler.setCps(scheduler.cps - 0.1));
|
||||
document.getElementById('faster').addEventListener('click', () => scheduler.setCps(scheduler.cps + 0.1));
|
||||
</script>
|
||||
<!--
|
||||
sequence(1,2).mul(55/2) // frequencies
|
||||
.mul(slowcat(1,2))
|
||||
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
|
||||
.fast(3)
|
||||
.freq()
|
||||
.velocity(.5)
|
||||
.s('sawtooth')
|
||||
.cutoff(800)
|
||||
.out()
|
||||
-->
|
||||
@@ -0,0 +1,44 @@
|
||||
<input
|
||||
type="text"
|
||||
id="text"
|
||||
value="seq('c3','eb3','g3').note().s('sawtooth').out()"
|
||||
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<button id="start">play</button>
|
||||
<div id="output"></div>
|
||||
<script type="module">
|
||||
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@latest');
|
||||
|
||||
const controls = await import('https://cdn.skypack.dev/@strudel.cycles/core@latest/controls.mjs');
|
||||
const { getAudioContext, Scheduler } = await import('https://cdn.skypack.dev/@strudel.cycles/webaudio@latest');
|
||||
let scheduler;
|
||||
|
||||
const audioContext = getAudioContext();
|
||||
const latency = 0.2;
|
||||
|
||||
Object.assign(window, strudel);
|
||||
Object.assign(window, controls.default);
|
||||
scheduler = new Scheduler({
|
||||
audioContext,
|
||||
interval: 0.1,
|
||||
latency,
|
||||
onEvent: (hap) => {
|
||||
if (!hap.context.onTrigger) {
|
||||
console.warn('no output chosen. use one of .out() .webdirt() .osc()');
|
||||
}
|
||||
},
|
||||
});
|
||||
let started;
|
||||
document.getElementById('start').addEventListener('click', async () => {
|
||||
const code = document.getElementById('text').value;
|
||||
const pattern = eval(code);
|
||||
const events = pattern._firstCycleValues;
|
||||
console.log(code, '->', events);
|
||||
scheduler.setPattern(pattern);
|
||||
if (!started) {
|
||||
scheduler.start();
|
||||
started = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Buildless Vanilla Strudel REPL</title>
|
||||
</head>
|
||||
<body style="margin: 0; background: #222">
|
||||
<div style="display: grid; height: 100vh">
|
||||
<textarea
|
||||
id="text"
|
||||
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
id="start"
|
||||
style="
|
||||
position: absolute;
|
||||
border-radius: 10px;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 20px;
|
||||
border: 2px solid white;
|
||||
background: transparent;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
evaluate
|
||||
</button>
|
||||
<div id="output"></div>
|
||||
<script type="module">
|
||||
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.3.2';
|
||||
import { mini } from 'https://cdn.skypack.dev/@strudel.cycles/mini@0.3.2';
|
||||
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.3.2';
|
||||
import { getAudioContext, webaudioOutput } from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.3.3';
|
||||
|
||||
const ctx = getAudioContext();
|
||||
const input = document.getElementById('text');
|
||||
input.innerHTML = getTune();
|
||||
|
||||
evalScope(
|
||||
controls,
|
||||
import('https://cdn.skypack.dev/@strudel.cycles/core@0.3.2'),
|
||||
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.3.2'),
|
||||
import('https://cdn.skypack.dev/@strudel.cycles/tonal@0.3.3'),
|
||||
import('https://cdn.skypack.dev/@strudel.cycles/webaudio@0.3.3'),
|
||||
);
|
||||
|
||||
const { evaluate } = repl({
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime: () => ctx.currentTime,
|
||||
transpiler,
|
||||
});
|
||||
document.getElementById('start').addEventListener('click', () => {
|
||||
ctx.resume();
|
||||
evaluate(input.value);
|
||||
});
|
||||
|
||||
function getTune() {
|
||||
return `await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
|
||||
stack(
|
||||
// amen
|
||||
n("0 1 2 3 4 5 6 7")
|
||||
.sometimes(x=>x.ply(2))
|
||||
.rarely(x=>x.speed("2 | -2"))
|
||||
.sometimesBy(.4, x=>x.delay(".5"))
|
||||
.s("amencutup")
|
||||
.slow(2)
|
||||
.room(.5)
|
||||
,
|
||||
// bass
|
||||
sine.add(saw.slow(4)).range(0,7).segment(8)
|
||||
.superimpose(x=>x.add(.1))
|
||||
.scale('G0 minor').note()
|
||||
.s("sawtooth").decay(.1).sustain(0)
|
||||
.gain(.4).cutoff(perlin.range(300,3000).slow(8)).resonance(10)
|
||||
.degradeBy("0 0.1 .5 .1")
|
||||
.rarely(add(note("12")))
|
||||
,
|
||||
// chord
|
||||
note("Bb3,D4".superimpose(x=>x.add(.2)))
|
||||
.s('sawtooth').cutoff(1000).struct("<~@3 [~ x]>")
|
||||
.decay(.05).sustain(.0).delay(.8).delaytime(.125).room(.8)
|
||||
,
|
||||
// alien
|
||||
s("breath").room(1).shape(.6).chop(16).rev().mask("<x ~@7>")
|
||||
,
|
||||
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
|
||||
).reset("<x@7 x(5,8)>")`;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
!dist
|
||||
@@ -0,0 +1,10 @@
|
||||
# vite-vanilla-repl
|
||||
|
||||
This folder demonstrates how to set up a strudel repl using vite and vanilla JS. Run it using:
|
||||
|
||||
```sh
|
||||
npm i
|
||||
npm run dev
|
||||
```
|
||||
|
||||
or view it [live on githack](https://rawcdn.githack.com/tidalcycles/strudel/5fb36acb046ead7cd6ad3cd10f532e7f585f536a/packages/core/examples/vite-vanilla-repl/dist/index.html)
|
||||
@@ -0,0 +1 @@
|
||||
import{b as s,h as i,m,a as n,c as p,p as t}from"./index.4cbc0a10.js";export{s as SyntaxError,i as h,m as mini,n as minify,p as parse,t as patternifyAST};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{g as s,f as d,i as t,n as r,l as u,j as o,d as f,k as p,r as g,s as i,w as l,e as m}from"./index.4cbc0a10.js";export{s as getAudioContext,d as getCachedBuffer,t as getLoadedBuffer,r as getLoadedSamples,u as loadBuffer,o as loadGithubSamples,f as panic,p as resetLoadedSamples,g as reverseBuffer,i as samples,l as webaudioOutput,m as webaudioOutputTrigger};
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite Vanilla Strudel REPL</title>
|
||||
<script type="module" crossorigin src="/tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/assets/index.4cbc0a10.js"></script>
|
||||
</head>
|
||||
<body style="margin: 0; background: #222">
|
||||
<div style="display: grid; height: 100vh">
|
||||
<textarea
|
||||
id="text"
|
||||
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
id="start"
|
||||
style="
|
||||
position: absolute;
|
||||
border-radius: 10px;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 20px;
|
||||
border: 2px solid white;
|
||||
background: transparent;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
evaluate
|
||||
</button>
|
||||
<div id="output"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite Vanilla Strudel REPL</title>
|
||||
</head>
|
||||
<body style="margin: 0; background: #222">
|
||||
<div style="display: grid; height: 100vh">
|
||||
<textarea
|
||||
id="text"
|
||||
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
id="start"
|
||||
style="
|
||||
position: absolute;
|
||||
border-radius: 10px;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 20px;
|
||||
border: 2px solid white;
|
||||
background: transparent;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
evaluate
|
||||
</button>
|
||||
<div id="output"></div>
|
||||
<script type="module" src="./main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { controls, repl, evalScope, setStringParser } from '@strudel.cycles/core';
|
||||
import { mini } from '@strudel.cycles/mini';
|
||||
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
// import { transpiler } from '@strudel.cycles/transpiler';
|
||||
import tune from './tune.mjs';
|
||||
|
||||
const ctx = getAudioContext();
|
||||
const input = document.getElementById('text');
|
||||
input.innerHTML = tune;
|
||||
|
||||
evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
);
|
||||
|
||||
setStringParser(mini)
|
||||
|
||||
const { evaluate } = repl({
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime: () => ctx.currentTime,
|
||||
// transpiler,
|
||||
});
|
||||
document.getElementById('start').addEventListener('click', () => {
|
||||
ctx.resume();
|
||||
evaluate(input.value);
|
||||
});
|
||||
@@ -0,0 +1,885 @@
|
||||
{
|
||||
"name": "vite-vanilla-repl",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vite-vanilla-repl",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"vite": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.13.tgz",
|
||||
"integrity": "sha512-RY2fVI8O0iFUNvZirXaQ1vMvK0xhCcl0gqRj74Z6yEiO1zAUa7hbsdwZM1kzqbxHK7LFyMizipfXT3JME+12Hw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.13.tgz",
|
||||
"integrity": "sha512-+BoyIm4I8uJmH/QDIH0fu7MG0AEx9OXEDXnqptXCwKOlOqZiS4iraH1Nr7/ObLMokW3sOCeBNyD68ATcV9b9Ag==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.13.tgz",
|
||||
"integrity": "sha512-Cu3SC84oyzzhrK/YyN4iEVy2jZu5t2fz66HEOShHURcjSkOSAVL8C/gfUT+lDJxkVHpg8GZ10DD0rMHRPqMFaQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.15.13",
|
||||
"@esbuild/linux-loong64": "0.15.13",
|
||||
"esbuild-android-64": "0.15.13",
|
||||
"esbuild-android-arm64": "0.15.13",
|
||||
"esbuild-darwin-64": "0.15.13",
|
||||
"esbuild-darwin-arm64": "0.15.13",
|
||||
"esbuild-freebsd-64": "0.15.13",
|
||||
"esbuild-freebsd-arm64": "0.15.13",
|
||||
"esbuild-linux-32": "0.15.13",
|
||||
"esbuild-linux-64": "0.15.13",
|
||||
"esbuild-linux-arm": "0.15.13",
|
||||
"esbuild-linux-arm64": "0.15.13",
|
||||
"esbuild-linux-mips64le": "0.15.13",
|
||||
"esbuild-linux-ppc64le": "0.15.13",
|
||||
"esbuild-linux-riscv64": "0.15.13",
|
||||
"esbuild-linux-s390x": "0.15.13",
|
||||
"esbuild-netbsd-64": "0.15.13",
|
||||
"esbuild-openbsd-64": "0.15.13",
|
||||
"esbuild-sunos-64": "0.15.13",
|
||||
"esbuild-windows-32": "0.15.13",
|
||||
"esbuild-windows-64": "0.15.13",
|
||||
"esbuild-windows-arm64": "0.15.13"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-android-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.13.tgz",
|
||||
"integrity": "sha512-yRorukXBlokwTip+Sy4MYskLhJsO0Kn0/Fj43s1krVblfwP+hMD37a4Wmg139GEsMLl+vh8WXp2mq/cTA9J97g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-android-arm64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.13.tgz",
|
||||
"integrity": "sha512-TKzyymLD6PiVeyYa4c5wdPw87BeAiTXNtK6amWUcXZxkV51gOk5u5qzmDaYSwiWeecSNHamFsaFjLoi32QR5/w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-darwin-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.13.tgz",
|
||||
"integrity": "sha512-WAx7c2DaOS6CrRcoYCgXgkXDliLnFv3pQLV6GeW1YcGEZq2Gnl8s9Pg7ahValZkpOa0iE/ojRVQ87sbUhF1Cbg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-darwin-arm64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.13.tgz",
|
||||
"integrity": "sha512-U6jFsPfSSxC3V1CLiQqwvDuj3GGrtQNB3P3nNC3+q99EKf94UGpsG9l4CQ83zBs1NHrk1rtCSYT0+KfK5LsD8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-freebsd-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.13.tgz",
|
||||
"integrity": "sha512-whItJgDiOXaDG/idy75qqevIpZjnReZkMGCgQaBWZuKHoElDJC1rh7MpoUgupMcdfOd+PgdEwNQW9DAE6i8wyA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-freebsd-arm64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.13.tgz",
|
||||
"integrity": "sha512-6pCSWt8mLUbPtygv7cufV0sZLeylaMwS5Fznj6Rsx9G2AJJsAjQ9ifA+0rQEIg7DwJmi9it+WjzNTEAzzdoM3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-32": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.13.tgz",
|
||||
"integrity": "sha512-VbZdWOEdrJiYApm2kkxoTOgsoCO1krBZ3quHdYk3g3ivWaMwNIVPIfEE0f0XQQ0u5pJtBsnk2/7OPiCFIPOe/w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.13.tgz",
|
||||
"integrity": "sha512-rXmnArVNio6yANSqDQlIO4WiP+Cv7+9EuAHNnag7rByAqFVuRusLbGi2697A5dFPNXoO//IiogVwi3AdcfPC6A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.13.tgz",
|
||||
"integrity": "sha512-Ac6LpfmJO8WhCMQmO253xX2IU2B3wPDbl4IvR0hnqcPrdfCaUa2j/lLMGTjmQ4W5JsJIdHEdW12dG8lFS0MbxQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.13.tgz",
|
||||
"integrity": "sha512-alEMGU4Z+d17U7KQQw2IV8tQycO6T+rOrgW8OS22Ua25x6kHxoG6Ngry6Aq6uranC+pNWNMB6aHFPh7aTQdORQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-mips64le": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.13.tgz",
|
||||
"integrity": "sha512-47PgmyYEu+yN5rD/MbwS6DxP2FSGPo4Uxg5LwIdxTiyGC2XKwHhHyW7YYEDlSuXLQXEdTO7mYe8zQ74czP7W8A==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-ppc64le": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.13.tgz",
|
||||
"integrity": "sha512-z6n28h2+PC1Ayle9DjKoBRcx/4cxHoOa2e689e2aDJSaKug3jXcQw7mM+GLg+9ydYoNzj8QxNL8ihOv/OnezhA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-riscv64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.13.tgz",
|
||||
"integrity": "sha512-+Lu4zuuXuQhgLUGyZloWCqTslcCAjMZH1k3Xc9MSEJEpEFdpsSU0sRDXAnk18FKOfEjhu4YMGaykx9xjtpA6ow==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-s390x": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.13.tgz",
|
||||
"integrity": "sha512-BMeXRljruf7J0TMxD5CIXS65y7puiZkAh+s4XFV9qy16SxOuMhxhVIXYLnbdfLrsYGFzx7U9mcdpFWkkvy/Uag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-netbsd-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.13.tgz",
|
||||
"integrity": "sha512-EHj9QZOTel581JPj7UO3xYbltFTYnHy+SIqJVq6yd3KkCrsHRbapiPb0Lx3EOOtybBEE9EyqbmfW1NlSDsSzvQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-openbsd-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.13.tgz",
|
||||
"integrity": "sha512-nkuDlIjF/sfUhfx8SKq0+U+Fgx5K9JcPq1mUodnxI0x4kBdCv46rOGWbuJ6eof2n3wdoCLccOoJAbg9ba/bT2w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-sunos-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.13.tgz",
|
||||
"integrity": "sha512-jVeu2GfxZQ++6lRdY43CS0Tm/r4WuQQ0Pdsrxbw+aOrHQPHV0+LNOLnvbN28M7BSUGnJnHkHm2HozGgNGyeIRw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-32": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.13.tgz",
|
||||
"integrity": "sha512-XoF2iBf0wnqo16SDq+aDGi/+QbaLFpkiRarPVssMh9KYbFNCqPLlGAWwDvxEVz+ywX6Si37J2AKm+AXq1kC0JA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.13.tgz",
|
||||
"integrity": "sha512-Et6htEfGycjDrtqb2ng6nT+baesZPYQIW+HUEHK4D1ncggNrDNk3yoboYQ5KtiVrw/JaDMNttz8rrPubV/fvPQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-arm64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.13.tgz",
|
||||
"integrity": "sha512-3bv7tqntThQC9SWLRouMDmZnlOukBhOCTlkzNqzGCmrkCJI7io5LLjwJBOVY6kOUlIvdxbooNZwjtBvj+7uuVg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/has": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
|
||||
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has": "^1.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.18",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
|
||||
"integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.4",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.9.0",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "2.79.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
|
||||
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz",
|
||||
"integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.15.9",
|
||||
"postcss": "^8.4.18",
|
||||
"resolve": "^1.22.1",
|
||||
"rollup": "^2.79.1"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"less": "*",
|
||||
"sass": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@esbuild/android-arm": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.13.tgz",
|
||||
"integrity": "sha512-RY2fVI8O0iFUNvZirXaQ1vMvK0xhCcl0gqRj74Z6yEiO1zAUa7hbsdwZM1kzqbxHK7LFyMizipfXT3JME+12Hw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-loong64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.13.tgz",
|
||||
"integrity": "sha512-+BoyIm4I8uJmH/QDIH0fu7MG0AEx9OXEDXnqptXCwKOlOqZiS4iraH1Nr7/ObLMokW3sOCeBNyD68ATcV9b9Ag==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.13.tgz",
|
||||
"integrity": "sha512-Cu3SC84oyzzhrK/YyN4iEVy2jZu5t2fz66HEOShHURcjSkOSAVL8C/gfUT+lDJxkVHpg8GZ10DD0rMHRPqMFaQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@esbuild/android-arm": "0.15.13",
|
||||
"@esbuild/linux-loong64": "0.15.13",
|
||||
"esbuild-android-64": "0.15.13",
|
||||
"esbuild-android-arm64": "0.15.13",
|
||||
"esbuild-darwin-64": "0.15.13",
|
||||
"esbuild-darwin-arm64": "0.15.13",
|
||||
"esbuild-freebsd-64": "0.15.13",
|
||||
"esbuild-freebsd-arm64": "0.15.13",
|
||||
"esbuild-linux-32": "0.15.13",
|
||||
"esbuild-linux-64": "0.15.13",
|
||||
"esbuild-linux-arm": "0.15.13",
|
||||
"esbuild-linux-arm64": "0.15.13",
|
||||
"esbuild-linux-mips64le": "0.15.13",
|
||||
"esbuild-linux-ppc64le": "0.15.13",
|
||||
"esbuild-linux-riscv64": "0.15.13",
|
||||
"esbuild-linux-s390x": "0.15.13",
|
||||
"esbuild-netbsd-64": "0.15.13",
|
||||
"esbuild-openbsd-64": "0.15.13",
|
||||
"esbuild-sunos-64": "0.15.13",
|
||||
"esbuild-windows-32": "0.15.13",
|
||||
"esbuild-windows-64": "0.15.13",
|
||||
"esbuild-windows-arm64": "0.15.13"
|
||||
}
|
||||
},
|
||||
"esbuild-android-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.13.tgz",
|
||||
"integrity": "sha512-yRorukXBlokwTip+Sy4MYskLhJsO0Kn0/Fj43s1krVblfwP+hMD37a4Wmg139GEsMLl+vh8WXp2mq/cTA9J97g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-android-arm64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.13.tgz",
|
||||
"integrity": "sha512-TKzyymLD6PiVeyYa4c5wdPw87BeAiTXNtK6amWUcXZxkV51gOk5u5qzmDaYSwiWeecSNHamFsaFjLoi32QR5/w==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-darwin-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.13.tgz",
|
||||
"integrity": "sha512-WAx7c2DaOS6CrRcoYCgXgkXDliLnFv3pQLV6GeW1YcGEZq2Gnl8s9Pg7ahValZkpOa0iE/ojRVQ87sbUhF1Cbg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-darwin-arm64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.13.tgz",
|
||||
"integrity": "sha512-U6jFsPfSSxC3V1CLiQqwvDuj3GGrtQNB3P3nNC3+q99EKf94UGpsG9l4CQ83zBs1NHrk1rtCSYT0+KfK5LsD8A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-freebsd-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.13.tgz",
|
||||
"integrity": "sha512-whItJgDiOXaDG/idy75qqevIpZjnReZkMGCgQaBWZuKHoElDJC1rh7MpoUgupMcdfOd+PgdEwNQW9DAE6i8wyA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-freebsd-arm64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.13.tgz",
|
||||
"integrity": "sha512-6pCSWt8mLUbPtygv7cufV0sZLeylaMwS5Fznj6Rsx9G2AJJsAjQ9ifA+0rQEIg7DwJmi9it+WjzNTEAzzdoM3Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-32": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.13.tgz",
|
||||
"integrity": "sha512-VbZdWOEdrJiYApm2kkxoTOgsoCO1krBZ3quHdYk3g3ivWaMwNIVPIfEE0f0XQQ0u5pJtBsnk2/7OPiCFIPOe/w==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.13.tgz",
|
||||
"integrity": "sha512-rXmnArVNio6yANSqDQlIO4WiP+Cv7+9EuAHNnag7rByAqFVuRusLbGi2697A5dFPNXoO//IiogVwi3AdcfPC6A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-arm": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.13.tgz",
|
||||
"integrity": "sha512-Ac6LpfmJO8WhCMQmO253xX2IU2B3wPDbl4IvR0hnqcPrdfCaUa2j/lLMGTjmQ4W5JsJIdHEdW12dG8lFS0MbxQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-arm64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.13.tgz",
|
||||
"integrity": "sha512-alEMGU4Z+d17U7KQQw2IV8tQycO6T+rOrgW8OS22Ua25x6kHxoG6Ngry6Aq6uranC+pNWNMB6aHFPh7aTQdORQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-mips64le": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.13.tgz",
|
||||
"integrity": "sha512-47PgmyYEu+yN5rD/MbwS6DxP2FSGPo4Uxg5LwIdxTiyGC2XKwHhHyW7YYEDlSuXLQXEdTO7mYe8zQ74czP7W8A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-ppc64le": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.13.tgz",
|
||||
"integrity": "sha512-z6n28h2+PC1Ayle9DjKoBRcx/4cxHoOa2e689e2aDJSaKug3jXcQw7mM+GLg+9ydYoNzj8QxNL8ihOv/OnezhA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-riscv64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.13.tgz",
|
||||
"integrity": "sha512-+Lu4zuuXuQhgLUGyZloWCqTslcCAjMZH1k3Xc9MSEJEpEFdpsSU0sRDXAnk18FKOfEjhu4YMGaykx9xjtpA6ow==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-linux-s390x": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.13.tgz",
|
||||
"integrity": "sha512-BMeXRljruf7J0TMxD5CIXS65y7puiZkAh+s4XFV9qy16SxOuMhxhVIXYLnbdfLrsYGFzx7U9mcdpFWkkvy/Uag==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-netbsd-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.13.tgz",
|
||||
"integrity": "sha512-EHj9QZOTel581JPj7UO3xYbltFTYnHy+SIqJVq6yd3KkCrsHRbapiPb0Lx3EOOtybBEE9EyqbmfW1NlSDsSzvQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-openbsd-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.13.tgz",
|
||||
"integrity": "sha512-nkuDlIjF/sfUhfx8SKq0+U+Fgx5K9JcPq1mUodnxI0x4kBdCv46rOGWbuJ6eof2n3wdoCLccOoJAbg9ba/bT2w==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-sunos-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.13.tgz",
|
||||
"integrity": "sha512-jVeu2GfxZQ++6lRdY43CS0Tm/r4WuQQ0Pdsrxbw+aOrHQPHV0+LNOLnvbN28M7BSUGnJnHkHm2HozGgNGyeIRw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-32": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.13.tgz",
|
||||
"integrity": "sha512-XoF2iBf0wnqo16SDq+aDGi/+QbaLFpkiRarPVssMh9KYbFNCqPLlGAWwDvxEVz+ywX6Si37J2AKm+AXq1kC0JA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.13.tgz",
|
||||
"integrity": "sha512-Et6htEfGycjDrtqb2ng6nT+baesZPYQIW+HUEHK4D1ncggNrDNk3yoboYQ5KtiVrw/JaDMNttz8rrPubV/fvPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"esbuild-windows-arm64": {
|
||||
"version": "0.15.13",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.13.tgz",
|
||||
"integrity": "sha512-3bv7tqntThQC9SWLRouMDmZnlOukBhOCTlkzNqzGCmrkCJI7io5LLjwJBOVY6kOUlIvdxbooNZwjtBvj+7uuVg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
},
|
||||
"has": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"is-core-module": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
|
||||
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
||||
"dev": true
|
||||
},
|
||||
"path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.4.18",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
|
||||
"integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"nanoid": "^3.3.4",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-core-module": "^2.9.0",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"rollup": {
|
||||
"version": "2.79.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
|
||||
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true
|
||||
},
|
||||
"vite": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz",
|
||||
"integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esbuild": "^0.15.9",
|
||||
"fsevents": "~2.3.2",
|
||||
"postcss": "^8.4.18",
|
||||
"resolve": "^1.22.1",
|
||||
"rollup": "^2.79.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "vite-vanilla-repl",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build-githack": "vite build --base /tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^3.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/* export default `await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
|
||||
stack(
|
||||
// amen
|
||||
n("0 1 2 3 4 5 6 7")
|
||||
.sometimes(x=>x.ply(2))
|
||||
.rarely(x=>x.speed("2 | -2"))
|
||||
.sometimesBy(.4, x=>x.delay(".5"))
|
||||
.s("amencutup")
|
||||
.slow(2)
|
||||
.room(.5)
|
||||
,
|
||||
// bass
|
||||
sine.add(saw.slow(4)).range(0,7).segment(8)
|
||||
.superimpose(x=>x.add(.1))
|
||||
.scale('G0 minor').note()
|
||||
.s("sawtooth").decay(.1).sustain(0)
|
||||
.gain(.4).cutoff(perlin.range(300,3000).slow(8)).resonance(10)
|
||||
.degradeBy("0 0.1 .5 .1")
|
||||
.rarely(add(note("12")))
|
||||
,
|
||||
// chord
|
||||
note("Bb3,D4".superimpose(x=>x.add(.2)))
|
||||
.s('sawtooth').cutoff(1000).struct("<~@3 [~ x]>")
|
||||
.decay(.05).sustain(.0).delay(.8).delaytime(.125).room(.8)
|
||||
,
|
||||
// alien
|
||||
s("breath").room(1).shape(.6).chop(16).rev().mask("<x ~@7>")
|
||||
,
|
||||
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
|
||||
).reset("<x@7 x(5,8)>")`;
|
||||
*/
|
||||
|
||||
export default `stack(
|
||||
n("c3 [eb3,g3]")
|
||||
.delay("<0 .5>")
|
||||
.delaytime(".16 | .33")
|
||||
.delayfeedback(".6 | .8")
|
||||
).sometimes(x=>x.speed("-1"))`;
|
||||
@@ -55,7 +55,8 @@ Fraction.prototype.min = function (other) {
|
||||
return this.lt(other) ? this : other;
|
||||
};
|
||||
|
||||
Fraction.prototype.show = function () {
|
||||
Fraction.prototype.show = function (/* excludeWhole = false */) {
|
||||
// return this.toFraction(excludeWhole);
|
||||
return this.s * this.n + '/' + this.d;
|
||||
};
|
||||
|
||||
|
||||
@@ -85,9 +85,13 @@ export class Hap {
|
||||
);
|
||||
}
|
||||
|
||||
showWhole() {
|
||||
showWhole(compact = false) {
|
||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
|
||||
typeof this.value === 'object' ? JSON.stringify(this.value) : this.value
|
||||
typeof this.value === 'object'
|
||||
? compact
|
||||
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||
: JSON.stringify(this.value)
|
||||
: this.value
|
||||
}`;
|
||||
}
|
||||
|
||||
|
||||
+12
-6
@@ -4,10 +4,11 @@ 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/>.
|
||||
*/
|
||||
|
||||
export * from './controls.mjs';
|
||||
import controls from './controls.mjs';
|
||||
export * from './euclid.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
export { Fraction };
|
||||
import { logger } from './logger.mjs';
|
||||
export { Fraction, controls };
|
||||
export * from './hap.mjs';
|
||||
export * from './pattern.mjs';
|
||||
export * from './signal.mjs';
|
||||
@@ -15,14 +16,19 @@ export * from './state.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './util.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * from './evaluate.mjs';
|
||||
export * from './repl.mjs';
|
||||
export * from './logger.mjs';
|
||||
export * from './time.mjs';
|
||||
export * from './draw.mjs';
|
||||
export * from './pianoroll.mjs';
|
||||
export * from './ui.mjs';
|
||||
export { default as drawLine } from './drawLine.mjs';
|
||||
export { default as gist } from './gist.js';
|
||||
// below won't work with runtime.mjs (json import fails)
|
||||
/* import * as p from './package.json';
|
||||
export const version = p.version; */
|
||||
console.log(
|
||||
'%c // 🌀 @strudel.cycles/core loaded 🌀', // keep "//" for runnable snapshot source..
|
||||
'background-color: black;color:white;padding:4px;border-radius:15px',
|
||||
);
|
||||
logger('🌀 @strudel.cycles/core loaded 🌀');
|
||||
if (globalThis._strudelLoaded) {
|
||||
console.warn(
|
||||
`@strudel.cycles/core was loaded more than once...
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export const logKey = 'strudel.log';
|
||||
|
||||
export function logger(message, type, data = {}) {
|
||||
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
||||
if (typeof CustomEvent !== 'undefined') {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(logKey, {
|
||||
detail: {
|
||||
message,
|
||||
type,
|
||||
data,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.key = logKey;
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.2.0",
|
||||
"version": "0.4.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.2.0",
|
||||
"version": "0.4.0",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
+453
-110
@@ -10,8 +10,15 @@ import Hap from './hap.mjs';
|
||||
import State from './state.mjs';
|
||||
import { unionWithObj } from './value.mjs';
|
||||
|
||||
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry, mod } from './util.mjs';
|
||||
import { compose, removeUndefineds, flatten, id, listRange, curry, mod, numeralArgs, parseNumeral } from './util.mjs';
|
||||
import drawLine from './drawLine.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
let stringParser;
|
||||
// parser is expected to turn a string into a pattern
|
||||
// if set, the reify function will parse all strings with it
|
||||
// intended to use with mini to automatically interpret all strings as mini notation
|
||||
export const setStringParser = (parser) => (stringParser = parser);
|
||||
|
||||
/** @class Class representing a pattern. */
|
||||
export class Pattern {
|
||||
@@ -32,8 +39,10 @@ export class Pattern {
|
||||
* @param {Fraction | number} end to time
|
||||
* @returns Hap[]
|
||||
* @example
|
||||
* const pattern = sequence('a', ['b', 'c']);
|
||||
* const haps = pattern.queryArc(0, 1);
|
||||
* const pattern = sequence('a', ['b', 'c'])
|
||||
* const haps = pattern.queryArc(0, 1)
|
||||
* console.log(haps)
|
||||
* silence
|
||||
*/
|
||||
queryArc(begin, end) {
|
||||
return this.query(new State(new TimeSpan(begin, end)));
|
||||
@@ -63,6 +72,17 @@ export class Pattern {
|
||||
return new Pattern((state) => this.query(state.withSpan(func)));
|
||||
}
|
||||
|
||||
withQuerySpanMaybe(func) {
|
||||
const pat = this;
|
||||
return new Pattern((state) => {
|
||||
const newState = state.withSpan(func);
|
||||
if (!newState.span) {
|
||||
return [];
|
||||
}
|
||||
return pat.query(newState);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* As with {@link Pattern#withQuerySpan}, but the function is applied to both the
|
||||
* begin and end time of the query timespan.
|
||||
@@ -435,40 +455,18 @@ export class Pattern {
|
||||
return otherPat.fmap((b) => this.fmap((a) => func(a)(b)))._TrigzeroJoin();
|
||||
}
|
||||
|
||||
_asNumber(dropfails = false, softfail = false) {
|
||||
return this._withHap((hap) => {
|
||||
const asNumber = Number(hap.value);
|
||||
if (!isNaN(asNumber)) {
|
||||
return hap.withValue(() => asNumber);
|
||||
}
|
||||
const specialValue = {
|
||||
e: Math.E,
|
||||
pi: Math.PI,
|
||||
}[hap.value];
|
||||
if (typeof specialValue !== 'undefined') {
|
||||
return hap.withValue(() => specialValue);
|
||||
}
|
||||
if (isNote(hap.value)) {
|
||||
// set context type to midi to let the player know its meant as midi number and not as frequency
|
||||
return new Hap(hap.whole, hap.part, toMidi(hap.value), { ...hap.context, type: 'midi' });
|
||||
}
|
||||
if (dropfails) {
|
||||
// return 'nothing'
|
||||
return undefined;
|
||||
}
|
||||
if (softfail) {
|
||||
// return original hap
|
||||
return hap;
|
||||
}
|
||||
throw new Error('cannot parse as number: "' + hap.value + '"');
|
||||
return hap;
|
||||
});
|
||||
_asNumber() {
|
||||
return this.fmap(parseNumeral);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes a numerical pattern. Returns a new pattern with all values rounded
|
||||
* to the nearest integer.
|
||||
* @name round
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "0.5 1.5 2.5".round().scale('C major').note()
|
||||
*/
|
||||
round() {
|
||||
return this._asNumber().fmap((v) => Math.round(v));
|
||||
@@ -513,13 +511,16 @@ export class Pattern {
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes a numerical pattern, containing unipolar values in the range 0 ..
|
||||
* 1. Returns a new pattern with values scaled to the given min/max range.
|
||||
* @param {Number} min
|
||||
* @param {Number} max
|
||||
* Assumes a numerical pattern, containing unipolar values in the range 0 .. 1.
|
||||
* Returns a new pattern with values scaled to the given min/max range.
|
||||
* Most useful in combination with continuous patterns.
|
||||
* @name range
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("bd sd,hh*4").cutoff(sine.range(500,2000).slow(4))
|
||||
*/
|
||||
range(min, max) {
|
||||
_range(min, max) {
|
||||
return this.mul(max - min).add(min);
|
||||
}
|
||||
|
||||
@@ -531,8 +532,8 @@ export class Pattern {
|
||||
* @param {Number} max
|
||||
* @returns Pattern
|
||||
*/
|
||||
rangex(min, max) {
|
||||
return this.range(Math.log(min), Math.log(max)).fmap(Math.exp);
|
||||
_rangex(min, max) {
|
||||
return this._range(Math.log(min), Math.log(max)).fmap(Math.exp);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -542,8 +543,8 @@ export class Pattern {
|
||||
* @param {Number} max
|
||||
* @returns Pattern
|
||||
*/
|
||||
range2(min, max) {
|
||||
return this._fromBipolar().range(min, max);
|
||||
_range2(min, max) {
|
||||
return this._fromBipolar()._range(min, max);
|
||||
}
|
||||
|
||||
_bindWhole(choose_whole, func) {
|
||||
@@ -642,13 +643,26 @@ export class Pattern {
|
||||
return this._trigJoin(true);
|
||||
}
|
||||
|
||||
// Like the other joins above, joins a pattern of patterns of values, into a flatter
|
||||
// pattern of values. In this case it takes whole cycles of the inner pattern to fit each event
|
||||
// in the outer pattern.
|
||||
_squeezeJoin() {
|
||||
// A pattern of patterns, which we call the 'outer' pattern, with patterns
|
||||
// as values which we call the 'inner' patterns.
|
||||
const pat_of_pats = this;
|
||||
function query(state) {
|
||||
// Get the events with the inner patterns. Ignore continuous events (without 'wholes')
|
||||
const haps = pat_of_pats.discreteOnly().query(state);
|
||||
// A function to map over the events from the outer pattern.
|
||||
function flatHap(outerHap) {
|
||||
const pat = outerHap.value._compressSpan(outerHap.wholeOrPart().cycleArc());
|
||||
const innerHaps = pat.query(state.setSpan(outerHap.part));
|
||||
// Get the inner pattern, slowed and shifted so that the 'whole'
|
||||
// timespan of the outer event corresponds to the first cycle of the
|
||||
// inner event
|
||||
const inner_pat = outerHap.value._focusSpan(outerHap.wholeOrPart());
|
||||
// Get the inner events, from the timespan of the outer event's part
|
||||
const innerHaps = inner_pat.query(state.setSpan(outerHap.part));
|
||||
// A function to map over the inner events, to combine them with the
|
||||
// outer event
|
||||
function munge(outer, inner) {
|
||||
let whole = undefined;
|
||||
if (inner.whole && outer.whole) {
|
||||
@@ -679,10 +693,27 @@ export class Pattern {
|
||||
return this.fmap(func)._squeezeJoin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Like layer, but with a single function:
|
||||
* @name apply
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* "<c3 eb3 g3>".scale('C minor').apply(scaleTranspose("0,2,4")).note()
|
||||
*/
|
||||
_apply(func) {
|
||||
return func(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layers the result of the given function(s). Like {@link superimpose}, but without the original pattern:
|
||||
* @name layer
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
|
||||
* .layer(x=>x.add("0,2"))
|
||||
* .scale('C minor').note()
|
||||
*/
|
||||
layer(...funcs) {
|
||||
return stack(...funcs.map((func) => func(this)));
|
||||
}
|
||||
@@ -707,21 +738,36 @@ export class Pattern {
|
||||
// // there is no gap.. so maybe revert to _fast?
|
||||
// return this._fast(factor)
|
||||
// }
|
||||
// A bit fiddly, to drop zero-width queries at the start of the next cycle
|
||||
const qf = function (span) {
|
||||
const cycle = span.begin.sam();
|
||||
const begin = cycle.add(span.begin.sub(cycle).mul(factor).min(1));
|
||||
const end = cycle.add(span.end.sub(cycle).mul(factor).min(1));
|
||||
return new TimeSpan(begin, end);
|
||||
const bpos = span.begin.sub(cycle).mul(factor).min(1);
|
||||
const epos = span.end.sub(cycle).mul(factor).min(1);
|
||||
if (bpos >= 1) {
|
||||
return undefined;
|
||||
}
|
||||
return new TimeSpan(cycle.add(bpos), cycle.add(epos));
|
||||
};
|
||||
const ef = function (span) {
|
||||
const cycle = span.begin.sam();
|
||||
const begin = cycle.add(span.begin.sub(cycle).div(factor).min(1));
|
||||
const end = cycle.add(span.end.sub(cycle).div(factor).min(1));
|
||||
return new TimeSpan(begin, end);
|
||||
// Also fiddly, to maintain the right 'whole' relative to the part
|
||||
const ef = function (hap) {
|
||||
const begin = hap.part.begin;
|
||||
const end = hap.part.end;
|
||||
const cycle = begin.sam();
|
||||
const beginPos = begin.sub(cycle).div(factor).min(1);
|
||||
const endPos = end.sub(cycle).div(factor).min(1);
|
||||
const newPart = new TimeSpan(cycle.add(beginPos), cycle.add(endPos));
|
||||
const newWhole = !hap.whole
|
||||
? undefined
|
||||
: new TimeSpan(
|
||||
newPart.begin.sub(begin.sub(hap.whole.begin).div(factor)),
|
||||
newPart.end.add(hap.whole.end.sub(end).div(factor)),
|
||||
);
|
||||
return new Hap(newWhole, newPart, hap.value, hap.context);
|
||||
};
|
||||
return this.withQuerySpan(qf).withHapSpan(ef)._splitQueries();
|
||||
return this.withQuerySpanMaybe(qf)._withHap(ef)._splitQueries();
|
||||
}
|
||||
|
||||
// Compress each cycle into the given timespan, leaving a gap
|
||||
_compress(b, e) {
|
||||
if (b.gt(e) || b.gt(1) || e.gt(1) || b.lt(0) || e.lt(0)) {
|
||||
return silence;
|
||||
@@ -733,15 +779,25 @@ export class Pattern {
|
||||
return this._compress(span.begin, span.end);
|
||||
}
|
||||
|
||||
// Similar to compress, but doesn't leave gaps, and the 'focus' can be
|
||||
// bigger than a cycle
|
||||
_focus(b, e) {
|
||||
return this._fast(Fraction(1).div(e.sub(b))).late(b.cyclePos());
|
||||
}
|
||||
|
||||
_focusSpan(span) {
|
||||
return this._focus(span.begin, span.end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Speed up a pattern by the given factor.
|
||||
* Speed up a pattern by the given factor. Used by "*" in mini notation.
|
||||
*
|
||||
* @name fast
|
||||
* @memberof Pattern
|
||||
* @param {number | Pattern} factor speed up factor
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* seq(e5, b4, d5, c5).fast(2)
|
||||
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
|
||||
*/
|
||||
_fast(factor) {
|
||||
const fastQuery = this.withQueryTime((t) => t.mul(factor));
|
||||
@@ -749,14 +805,14 @@ export class Pattern {
|
||||
}
|
||||
|
||||
/**
|
||||
* Slow down a pattern over the given number of cycles.
|
||||
* Slow down a pattern over the given number of cycles. Like the "/" operator in mini notation.
|
||||
*
|
||||
* @name slow
|
||||
* @memberof Pattern
|
||||
* @param {number | Pattern} factor slow down factor
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* seq(e5, b4, d5, c5).slow(2)
|
||||
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
|
||||
*/
|
||||
_slow(factor) {
|
||||
return this._fast(Fraction(1).div(factor));
|
||||
@@ -774,6 +830,20 @@ export class Pattern {
|
||||
return this.fmap((x) => pure(x)._fast(factor))._squeezeJoin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cuts each sample into the given number of parts, allowing you to explore a technique known as 'granular synthesis'.
|
||||
* It turns a pattern of samples into a pattern of parts of samples.
|
||||
* @name chop
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
||||
* s("rhodes")
|
||||
* .chop(4)
|
||||
* .rev() // reverse order of chops
|
||||
* .loopAt(4,1) // fit sample into 4 cycles
|
||||
*
|
||||
*/
|
||||
_chop(n) {
|
||||
const slices = Array.from({ length: n }, (x, i) => i);
|
||||
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
|
||||
@@ -795,14 +865,32 @@ export class Pattern {
|
||||
return this._fast(cpm / 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nudge a pattern to start earlier in time. Equivalent of Tidal's <~ operator
|
||||
*
|
||||
* @name early
|
||||
* @memberof Pattern
|
||||
* @param {number | Pattern} cycles number of cycles to nudge left
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "bd ~".stack("hh ~".early(.1)).s()
|
||||
*/
|
||||
_early(offset) {
|
||||
// Equivalent of Tidal's <~ operator
|
||||
offset = Fraction(offset);
|
||||
return this.withQueryTime((t) => t.add(offset)).withHapTime((t) => t.sub(offset));
|
||||
}
|
||||
|
||||
/**
|
||||
* Nudge a pattern to start later in time. Equivalent of Tidal's ~> operator
|
||||
*
|
||||
* @name late
|
||||
* @memberof Pattern
|
||||
* @param {number | Pattern} cycles number of cycles to nudge right
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "bd ~".stack("hh ~".late(.1)).s()
|
||||
*/
|
||||
_late(offset) {
|
||||
// Equivalent of Tidal's ~> operator
|
||||
offset = Fraction(offset);
|
||||
return this._early(Fraction(0).sub(offset));
|
||||
}
|
||||
@@ -829,6 +917,17 @@ export class Pattern {
|
||||
return this._zoom(0, t)._slow(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given structure to the pattern:
|
||||
*
|
||||
* @name struct
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("c3,eb3,g3")
|
||||
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
|
||||
* .slow(4)
|
||||
*/
|
||||
// struct(...binary_pats) {
|
||||
// // Re structure the pattern according to a binary pattern (false values are dropped)
|
||||
// const binary_pat = sequence(binary_pats);
|
||||
@@ -876,6 +975,16 @@ export class Pattern {
|
||||
return this.invert();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given function whenever the given pattern is in a true state.
|
||||
* @name when
|
||||
* @memberof Pattern
|
||||
* @param {Pattern} binary_pat
|
||||
* @param {function} func
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "c3 eb3 g3".when("<0 1>/2", x=>x.sub(5)).note()
|
||||
*/
|
||||
when(binary_pat, func) {
|
||||
//binary_pat = sequence(binary_pat)
|
||||
const true_pat = binary_pat._filterValues(id);
|
||||
@@ -885,10 +994,47 @@ export class Pattern {
|
||||
return stack(with_pat, without_pat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Superimposes the function result on top of the original pattern, delayed by the given time.
|
||||
* @name off
|
||||
* @memberof Pattern
|
||||
* @param {Pattern | number} time offset time
|
||||
* @param {function} func function to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "c3 eb3 g3".off(1/8, x=>x.add(7)).note()
|
||||
*/
|
||||
off(time_pat, func) {
|
||||
return stack(this, func(this.late(time_pat)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given function every n cycles.
|
||||
* @name every
|
||||
* @memberof Pattern
|
||||
* @param {number} n how many cycles
|
||||
* @param {function} func function to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("c3 d3 e3 g3").every(4, x=>x.rev())
|
||||
*/
|
||||
every(n, func) {
|
||||
const pat = this;
|
||||
const pats = Array(n - 1).fill(pat);
|
||||
// pats.unshift(func(pat));
|
||||
pats.push(func(pat));
|
||||
return slowcatPrime(...pats);
|
||||
}
|
||||
/**
|
||||
* Applies the given function every n cycles, starting from the first cycle.
|
||||
* @name every
|
||||
* @memberof Pattern
|
||||
* @param {number} n how many cycles
|
||||
* @param {function} func function to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("c3 d3 e3 g3").every(4, x=>x.rev())
|
||||
*/
|
||||
every(n, func) {
|
||||
const pat = this;
|
||||
const pats = Array(n - 1).fill(pat);
|
||||
@@ -896,6 +1042,23 @@ export class Pattern {
|
||||
return slowcatPrime(...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given function every n cycles, starting from the last cycle.
|
||||
* @name each
|
||||
* @memberof Pattern
|
||||
* @param {number} n how many cycles
|
||||
* @param {function} func function to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("c3 d3 e3 g3").every(4, x=>x.rev())
|
||||
*/
|
||||
each(n, func) {
|
||||
const pat = this;
|
||||
const pats = Array(n - 1).fill(pat);
|
||||
pats.push(func(pat));
|
||||
return slowcatPrime(...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new pattern where every other cycle is played once, twice as
|
||||
* fast, and offset in time by one quarter of a cycle. Creates a kind of
|
||||
@@ -906,6 +1069,15 @@ export class Pattern {
|
||||
return this.when(slowcat(false, true), (x) => fastcat(x, silence)._late(0.25));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse all haps in a pattern
|
||||
*
|
||||
* @name rev
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("c3 d3 e3 g3").rev()
|
||||
*/
|
||||
rev() {
|
||||
const pat = this;
|
||||
const query = function (state) {
|
||||
@@ -948,6 +1120,15 @@ export class Pattern {
|
||||
return this.juxBy(1, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stacks the given pattern(s) to the current pattern.
|
||||
* @name stack
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("hh*2").stack(
|
||||
* n("c2(3,8)")
|
||||
* )
|
||||
*/
|
||||
stack(...pats) {
|
||||
return stack(this, ...pats);
|
||||
}
|
||||
@@ -956,11 +1137,28 @@ export class Pattern {
|
||||
return sequence(this, ...pats);
|
||||
}
|
||||
|
||||
// shorthand for sequence
|
||||
/**
|
||||
* Appends the given pattern(s) to the current pattern. Synonyms: .sequence .fastcat
|
||||
* @name seq
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("hh*2").seq(
|
||||
* n("c2(3,8)")
|
||||
* )
|
||||
*/
|
||||
seq(...pats) {
|
||||
return sequence(this, ...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the given pattern(s) to the next cycle. Synonym: .slowcat
|
||||
* @name cat
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("hh*2").cat(
|
||||
* n("c2(3,8)")
|
||||
* )
|
||||
*/
|
||||
cat(...pats) {
|
||||
return cat(this, ...pats);
|
||||
}
|
||||
@@ -973,6 +1171,16 @@ export class Pattern {
|
||||
return slowcat(this, ...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Superimposes the result of the given function(s) on top of the original pattern:
|
||||
* @name superimpose
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
|
||||
* .superimpose(x=>x.add(2))
|
||||
* .scale('C minor').note()
|
||||
*/
|
||||
superimpose(...funcs) {
|
||||
return this.stack(...funcs.map((func) => func(this)));
|
||||
}
|
||||
@@ -985,24 +1193,70 @@ export class Pattern {
|
||||
return this.stutWith(times, time, (pat, i) => pat.velocity(Math.pow(feedback, i)));
|
||||
}
|
||||
|
||||
// these might change with: https://github.com/tidalcycles/Tidal/issues/902
|
||||
/**
|
||||
* Superimpose and offset multiple times, applying the given function each time.
|
||||
* @name echoWith
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @param {number} times how many times to repeat
|
||||
* @param {number} time cycle offset between iterations
|
||||
* @param {function} func function to apply, given the pattern and the iteration index
|
||||
* @example
|
||||
* "<0 [2 4]>"
|
||||
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
|
||||
* .scale('C minor').note().legato(.2)
|
||||
*/
|
||||
_echoWith(times, time, func) {
|
||||
return stack(...listRange(0, times - 1).map((i) => func(this.late(Fraction(time).mul(i)), i)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Superimpose and offset multiple times, gradually decreasing the velocity
|
||||
* @name echo
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @param {number} times how many times to repeat
|
||||
* @param {number} time cycle offset between iterations
|
||||
* @param {number} feedback velocity multiplicator for each iteration
|
||||
* @example
|
||||
* s("bd sd").echo(3, 1/6, .8)
|
||||
*/
|
||||
_echo(times, time, feedback) {
|
||||
return this._echoWith(times, time, (pat, i) => pat.velocity(Math.pow(feedback, i)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played.
|
||||
* @name iter
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("0 1 2 3".scale('A minor')).iter(4)
|
||||
*/
|
||||
iter(times, back = false) {
|
||||
return slowcat(...listRange(0, times - 1).map((i) => (back ? this.late(i / times) : this.early(i / times))));
|
||||
}
|
||||
|
||||
// known as iter' in tidalcycles
|
||||
/**
|
||||
* Like `iter`, but plays the subdivisions in reverse order. Known as iter' in tidalcycles
|
||||
* @name iterBack
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("0 1 2 3".scale('A minor')).iterBack(4)
|
||||
*/
|
||||
iterBack(times) {
|
||||
return this.iter(times, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle).
|
||||
* @name chunk
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "0 1 2 3".chunk(4, x=>x.add(7)).scale('A minor').note()
|
||||
*/
|
||||
_chunk(n, func, back = false) {
|
||||
const binary = Array(n - 1).fill(false);
|
||||
binary.unshift(true);
|
||||
@@ -1010,6 +1264,14 @@ export class Pattern {
|
||||
return this.when(binary_pat, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `chunk`, but cycles through the parts in reverse order. Known as chunk' in tidalcycles
|
||||
* @name chunkBack
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "0 1 2 3".chunkBack(4, x=>x.add(7)).scale('A minor').note()
|
||||
*/
|
||||
_chunkBack(n, func) {
|
||||
return this._chunk(n, func, true);
|
||||
}
|
||||
@@ -1028,37 +1290,64 @@ export class Pattern {
|
||||
return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
|
||||
}
|
||||
|
||||
// sets hap relative duration of haps
|
||||
/**
|
||||
*
|
||||
* Multiplies the hap duration with the given factor.
|
||||
* @name legato
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* note("c3 eb3 g3 c4").legato("<.25 .5 1 2>")
|
||||
*/
|
||||
_legato(value) {
|
||||
return this.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(span.end.sub(span.begin).mul(value))));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Sets the velocity from 0 to 1. Is multiplied together with gain.
|
||||
* @name velocity
|
||||
* @example
|
||||
* s("hh*8")
|
||||
* .gain(".4!2 1 .4!2 1 .4 1")
|
||||
* .velocity(".4 1")
|
||||
*/
|
||||
_velocity(velocity) {
|
||||
return this._withContext((context) => ({ ...context, velocity: (context.velocity || 1) * velocity }));
|
||||
}
|
||||
|
||||
// move this to controls? (speed and unit are controls)
|
||||
/**
|
||||
* Makes the sample fit the given number of cycles by changing the speed.
|
||||
* @name loopAt
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
||||
* s("rhodes").loopAt(4,1)
|
||||
*/
|
||||
_loopAt(factor, cps = 1) {
|
||||
return this.speed((1 / factor) * cps)
|
||||
.unit('c')
|
||||
.slow(factor);
|
||||
}
|
||||
onTrigger(onTrigger) {
|
||||
return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger }));
|
||||
}
|
||||
log(func = id) {
|
||||
onTrigger(onTrigger, dominant = true) {
|
||||
return this._withHap((hap) =>
|
||||
hap.setContext({
|
||||
...hap.context,
|
||||
onTrigger: (...args) => {
|
||||
if (hap.context.onTrigger) {
|
||||
if (!dominant && hap.context.onTrigger) {
|
||||
hap.context.onTrigger(...args);
|
||||
}
|
||||
console.log(func(...args));
|
||||
onTrigger(...args);
|
||||
},
|
||||
// we need this to know later if the default trigger should still fire
|
||||
dominantTrigger: dominant,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`) {
|
||||
return this.onTrigger((...args) => logger(func(...args)), false);
|
||||
}
|
||||
logValues(func = id) {
|
||||
return this.log((_, hap) => func(hap.value));
|
||||
}
|
||||
@@ -1083,9 +1372,6 @@ function _composeOp(a, b, func) {
|
||||
|
||||
// Make composers
|
||||
(function () {
|
||||
const num = (pat) => pat._asNumber();
|
||||
const numOrString = (pat) => pat._asNumber(false, true);
|
||||
|
||||
// pattern composers
|
||||
const composers = {
|
||||
set: [(a, b) => b],
|
||||
@@ -1093,17 +1379,56 @@ function _composeOp(a, b, func) {
|
||||
keepif: [(a, b) => (b ? a : undefined)],
|
||||
|
||||
// numerical functions
|
||||
add: [(a, b) => a + b, numOrString], // support string concatenation
|
||||
sub: [(a, b) => a - b, num],
|
||||
mul: [(a, b) => a * b, num],
|
||||
div: [(a, b) => a / b, num],
|
||||
mod: [mod, num],
|
||||
pow: [Math.pow, num],
|
||||
_and: [(a, b) => a & b, num],
|
||||
_or: [(a, b) => a | b, num],
|
||||
_xor: [(a, b) => a ^ b, num],
|
||||
_lshift: [(a, b) => a << b, num],
|
||||
_rshift: [(a, b) => a >> b, num],
|
||||
/**
|
||||
*
|
||||
* Assumes a pattern of numbers. Adds the given number to each item in the pattern.
|
||||
* @name add
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* // Here, the triad 0, 2, 4 is shifted by different amounts
|
||||
* "0 2 4".add("<0 3 4 0>").scale('C major').note()
|
||||
* // Without add, the equivalent would be:
|
||||
* // "<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>".scale('C major').note()
|
||||
* @example
|
||||
* // You can also use add with notes:
|
||||
* "c3 e3 g3".add("<0 5 7 0>").note()
|
||||
* // Behind the scenes, the notes are converted to midi numbers:
|
||||
* // "48 52 55".add("<0 5 7 0>").note()
|
||||
*/
|
||||
add: [numeralArgs((a, b) => a + b)], // support string concatenation
|
||||
/**
|
||||
*
|
||||
* Like add, but the given numbers are subtracted.
|
||||
* @name sub
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* "0 2 4".sub("<0 1 2 3>").scale('C4 minor').note()
|
||||
* // See add for more information.
|
||||
*/
|
||||
sub: [numeralArgs((a, b) => a - b)],
|
||||
/**
|
||||
*
|
||||
* Multiplies each number by the given factor.
|
||||
* @name mul
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* "1 1.5 [1.66, <2 2.33>]".mul(150).freq()
|
||||
*/
|
||||
mul: [numeralArgs((a, b) => a * b)],
|
||||
/**
|
||||
*
|
||||
* Divides each number by the given factor.
|
||||
* @name div
|
||||
* @memberof Pattern
|
||||
*/
|
||||
div: [numeralArgs((a, b) => a / b)],
|
||||
mod: [numeralArgs(mod)],
|
||||
pow: [numeralArgs(Math.pow)],
|
||||
_and: [numeralArgs((a, b) => a & b)],
|
||||
_or: [numeralArgs((a, b) => a | b)],
|
||||
_xor: [numeralArgs((a, b) => a ^ b)],
|
||||
_lshift: [numeralArgs((a, b) => a << b)],
|
||||
_rshift: [numeralArgs((a, b) => a >> b)],
|
||||
|
||||
// TODO - force numerical comparison if both look like numbers?
|
||||
lt: [(a, b) => a < b],
|
||||
@@ -1131,10 +1456,14 @@ function _composeOp(a, b, func) {
|
||||
pat = preprocess(pat);
|
||||
other = preprocess(other);
|
||||
}
|
||||
var result = pat['_op' + how](other, (a) => (b) => _composeOp(a, b, op));
|
||||
var result;
|
||||
// hack to remove undefs when doing 'keepif'
|
||||
if (what === 'keepif') {
|
||||
// avoid union, as we want to throw away the value of 'b' completely
|
||||
result = pat['_op' + how](other, (a) => (b) => op(a, b));
|
||||
result = result._removeUndefineds();
|
||||
} else {
|
||||
result = pat['_op' + how](other, (a) => (b) => _composeOp(a, b, op));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -1184,6 +1513,11 @@ Pattern.prototype.patternified = [
|
||||
'slow',
|
||||
'velocity',
|
||||
];
|
||||
|
||||
// aliases
|
||||
export const polyrhythm = stack;
|
||||
export const pr = stack;
|
||||
|
||||
// methods that create patterns, which are added to patternified Pattern methods
|
||||
Pattern.prototype.factories = {
|
||||
pure,
|
||||
@@ -1206,12 +1540,11 @@ Pattern.prototype.factories = {
|
||||
// Nothing
|
||||
export const silence = new Pattern((_) => []);
|
||||
|
||||
/** A discrete value that repeats once per cycle:
|
||||
/** A discrete value that repeats once per cycle.
|
||||
*
|
||||
* @param {any} value - The value to repeat
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* pure('e4')
|
||||
* pure('e4') // "e4"
|
||||
*/
|
||||
export function pure(value) {
|
||||
function query(state) {
|
||||
@@ -1222,7 +1555,7 @@ export function pure(value) {
|
||||
|
||||
export function isPattern(thing) {
|
||||
// thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled
|
||||
const is = thing instanceof Pattern || thing._Pattern;
|
||||
const is = thing instanceof Pattern || thing?._Pattern;
|
||||
if (!thing instanceof Pattern) {
|
||||
console.warn(
|
||||
`Found Pattern that fails "instanceof Pattern" check.
|
||||
@@ -1238,15 +1571,17 @@ export function reify(thing) {
|
||||
if (isPattern(thing)) {
|
||||
return thing;
|
||||
}
|
||||
if (stringParser && typeof thing === 'string') {
|
||||
return stringParser(thing);
|
||||
}
|
||||
return pure(thing);
|
||||
}
|
||||
|
||||
/** The given items are played at the same time at the same length:
|
||||
/** The given items are played at the same time at the same length.
|
||||
*
|
||||
* @param {...any} items - The items to stack
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* stack(g3, b3, [e4, d4])
|
||||
* stack(g3, b3, [e4, d4]).note() // "g3,b3,[e4,d4]".note()
|
||||
*/
|
||||
export function stack(...pats) {
|
||||
// Array test here is to avoid infinite recursions..
|
||||
@@ -1259,7 +1594,6 @@ export function stack(...pats) {
|
||||
*
|
||||
* synonyms: {@link cat}
|
||||
*
|
||||
* @param {...any} items - The items to concatenate
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* slowcat(e5, b4, [d5, c5])
|
||||
@@ -1315,16 +1649,22 @@ export function fastcat(...pats) {
|
||||
return slowcat(...pats)._fast(pats.length);
|
||||
}
|
||||
|
||||
/** See {@link slowcat} */
|
||||
/** The given items are con**cat**enated, where each one takes one cycle. Synonym: slowcat
|
||||
*
|
||||
* @param {...any} items - The items to concatenate
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* cat(e5, b4, [d5, c5]).note() // "<e5 b4 [d5 c5]>".note()
|
||||
*
|
||||
*/
|
||||
export function cat(...pats) {
|
||||
return slowcat(...pats);
|
||||
}
|
||||
|
||||
/** Like {@link fastcat}, but where each step has a temporal weight:
|
||||
* @param {...Array} items - The items to concatenate
|
||||
/** Like {@link seq}, but each step has a length, relative to the whole.
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* timeCat([3,e3],[1, g3])
|
||||
* timeCat([3,e3],[1, g3]).note() // "e3@3 g3".note()
|
||||
*/
|
||||
export function timeCat(...timepats) {
|
||||
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
|
||||
@@ -1343,7 +1683,11 @@ export function sequence(...pats) {
|
||||
return fastcat(...pats);
|
||||
}
|
||||
|
||||
/** See {@link fastcat} */
|
||||
/** Like **cat**, but the items are crammed into one cycle. Synonyms: fastcat, sequence
|
||||
* @example
|
||||
* seq(e5, b4, [d5, c5]).note() // "e5 b4 [d5 c5]".note()
|
||||
*
|
||||
*/
|
||||
export function seq(...pats) {
|
||||
return fastcat(...pats);
|
||||
}
|
||||
@@ -1392,20 +1736,6 @@ export function pm(...args) {
|
||||
polymeter(...args);
|
||||
}
|
||||
|
||||
export function polyrhythm(...xs) {
|
||||
const seqs = xs.map((a) => sequence(a));
|
||||
|
||||
if (seqs.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
return stack(...seqs);
|
||||
}
|
||||
|
||||
// alias
|
||||
export function pr(args) {
|
||||
polyrhythm(args);
|
||||
}
|
||||
|
||||
export const add = curry((a, pat) => pat.add(a));
|
||||
export const chop = curry((a, pat) => pat.chop(a));
|
||||
export const chunk = curry((a, pat) => pat.chunk(a));
|
||||
@@ -1428,6 +1758,7 @@ export const mul = curry((a, pat) => pat.mul(a));
|
||||
export const off = curry((t, f, pat) => pat.off(t, f));
|
||||
export const ply = curry((a, pat) => pat.ply(a));
|
||||
export const range = curry((a, b, pat) => pat.range(a, b));
|
||||
export const rangex = curry((a, b, pat) => pat.rangex(a, b));
|
||||
export const range2 = curry((a, b, pat) => pat.range2(a, b));
|
||||
export const rev = (pat) => pat.rev();
|
||||
export const slow = curry((a, pat) => pat.slow(a));
|
||||
@@ -1514,6 +1845,18 @@ Pattern.prototype.inside = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._inside)(...args, this);
|
||||
};
|
||||
Pattern.prototype.range = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._range)(...args, this);
|
||||
};
|
||||
Pattern.prototype.rangex = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._rangex)(...args, this);
|
||||
};
|
||||
Pattern.prototype.range2 = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._range2)(...args, this);
|
||||
};
|
||||
|
||||
// call this after all Patter.prototype.define calls have been executed! (right before evaluate)
|
||||
Pattern.prototype.bootstrap = function () {
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { Pattern } from '@strudel.cycles/core';
|
||||
import { Pattern } from './index.mjs';
|
||||
|
||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||
const getValue = (e) => {
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Cyclist } from './cyclist.mjs';
|
||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { setTime } from './time.mjs';
|
||||
|
||||
export function repl({
|
||||
interval,
|
||||
defaultOutput,
|
||||
onSchedulerError,
|
||||
onEvalError,
|
||||
beforeEval,
|
||||
afterEval,
|
||||
getTime,
|
||||
transpiler,
|
||||
onToggle,
|
||||
}) {
|
||||
const scheduler = new Cyclist({
|
||||
interval,
|
||||
onTrigger: async (hap, deadline, duration) => {
|
||||
try {
|
||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||
await defaultOutput(hap, deadline, duration);
|
||||
}
|
||||
if (hap.context.onTrigger) {
|
||||
const cps = 1;
|
||||
// call signature of output / onTrigger is different...
|
||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
||||
}
|
||||
} catch (err) {
|
||||
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||
}
|
||||
},
|
||||
onError: onSchedulerError,
|
||||
getTime,
|
||||
onToggle,
|
||||
});
|
||||
setTime(() => scheduler.getPhase()); // TODO: refactor?
|
||||
const evaluate = async (code, autostart = true) => {
|
||||
if (!code) {
|
||||
throw new Error('no code to evaluate');
|
||||
}
|
||||
try {
|
||||
beforeEval({ code });
|
||||
const { pattern } = await _evaluate(code, transpiler);
|
||||
logger(`[eval] code updated`);
|
||||
scheduler.setPattern(pattern, autostart);
|
||||
afterEval({ code, pattern });
|
||||
return pattern;
|
||||
} catch (err) {
|
||||
// console.warn(`[repl] eval error: ${err.message}`);
|
||||
logger(`[eval] error: ${err.message}`, 'error');
|
||||
onEvalError?.(err);
|
||||
}
|
||||
};
|
||||
const stop = () => scheduler.stop();
|
||||
const start = () => scheduler.start();
|
||||
const pause = () => scheduler.pause();
|
||||
return { scheduler, evaluate, start, stop, pause };
|
||||
}
|
||||
+203
-11
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { Hap } from './hap.mjs';
|
||||
import { Pattern, fastcat, reify, silence, stack } from './pattern.mjs';
|
||||
import { Pattern, fastcat, reify, silence, stack, isPattern } from './pattern.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
import { id } from './util.mjs';
|
||||
|
||||
@@ -27,9 +27,9 @@ export const isaw2 = isaw._toBipolar();
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4))
|
||||
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
|
||||
* @example
|
||||
* saw.range(0,8).segment(8).scale('C major').slow(4)
|
||||
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
||||
*
|
||||
*/
|
||||
export const saw = signal((t) => t % 1);
|
||||
@@ -42,7 +42,7 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* sine.segment(16).range(0,15).slow(2).scale('C minor')
|
||||
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
|
||||
*
|
||||
*/
|
||||
export const sine = sine2._fromBipolar();
|
||||
@@ -52,7 +52,7 @@ export const sine = sine2._fromBipolar();
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor')
|
||||
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor').note()
|
||||
*
|
||||
*/
|
||||
export const cosine = sine._early(Fraction(1).div(4));
|
||||
@@ -63,7 +63,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* square.segment(2).range(0,7).scale('C minor')
|
||||
* square.segment(2).range(0,7).scale('C minor').note()
|
||||
*
|
||||
*/
|
||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
||||
@@ -74,7 +74,7 @@ export const square2 = square._toBipolar();
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* triangle.segment(2).range(0,7).scale('C minor')
|
||||
* tri.segment(8).range(0,7).scale('C minor').note()
|
||||
*
|
||||
*/
|
||||
export const tri = fastcat(isaw, saw);
|
||||
@@ -111,7 +111,17 @@ const timeToRandsPrime = (seed, n) => {
|
||||
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
||||
|
||||
/**
|
||||
* A continuous pattern of random numbers, between 0 and 1
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* A continuous pattern of random numbers, between 0 and 1.
|
||||
*
|
||||
* @name rand
|
||||
* @example
|
||||
* // randomly change the cutoff
|
||||
* s("bd sd,hh*4").cutoff(rand.range(500,2000))
|
||||
*
|
||||
*/
|
||||
export const rand = signal(timeToRand);
|
||||
/**
|
||||
@@ -124,6 +134,17 @@ export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
||||
export const brand = _brandBy(0.5);
|
||||
|
||||
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
||||
|
||||
/**
|
||||
* A continuous pattern of random integers, between 0 and n-1.
|
||||
*
|
||||
* @name irand
|
||||
* @param {number} n max value (exclusive)
|
||||
* @example
|
||||
* // randomly select scale notes from 0 - 7 (= C to C)
|
||||
* irand(8).struct("x(3,8)").scale('C minor').note()
|
||||
*
|
||||
*/
|
||||
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
||||
|
||||
export const __chooseWith = (pat, xs) => {
|
||||
@@ -156,8 +177,8 @@ export const chooseInWith = (pat, xs) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Chooses randomly from the given list of values.
|
||||
* @param {...any} xs
|
||||
* Chooses randomly from the given list of elements.
|
||||
* @param {...any} xs values / patterns to choose from.
|
||||
* @returns {Pattern} - a continuous pattern.
|
||||
*/
|
||||
export const choose = (...xs) => chooseWith(rand, xs);
|
||||
@@ -183,6 +204,14 @@ Pattern.prototype.choose2 = function (...xs) {
|
||||
return chooseWith(this._fromBipolar(), xs);
|
||||
};
|
||||
|
||||
/**
|
||||
* Picks one of the elements at random each cycle.
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* chooseCycles("bd", "hh", "sd").s().fast(4)
|
||||
* @example
|
||||
* "bd | hh | sd".s().fast(4)
|
||||
*/
|
||||
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
|
||||
|
||||
export const randcat = chooseCycles;
|
||||
@@ -209,6 +238,7 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
||||
|
||||
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
|
||||
|
||||
// this function expects pat to be a pattern of floats...
|
||||
export const perlinWith = (pat) => {
|
||||
const pata = pat.fmap(Math.floor);
|
||||
const patb = pat.fmap((t) => Math.floor(t) + 1);
|
||||
@@ -217,20 +247,68 @@ export const perlinWith = (pat) => {
|
||||
return pat.sub(pata).fmap(interp).appBoth(pata.fmap(timeToRand)).appBoth(patb.fmap(timeToRand));
|
||||
};
|
||||
|
||||
export const perlin = perlinWith(time);
|
||||
/**
|
||||
* Generates a continuous pattern of [perlin noise](https://en.wikipedia.org/wiki/Perlin_noise), in the range 0..1.
|
||||
*
|
||||
* @name perlin
|
||||
* @example
|
||||
* // randomly change the cutoff
|
||||
* s("bd sd,hh*4").cutoff(perlin.range(500,2000))
|
||||
*
|
||||
*/
|
||||
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
||||
|
||||
Pattern.prototype._degradeByWith = function (withPat, x) {
|
||||
return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x));
|
||||
};
|
||||
|
||||
/**
|
||||
* Randomly removes events from the pattern by a given amount.
|
||||
* 0 = 0% chance of removal
|
||||
* 1 = 100% chance of removal
|
||||
*
|
||||
* @name degradeBy
|
||||
* @memberof Pattern
|
||||
* @param {number} amount - a number between 0 and 1
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").degradeBy(0.2)
|
||||
* @example
|
||||
* s("[hh?0.2]*8")
|
||||
*/
|
||||
Pattern.prototype._degradeBy = function (x) {
|
||||
return this._degradeByWith(rand, x);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Randomly removes 50% of events from the pattern. Shorthand for `.degradeBy(0.5)`
|
||||
*
|
||||
* @name degrade
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").degrade()
|
||||
* @example
|
||||
* s("[hh?]*8")
|
||||
*/
|
||||
Pattern.prototype.degrade = function () {
|
||||
return this._degradeBy(0.5);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inverse of {@link Pattern#degradeBy}: Randomly removes events from the pattern by a given amount.
|
||||
* 0 = 100% chance of removal
|
||||
* 1 = 0% chance of removal
|
||||
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
|
||||
*
|
||||
* @name undegradeBy
|
||||
* @memberof Pattern
|
||||
* @param {number} amount - a number between 0 and 1
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").undegradeBy(0.2)
|
||||
*/
|
||||
Pattern.prototype._undegradeBy = function (x) {
|
||||
return this._degradeByWith(
|
||||
rand.fmap((r) => 1 - r),
|
||||
@@ -246,6 +324,25 @@ Pattern.prototype._sometimesBy = function (x, func) {
|
||||
return stack(this._degradeBy(x), func(this._undegradeBy(1 - x)));
|
||||
};
|
||||
|
||||
// https://github.com/tidalcycles/strudel/discussions/198
|
||||
/* Pattern.prototype._sometimesBy = function (x, other) {
|
||||
other = typeof other === 'function' ? other(this._undegradeBy(1 - x)) : reify(other)._undegradeBy(1 - x);
|
||||
return stack(this._degradeBy(x), other);
|
||||
}; */
|
||||
|
||||
/**
|
||||
*
|
||||
* Randomly applies the given function by the given probability.
|
||||
* Similar to {@link Pattern#someCyclesBy}
|
||||
*
|
||||
* @name sometimesBy
|
||||
* @memberof Pattern
|
||||
* @param {number | Pattern} probability - a number between 0 and 1
|
||||
* @param {function} function - the transformation to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.sometimesBy = function (patx, func) {
|
||||
const pat = this;
|
||||
return reify(patx)
|
||||
@@ -253,6 +350,7 @@ Pattern.prototype.sometimesBy = function (patx, func) {
|
||||
.innerJoin();
|
||||
};
|
||||
|
||||
// why does this exist? it is identical to sometimesBy
|
||||
Pattern.prototype._sometimesByPre = function (x, func) {
|
||||
return stack(this._degradeBy(x), func(this).undegradeBy(1 - x));
|
||||
};
|
||||
@@ -264,6 +362,17 @@ Pattern.prototype.sometimesByPre = function (patx, func) {
|
||||
.innerJoin();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Applies the given function with a 50% chance
|
||||
*
|
||||
* @name sometimes
|
||||
* @memberof Pattern
|
||||
* @param {function} function - the transformation to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*4").sometimes(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.sometimes = function (func) {
|
||||
return this._sometimesBy(0.5, func);
|
||||
};
|
||||
@@ -279,6 +388,19 @@ Pattern.prototype._someCyclesBy = function (x, func) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Randomly applies the given function by the given probability on a cycle by cycle basis.
|
||||
* Similar to {@link Pattern#sometimesBy}
|
||||
*
|
||||
* @name someCyclesBy
|
||||
* @memberof Pattern
|
||||
* @param {number | Pattern} probability - a number between 0 and 1
|
||||
* @param {function} function - the transformation to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.someCyclesBy = function (patx, func) {
|
||||
const pat = this;
|
||||
return reify(patx)
|
||||
@@ -286,30 +408,100 @@ Pattern.prototype.someCyclesBy = function (patx, func) {
|
||||
.innerJoin();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Shorthand for `.someCyclesBy(0.5, fn)`
|
||||
*
|
||||
* @name someCycles
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.someCycles = function (func) {
|
||||
return this._someCyclesBy(0.5, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Shorthand for `.sometimesBy(0.75, fn)`
|
||||
*
|
||||
* @name often
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").often(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.often = function (func) {
|
||||
return this.sometimesBy(0.75, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Shorthand for `.sometimesBy(0.25, fn)`
|
||||
*
|
||||
* @name rarely
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").rarely(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.rarely = function (func) {
|
||||
return this.sometimesBy(0.25, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Shorthand for `.sometimesBy(0.1, fn)`
|
||||
*
|
||||
* @name almostNever
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").almostNever(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.almostNever = function (func) {
|
||||
return this.sometimesBy(0.1, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Shorthand for `.sometimesBy(0.9, fn)`
|
||||
*
|
||||
* @name almostAlways
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").almostAlways(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.almostAlways = function (func) {
|
||||
return this.sometimesBy(0.9, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Shorthand for `.sometimesBy(0, fn)` (never calls fn)
|
||||
*
|
||||
* @name never
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").never(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.never = function (func) {
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Shorthand for `.sometimesBy(1, fn)` (always calls fn)
|
||||
*
|
||||
* @name always
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*8").always(x=>x.speed("0.5"))
|
||||
*/
|
||||
Pattern.prototype.always = function (func) {
|
||||
return func(this);
|
||||
};
|
||||
|
||||
@@ -32,11 +32,8 @@ function speak(words, lang, voice) {
|
||||
}
|
||||
|
||||
Pattern.prototype._speak = function (lang, voice) {
|
||||
return this._withHap((hap) => {
|
||||
const onTrigger = (time, hap) => {
|
||||
speak(hap.value, lang, voice);
|
||||
};
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
return this.onTrigger((_, hap) => {
|
||||
speak(hap.value, lang, voice);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -42,10 +42,14 @@ import {
|
||||
id,
|
||||
ply,
|
||||
rev,
|
||||
time,
|
||||
} from '../index.mjs';
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
|
||||
import controls from '../controls.mjs';
|
||||
|
||||
const { n } = controls;
|
||||
const st = (begin, end) => new State(ts(begin, end));
|
||||
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
||||
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
|
||||
@@ -136,6 +140,9 @@ describe('Pattern', () => {
|
||||
it('Can make a pattern', () => {
|
||||
expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3);
|
||||
});
|
||||
it('Supports zero-width queries', () => {
|
||||
expect(pure('hello').queryArc(0, 0).length).toBe(1);
|
||||
});
|
||||
});
|
||||
describe('fmap()', () => {
|
||||
it('Can add things', () => {
|
||||
@@ -190,6 +197,9 @@ describe('Pattern', () => {
|
||||
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
|
||||
);
|
||||
});
|
||||
it('can add object patterns', () => {
|
||||
sameFirst(n(sequence(1, [2, 3])).add(n(10)), n(sequence(11, [12, 13])));
|
||||
});
|
||||
});
|
||||
describe('keep()', () => {
|
||||
it('can structure In()', () => {
|
||||
@@ -371,6 +381,12 @@ describe('Pattern', () => {
|
||||
sequence(['a', 'b', 'c'], silence, ['a', 'b', 'c'], silence).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('copes with breaking up events across cycles', () => {
|
||||
expect(pure('a').slow(2)._fastGap(2)._setContext({}).query(st(0, 2))).toStrictEqual([
|
||||
hap(ts(0, 1), ts(0, 0.5), 'a'),
|
||||
hap(ts(0.5, 1.5), ts(1, 1.5), 'a'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('_compressSpan()', () => {
|
||||
it('Can squash cycles of a pattern into a given timespan', () => {
|
||||
@@ -424,6 +440,16 @@ describe('Pattern', () => {
|
||||
// mini('[c3 g3]/2 eb3') always plays [c3 eb3]
|
||||
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
|
||||
});
|
||||
it('Supports zero-length queries', () => {
|
||||
expect(steady('a')._slow(1).queryArc(0, 0)).toStrictEqual(steady('a').queryArc(0, 0));
|
||||
});
|
||||
});
|
||||
describe('slow()', () => {
|
||||
it('Supports zero-length queries', () => {
|
||||
expect(steady('a').slow(1)._setContext({}).queryArc(0, 0)).toStrictEqual(
|
||||
steady('a')._setContext({}).queryArc(0, 0),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('inside', () => {
|
||||
it('can rev inside a cycle', () => {
|
||||
@@ -791,6 +817,14 @@ describe('Pattern', () => {
|
||||
).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('Squeezes to the correct cycle', () => {
|
||||
expect(
|
||||
pure(time.struct(true))
|
||||
._squeezeJoin()
|
||||
.queryArc(3, 4)
|
||||
.map((x) => x.value),
|
||||
).toStrictEqual([Fraction(3.5)]);
|
||||
});
|
||||
});
|
||||
describe('ply', () => {
|
||||
it('Can ply(3)', () => {
|
||||
@@ -840,6 +874,11 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('range', () => {
|
||||
it('Can be patterned', () => {
|
||||
expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(sequence(0, 0.5).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('range2', () => {
|
||||
it('Can change the range of a bipolar pattern', () => {
|
||||
expect(sequence(-1, -0.5, 0, 0.5).range2(1000, 1100).firstCycle()).toStrictEqual(
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
/*
|
||||
util.test.mjs - <short description TODO>
|
||||
util.test.mjs - Tests for the core 'util' module
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/util.test.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 { pure } from '../pattern.mjs';
|
||||
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency } from '../util.mjs';
|
||||
import {
|
||||
isNote,
|
||||
tokenizeNote,
|
||||
toMidi,
|
||||
fromMidi,
|
||||
mod,
|
||||
compose,
|
||||
getFrequency,
|
||||
getPlayableNoteValue,
|
||||
parseNumeral,
|
||||
parseFractional,
|
||||
numeralArgs,
|
||||
fractionalArgs,
|
||||
} from '../util.mjs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('isNote', () => {
|
||||
@@ -70,6 +83,10 @@ describe('toMidi', () => {
|
||||
expect(toMidi('C#3')).toEqual(49);
|
||||
expect(toMidi('C##3')).toEqual(50);
|
||||
});
|
||||
it('should throw an error when given a non-note', () => {
|
||||
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||
});
|
||||
});
|
||||
describe('fromMidi', () => {
|
||||
it('should turn midi into frequency', () => {
|
||||
@@ -78,13 +95,27 @@ describe('fromMidi', () => {
|
||||
});
|
||||
});
|
||||
describe('getFrequency', () => {
|
||||
it('should turn midi into frequency', () => {
|
||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||
it('should turn note into frequency', () => {
|
||||
expect(getFrequency(happify('a4'))).toEqual(440);
|
||||
expect(getFrequency(happify('a3'))).toEqual(220);
|
||||
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440); // TODO: migrate when values are objects..
|
||||
});
|
||||
it('should turn midi into frequency', () => {
|
||||
expect(getFrequency(happify(69, { type: 'midi' }))).toEqual(440);
|
||||
expect(getFrequency(happify(57, { type: 'midi' }))).toEqual(220);
|
||||
});
|
||||
it('should return frequencies unchanged', () => {
|
||||
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440);
|
||||
expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432);
|
||||
});
|
||||
it('should turn object with a "freq" property into frequency', () => {
|
||||
expect(getFrequency(happify({ freq: 220 }))).toEqual(220);
|
||||
expect(getFrequency(happify({ freq: 440 }))).toEqual(440);
|
||||
});
|
||||
it('should throw an error when given a non-note', () => {
|
||||
expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`);
|
||||
expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mod', () => {
|
||||
@@ -118,3 +149,76 @@ describe('compose', () => {
|
||||
expect(compose(addS('a'), addS('b'))('x')).toEqual('xab');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlayableNoteValue', () => {
|
||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||
it('should return object "note" property', () => {
|
||||
expect(getPlayableNoteValue(happify({ note: 'a4' }))).toEqual('a4');
|
||||
});
|
||||
it('should return object "n" property', () => {
|
||||
expect(getPlayableNoteValue(happify({ n: 'a4' }))).toEqual('a4');
|
||||
});
|
||||
it('should return object "value" property', () => {
|
||||
expect(getPlayableNoteValue(happify({ value: 'a4' }))).toEqual('a4');
|
||||
});
|
||||
it('should turn midi into frequency', () => {
|
||||
expect(getPlayableNoteValue(happify(57, { type: 'midi' }))).toEqual(220);
|
||||
});
|
||||
it('should return frequency value', () => {
|
||||
expect(getPlayableNoteValue(happify(220, { type: 'frequency' }))).toEqual(220);
|
||||
});
|
||||
it('should throw an error if value is not an object, number, or string', () => {
|
||||
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`);
|
||||
expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseNumeral', () => {
|
||||
it('should parse numbers as is', () => {
|
||||
expect(parseNumeral(4)).toBe(4);
|
||||
expect(parseNumeral(0)).toBe(0);
|
||||
expect(parseNumeral(20)).toBe(20);
|
||||
expect(parseNumeral('20')).toBe(20);
|
||||
expect(parseNumeral(1.5)).toBe(1.5);
|
||||
});
|
||||
it('should parse notes', () => {
|
||||
expect(parseNumeral('c4')).toBe(60);
|
||||
expect(parseNumeral('c#4')).toBe(61);
|
||||
expect(parseNumeral('db4')).toBe(61);
|
||||
});
|
||||
it('should throw an error for unknown strings', () => {
|
||||
expect(() => parseNumeral('xyz')).toThrowError('cannot parse as numeral: "xyz"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseFractional', () => {
|
||||
it('should parse numbers as is', () => {
|
||||
expect(parseFractional(4)).toBe(4);
|
||||
expect(parseFractional(0)).toBe(0);
|
||||
expect(parseFractional(20)).toBe(20);
|
||||
expect(parseFractional('20')).toBe(20);
|
||||
expect(parseFractional(1.5)).toBe(1.5);
|
||||
});
|
||||
it('should parse fractional shorthands values', () => {
|
||||
expect(parseFractional('w')).toBe(1);
|
||||
expect(parseFractional('h')).toBe(0.5);
|
||||
expect(parseFractional('q')).toBe(0.25);
|
||||
expect(parseFractional('e')).toBe(0.125);
|
||||
});
|
||||
it('should throw an error for unknown strings', () => {
|
||||
expect(() => parseFractional('xyz')).toThrowError('cannot parse as fractional: "xyz"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('numeralArgs', () => {
|
||||
it('should convert function arguments to numbers', () => {
|
||||
const add = numeralArgs((a, b) => a + b);
|
||||
expect(add('c4', 2)).toBe(62);
|
||||
});
|
||||
});
|
||||
describe('fractionalArgs', () => {
|
||||
it('should convert function arguments to numbers', () => {
|
||||
const add = fractionalArgs((a, b) => a + b);
|
||||
expect(add('q', 2)).toBe(2.25);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
let time;
|
||||
export function getTime() {
|
||||
if (!time) {
|
||||
throw new Error('no time set! use setTime to define a time source');
|
||||
}
|
||||
return time();
|
||||
}
|
||||
|
||||
export function setTime(func) {
|
||||
time = func;
|
||||
}
|
||||
@@ -18,6 +18,11 @@ export class TimeSpan {
|
||||
const end = this.end;
|
||||
const end_sam = end.sam();
|
||||
|
||||
// Support zero-width timespans
|
||||
if (begin.equals(end)) {
|
||||
return([new TimeSpan(begin, end)]);
|
||||
}
|
||||
|
||||
while (end.gt(begin)) {
|
||||
// If begin and end are in the same cycle, we're done.
|
||||
if (begin.sam().equals(end_sam)) {
|
||||
|
||||
@@ -4,19 +4,14 @@ 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 { Tone } from './tone.mjs';
|
||||
|
||||
export const hideHeader = () => {
|
||||
document.getElementById('header').style = 'display:none';
|
||||
};
|
||||
import { getTime } from './time.mjs';
|
||||
|
||||
function frame(callback) {
|
||||
if (window.strudelAnimation) {
|
||||
cancelAnimationFrame(window.strudelAnimation);
|
||||
}
|
||||
const animate = (animationTime) => {
|
||||
const toneTime = Tone.getTransport().seconds;
|
||||
callback(animationTime, toneTime);
|
||||
callback(animationTime, getTime());
|
||||
window.strudelAnimation = requestAnimationFrame(animate);
|
||||
};
|
||||
requestAnimationFrame(animate);
|
||||
@@ -51,6 +46,7 @@ export const cleanupUi = () => {
|
||||
const container = document.getElementById('code');
|
||||
if (container) {
|
||||
container.style = '';
|
||||
container.className = 'grow relative'; // has to match App.tsx
|
||||
// TODO: find a way to remove that duplication..
|
||||
container.className = 'grow flex text-gray-100 relative overflow-auto cursor-text pb-0'; // has to match App.tsx
|
||||
}
|
||||
};
|
||||
+60
-4
@@ -31,6 +31,9 @@ export const fromMidi = (n) => {
|
||||
return Math.pow(2, (n - 69) / 12) * 440;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||
*/
|
||||
export const getFreq = (noteOrMidi) => {
|
||||
if (typeof noteOrMidi === 'number') {
|
||||
return fromMidi(noteOrMidi);
|
||||
@@ -38,6 +41,9 @@ export const getFreq = (noteOrMidi) => {
|
||||
return fromMidi(toMidi(noteOrMidi));
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||
*/
|
||||
export const midi2note = (n) => {
|
||||
const oct = Math.floor(n / 12) - 1;
|
||||
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
|
||||
@@ -49,9 +55,13 @@ export const midi2note = (n) => {
|
||||
export const mod = (n, m) => ((n % m) + m) % m;
|
||||
|
||||
export const getPlayableNoteValue = (hap) => {
|
||||
let { value: note, context } = hap;
|
||||
let { value, context } = hap;
|
||||
let note = value;
|
||||
if (typeof note === 'object' && !Array.isArray(note)) {
|
||||
note = note.note || note.n || note.value;
|
||||
if (note === undefined) {
|
||||
throw new Error(`cannot find a playable note for ${JSON.stringify(value)}`);
|
||||
}
|
||||
}
|
||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||
@@ -67,15 +77,18 @@ export const getPlayableNoteValue = (hap) => {
|
||||
export const getFrequency = (hap) => {
|
||||
let { value, context } = hap;
|
||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||
if (typeof value === 'object' && value.freq) {
|
||||
return value.freq;
|
||||
if (typeof value === 'object') {
|
||||
if (value.freq) {
|
||||
return value.freq;
|
||||
}
|
||||
return getFreq(value.note || value.n || value.value);
|
||||
}
|
||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||
value = fromMidi(hap.value);
|
||||
} else if (typeof value === 'string' && isNote(value)) {
|
||||
value = fromMidi(toMidi(hap.value));
|
||||
} else if (typeof value !== 'number') {
|
||||
throw new Error('not a note or frequency:' + value);
|
||||
throw new Error('not a note or frequency: ' + value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
@@ -124,3 +137,46 @@ export function curry(func, overload) {
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
export function parseNumeral(numOrString) {
|
||||
const asNumber = Number(numOrString);
|
||||
if (!isNaN(asNumber)) {
|
||||
return asNumber;
|
||||
}
|
||||
if (isNote(numOrString)) {
|
||||
return toMidi(numOrString);
|
||||
}
|
||||
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
||||
}
|
||||
|
||||
export function mapArgs(fn, mapFn) {
|
||||
return (...args) => fn(...args.map(mapFn));
|
||||
}
|
||||
|
||||
export function numeralArgs(fn) {
|
||||
return mapArgs(fn, parseNumeral);
|
||||
}
|
||||
|
||||
export function parseFractional(numOrString) {
|
||||
const asNumber = Number(numOrString);
|
||||
if (!isNaN(asNumber)) {
|
||||
return asNumber;
|
||||
}
|
||||
const specialValue = {
|
||||
pi: Math.PI,
|
||||
w: 1,
|
||||
h: 0.5,
|
||||
q: 0.25,
|
||||
e: 0.125,
|
||||
s: 0.0625,
|
||||
t: 1 / 3,
|
||||
f: 0.2,
|
||||
x: 1 / 6,
|
||||
}[numOrString];
|
||||
if (typeof specialValue !== 'undefined') {
|
||||
return specialValue;
|
||||
}
|
||||
throw new Error(`cannot parse as fractional: "${numOrString}"`);
|
||||
}
|
||||
|
||||
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// will move to https://github.com/felixroos/zyklus
|
||||
// TODO: started flag
|
||||
|
||||
function createClock(
|
||||
getTime,
|
||||
callback, // called slightly before each cycle
|
||||
duration = 0.05, // duration of each cycle
|
||||
interval = 0.1, // interval between callbacks
|
||||
overlap = 0.1, // overlap between callbacks
|
||||
) {
|
||||
let tick = 0; // counts callbacks
|
||||
let phase = 0; // next callback time
|
||||
let precision = 10 ** 4; // used to round phase
|
||||
let minLatency = 0.01;
|
||||
const setDuration = (setter) => (duration = setter(duration));
|
||||
overlap = overlap || interval / 2;
|
||||
const onTick = () => {
|
||||
const t = getTime();
|
||||
const lookahead = t + interval + overlap; // the time window for this tick
|
||||
if (phase === 0) {
|
||||
phase = t + minLatency;
|
||||
}
|
||||
// callback as long as we're inside the lookahead
|
||||
while (phase < lookahead) {
|
||||
phase = Math.round(phase * precision) / precision;
|
||||
phase >= t && callback(phase, duration, tick);
|
||||
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
|
||||
phase += duration; // increment phase by duration
|
||||
tick++;
|
||||
}
|
||||
};
|
||||
let intervalID;
|
||||
const start = () => {
|
||||
clear(); // just in case start was called more than once
|
||||
onTick();
|
||||
intervalID = setInterval(onTick, interval * 1000);
|
||||
};
|
||||
const clear = () => intervalID !== undefined && clearInterval(intervalID);
|
||||
const pause = () => clear();
|
||||
const stop = () => {
|
||||
tick = 0;
|
||||
phase = 0;
|
||||
clear();
|
||||
};
|
||||
const getPhase = () => phase;
|
||||
// setCallback
|
||||
return { setDuration, start, stop, pause, duration, getPhase };
|
||||
}
|
||||
export default createClock;
|
||||
+16
-10
@@ -10,16 +10,22 @@ Either install with `npm i @strudel.cycles/embed` or just use a cdn to import th
|
||||
<script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
|
||||
<strudel-repl>
|
||||
<!--
|
||||
"a4 [a3 c3] a3 c3".color('#F9D649')
|
||||
.sub("<7 12 5 12>".slow(2))
|
||||
.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928"))
|
||||
.off(1/8,x=>x.add(12).color('#215CB6'))
|
||||
.slow(2)
|
||||
.legato(sine.range(0.3, 2).slow(28))
|
||||
.wave("sawtooth square".fast(2))
|
||||
.filter('lowpass', cosine.range(500,4000).slow(16))
|
||||
.out()
|
||||
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
|
||||
note(`[[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]`).slow(16)
|
||||
-->
|
||||
</strudel-repl>
|
||||
```
|
||||
|
||||
+16
-10
@@ -2,15 +2,21 @@
|
||||
<!-- <script src="./embed.js"></script> -->
|
||||
<strudel-repl>
|
||||
<!--
|
||||
"a4 [a3 c3] a3 c3".color('#F9D649')
|
||||
.sub("<7 12 5 12>".slow(2))
|
||||
.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928"))
|
||||
.off(1/8,x=>x.add(12).color('#215CB6'))
|
||||
.slow(2)
|
||||
.legato(sine.range(0.3, 2).slow(28))
|
||||
.wave("sawtooth square".fast(2))
|
||||
.filter('lowpass', cosine.range(500,4000).slow(16))
|
||||
.out()
|
||||
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
|
||||
note(`[[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]`).slow(16)
|
||||
-->
|
||||
</strudel-repl>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/embed",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
||||
"main": "embed.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -11,10 +11,9 @@ npm i @strudel.cycles/eval --save
|
||||
|
||||
## Example
|
||||
|
||||
<!-- TODO: -extend +evalScope -->
|
||||
|
||||
```js
|
||||
import { evaluate, extend } from '@strudel.cycles/eval';
|
||||
import { evalScope } from '@strudel.cycles/core';
|
||||
import { evaluate } from '@strudel.cycles/eval';
|
||||
|
||||
evalScope(
|
||||
import('@strudel.cycles/core'),
|
||||
|
||||
@@ -4,46 +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 as _evaluate } from '@strudel.cycles/core';
|
||||
import shapeshifter from './shapeshifter.mjs';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
const { isPattern, Pattern } = strudel;
|
||||
|
||||
export const extend = (...args) => {
|
||||
console.warn('@strudel.cycles/eval extend is deprecated, please use evalScope instead');
|
||||
Object.assign(globalThis, ...args);
|
||||
};
|
||||
|
||||
let scoped = false;
|
||||
export const evalScope = async (...args) => {
|
||||
if (scoped) {
|
||||
console.warn('@strudel.cycles/eval evalScope was called more than once.');
|
||||
}
|
||||
scoped = true;
|
||||
const results = await Promise.allSettled(args);
|
||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||
results.forEach((result, i) => {
|
||||
if (result.status === 'rejected') {
|
||||
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
|
||||
}
|
||||
});
|
||||
Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap());
|
||||
};
|
||||
|
||||
function safeEval(str) {
|
||||
return Function('"use strict";return (' + str + ')')();
|
||||
}
|
||||
|
||||
export const evaluate = async (code) => {
|
||||
if (!scoped) {
|
||||
await evalScope(); // at least scope Pattern.prototype.boostrap
|
||||
}
|
||||
const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code
|
||||
let evaluated = await safeEval(shapeshifted);
|
||||
if (!isPattern(evaluated)) {
|
||||
console.log('evaluated', evaluated);
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||
}
|
||||
return { mode: 'javascript', pattern: evaluated };
|
||||
return _evaluate(code, shapeshifter);
|
||||
};
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.2.0",
|
||||
"version": "0.4.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.2.0",
|
||||
"version": "0.4.0",
|
||||
"description": "Code evaluator for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -28,12 +28,12 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@strudel.cycles/core": "^0.4.0",
|
||||
"estraverse": "^5.3.0",
|
||||
"shift-ast": "^6.1.0",
|
||||
"shift-codegen": "^7.0.3",
|
||||
"shift-parser": "^7.0.3",
|
||||
"shift-spec": "^2018.0.2",
|
||||
"shift-ast": "^7.0.0",
|
||||
"shift-codegen": "^8.1.0",
|
||||
"shift-parser": "^8.0.0",
|
||||
"shift-spec": "^2019.0.0",
|
||||
"shift-traverser": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
|
||||
const addLocations = true;
|
||||
export const addMiniLocations = true;
|
||||
export const minifyStrings = true;
|
||||
export const wrappedAsync = true;
|
||||
export const wrappedAsync = false; // this is now handled by core evaluate by default
|
||||
export const shouldAddReturn = true;
|
||||
|
||||
export default (_code) => {
|
||||
const { code, addReturn } = wrapAsync(_code);
|
||||
@@ -125,7 +126,7 @@ export default (_code) => {
|
||||
},
|
||||
});
|
||||
// add return to last statement (because it's wrapped in an async function artificially)
|
||||
if (wrappedAsync) {
|
||||
if (shouldAddReturn) {
|
||||
addReturn(shifted);
|
||||
}
|
||||
const generated = undisguiseImports(codegen(shifted));
|
||||
@@ -153,7 +154,7 @@ ${code}
|
||||
})()`;
|
||||
}
|
||||
const addReturn = (ast) => {
|
||||
const body = ast.statements[0].expression.callee.body; // actual code ast inside async function body
|
||||
const body = wrappedAsync ? ast.statements[0].expression.callee.body : ast;
|
||||
body.statements = body.statements
|
||||
.slice(0, -1)
|
||||
.concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]);
|
||||
|
||||
@@ -6,10 +6,10 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import { expect, describe, it } from 'vitest';
|
||||
|
||||
import { evaluate, evalScope } from '../evaluate.mjs';
|
||||
import { evaluate } from '../evaluate.mjs';
|
||||
import { mini } from '@strudel.cycles/mini';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
const { fastcat } = strudel;
|
||||
const { fastcat, evalScope } = strudel;
|
||||
|
||||
describe('evaluate', async () => {
|
||||
await evalScope({ mini }, strudel);
|
||||
|
||||
@@ -5,15 +5,21 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import shapeshifter from '../shapeshifter.mjs';
|
||||
import shapeshifter, { wrappedAsync } from '../shapeshifter.mjs';
|
||||
|
||||
describe('shapeshifter', () => {
|
||||
it('Should shift simple double quote string', () => {
|
||||
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
|
||||
});
|
||||
it('Should handle dynamic imports', () => {
|
||||
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
|
||||
'(async()=>{const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])})()',
|
||||
);
|
||||
if (wrappedAsync) {
|
||||
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
|
||||
} else {
|
||||
expect(shapeshifter('"c3"')).toEqual('return mini("c3").withMiniLocation([1,0,0],[1,4,4])');
|
||||
}
|
||||
});
|
||||
if (wrappedAsync) {
|
||||
it('Should handle dynamic imports', () => {
|
||||
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
|
||||
'const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])',
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+70
-48
@@ -4,15 +4,19 @@ 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 _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern } from '@strudel.cycles/core';
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
import * as _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern, isNote, getPlayableNoteValue, logger } 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;
|
||||
export const { WebMidi } = _WebMidi;
|
||||
|
||||
export function enableWebMidi() {
|
||||
export function enableWebMidi(options = {}) {
|
||||
const { onReady, onConnected, onDisconnected } = options;
|
||||
|
||||
if (typeof navigator.requestMIDIAccess !== 'function') {
|
||||
throw new Error('Your Browser does not support WebMIDI.');
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
if (WebMidi.enabled) {
|
||||
// if already enabled, just resolve WebMidi
|
||||
@@ -23,6 +27,14 @@ export function enableWebMidi() {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
WebMidi.addListener('connected', (e) => {
|
||||
onConnected?.(WebMidi);
|
||||
});
|
||||
// Reacting when a device becomes unavailable
|
||||
WebMidi.addListener('disconnected', (e) => {
|
||||
onDisconnected?.(WebMidi, e);
|
||||
});
|
||||
onReady?.(WebMidi);
|
||||
resolve(WebMidi);
|
||||
});
|
||||
});
|
||||
@@ -31,7 +43,21 @@ export function enableWebMidi() {
|
||||
const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||
|
||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||
Pattern.prototype.midi = function (output, channel = 1) {
|
||||
Pattern.prototype.midi = async function (output, channel = 1) {
|
||||
await enableWebMidi({
|
||||
onConnected: ({ outputs }) =>
|
||||
logger(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
||||
onDisconnected: ({ outputs }) =>
|
||||
logger(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
||||
onReady: ({ outputs }) => {
|
||||
const chosenOutput = output ?? outputs[0];
|
||||
const otherOutputs = outputs
|
||||
.filter((o) => o.name !== chosenOutput.name)
|
||||
.map((o) => `'${o.name}'`)
|
||||
.join(' | ');
|
||||
logger(`Midi connected! Using "${chosenOutput.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
|
||||
},
|
||||
});
|
||||
if (isPattern(output?.constructor?.name)) {
|
||||
throw new Error(
|
||||
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
||||
@@ -39,46 +65,42 @@ Pattern.prototype.midi = function (output, channel = 1) {
|
||||
}')`,
|
||||
);
|
||||
}
|
||||
return this._withHap((hap) => {
|
||||
// const onTrigger = (time: number, hap: any) => {
|
||||
const onTrigger = (time, hap) => {
|
||||
let note = hap.value;
|
||||
const velocity = hap.context?.velocity ?? 0.9;
|
||||
if (!isNote(note)) {
|
||||
throw new Error('not a note: ' + note);
|
||||
}
|
||||
if (!WebMidi.enabled) {
|
||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||
}
|
||||
if (!WebMidi.outputs.length) {
|
||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||
}
|
||||
let device;
|
||||
if (typeof output === 'number') {
|
||||
device = WebMidi.outputs[output];
|
||||
} else if (typeof output === 'string') {
|
||||
device = outputByName(output);
|
||||
} else {
|
||||
device = WebMidi.outputs[0];
|
||||
}
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||
.map((o) => `'${o.name}'`)
|
||||
.join(' | ')}`,
|
||||
);
|
||||
}
|
||||
// console.log('midi', value, output);
|
||||
const timingOffset = WebMidi.time - Tone.getContext().currentTime * 1000;
|
||||
time = time * 1000 + timingOffset;
|
||||
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
||||
// await enableWebMidi()
|
||||
device.playNote(note, channel, {
|
||||
time,
|
||||
duration: hap.duration.valueOf() * 1000 - 5,
|
||||
velocity,
|
||||
});
|
||||
};
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
return this.onTrigger((time, hap) => {
|
||||
let note = getPlayableNoteValue(hap);
|
||||
const velocity = hap.context?.velocity ?? 0.9;
|
||||
if (!isNote(note)) {
|
||||
throw new Error('not a note: ' + note);
|
||||
}
|
||||
if (!WebMidi.enabled) {
|
||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||
}
|
||||
if (!WebMidi.outputs.length) {
|
||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||
}
|
||||
let device;
|
||||
if (typeof output === 'number') {
|
||||
device = WebMidi.outputs[output];
|
||||
} else if (typeof output === 'string') {
|
||||
device = outputByName(output);
|
||||
} else {
|
||||
device = WebMidi.outputs[0];
|
||||
}
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||
.map((o) => `'${o.name}'`)
|
||||
.join(' | ')}`,
|
||||
);
|
||||
}
|
||||
// console.log('midi', value, output);
|
||||
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
|
||||
time = time * 1000 + timingOffset;
|
||||
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
||||
// await enableWebMidi()
|
||||
device.playNote(note, channel, {
|
||||
time,
|
||||
duration: hap.duration.valueOf() * 1000 - 5,
|
||||
attack: velocity,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.2.0",
|
||||
"version": "0.4.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.2.0",
|
||||
"version": "0.4.0",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"repository": {
|
||||
@@ -21,8 +21,8 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/tone": "^0.2.0",
|
||||
"@strudel.cycles/tone": "^0.4.0",
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^2.5.2"
|
||||
"webmidi": "^3.0.21"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,3 +33,12 @@ yields:
|
||||
## Mini Notation API
|
||||
|
||||
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
|
||||
|
||||
## Building the Parser
|
||||
|
||||
The parser [krill-parser.js] is generated from [krill.pegjs](./krill.pegjs) using [peggy](https://peggyjs.org/).
|
||||
To generate the parser, run
|
||||
|
||||
```js
|
||||
npm run build:parser
|
||||
```
|
||||
|
||||
@@ -32,7 +32,7 @@ function peg$padEnd(str, targetLength, padString) {
|
||||
}
|
||||
|
||||
peg$SyntaxError.prototype.format = function(sources) {
|
||||
var str = "Error: " + this.message;
|
||||
var str = "peg error: " + this.message;
|
||||
if (this.location) {
|
||||
var src = null;
|
||||
var k;
|
||||
|
||||
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import * as krill from './krill-parser.js';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs';
|
||||
// import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs';
|
||||
|
||||
const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel;
|
||||
|
||||
@@ -29,7 +29,10 @@ const applyOptions = (parent) => (pat, i) => {
|
||||
case 'bjorklund':
|
||||
return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation);
|
||||
case 'degradeBy':
|
||||
return reify(pat)._degradeByWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), operator.arguments_.amount);
|
||||
return reify(pat)._degradeByWith(
|
||||
strudel.rand.early(randOffset * _nextSeed()).segment(1),
|
||||
operator.arguments_.amount,
|
||||
);
|
||||
// TODO: case 'fixed-step': "%"
|
||||
}
|
||||
console.warn(`operator "${operator.type_}" not implemented`);
|
||||
@@ -112,9 +115,9 @@ export function patternifyAST(ast) {
|
||||
return silence;
|
||||
}
|
||||
if (typeof ast.source_ !== 'object') {
|
||||
if (!addMiniLocations) {
|
||||
/* if (!addMiniLocations) {
|
||||
return ast.source_;
|
||||
}
|
||||
} */
|
||||
if (!ast.location_) {
|
||||
console.warn('no location for', ast);
|
||||
return ast.source_;
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.2.0",
|
||||
"version": "0.4.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.2.0",
|
||||
"version": "0.4.0",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -26,9 +26,9 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@strudel.cycles/eval": "^0.2.0",
|
||||
"@strudel.cycles/tone": "^0.2.0"
|
||||
"@strudel.cycles/core": "^0.4.0",
|
||||
"@strudel.cycles/eval": "^0.4.0",
|
||||
"@strudel.cycles/tone": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"peggy": "^2.0.1"
|
||||
|
||||
@@ -35,3 +35,5 @@ s("<bd sd> hh").osc()
|
||||
```
|
||||
|
||||
or just [click here](http://localhost:3000/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
|
||||
|
||||
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.tidalcycles.org/tutorial/#superdirt-api)
|
||||
|
||||
+47
-22
@@ -5,10 +5,34 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import OSC from 'osc-js';
|
||||
import { Pattern } from '@strudel.cycles/core';
|
||||
import { logger, parseNumeral, Pattern } from '@strudel.cycles/core';
|
||||
|
||||
let connection; // Promise<OSC>
|
||||
function connect() {
|
||||
if (!connection) {
|
||||
// make sure this runs only once
|
||||
connection = new Promise((resolve, reject) => {
|
||||
const osc = new OSC();
|
||||
osc.open();
|
||||
osc.on('open', () => {
|
||||
const url = osc.options?.plugin?.socket?.url;
|
||||
logger(`[osc] connected${url ? ` to ${url}` : ''}`);
|
||||
resolve(osc);
|
||||
});
|
||||
osc.on('close', () => {
|
||||
connection = undefined; // allows new connection afterwards
|
||||
console.log('[osc] disconnected');
|
||||
reject('OSC connection closed');
|
||||
});
|
||||
osc.on('error', (err) => reject(err));
|
||||
}).catch((err) => {
|
||||
connection = undefined;
|
||||
throw new Error('Could not connect to OSC server. Is it running?');
|
||||
});
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
const comm = new OSC();
|
||||
comm.open();
|
||||
const latency = 0.1;
|
||||
let startedAt = -1;
|
||||
|
||||
@@ -20,24 +44,25 @@ let startedAt = -1;
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
*/
|
||||
Pattern.prototype.osc = function () {
|
||||
return this._withHap((hap) => {
|
||||
const onTrigger = (time, hap, currentTime, cps) => {
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
// time should be audio time of onset
|
||||
// currentTime should be current time of audio context (slightly before time)
|
||||
if (startedAt < 0) {
|
||||
startedAt = Date.now() - currentTime * 1000;
|
||||
}
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
const ts = Math.floor(startedAt + (time + latency) * 1000);
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
const bundle = new OSC.Bundle([message], ts);
|
||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||
comm.send(bundle);
|
||||
};
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
Pattern.prototype.osc = async function () {
|
||||
const osc = await connect();
|
||||
return this.onTrigger((time, hap, currentTime, cps = 1) => {
|
||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||
const delta = hap.duration.valueOf();
|
||||
// time should be audio time of onset
|
||||
// currentTime should be current time of audio context (slightly before time)
|
||||
if (startedAt < 0) {
|
||||
startedAt = Date.now() - currentTime * 1000;
|
||||
}
|
||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
||||
// make sure n and note are numbers
|
||||
controls.n && (controls.n = parseNumeral(controls.n));
|
||||
controls.note && (controls.note = parseNumeral(controls.note));
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
const ts = Math.floor(startedAt + (time + latency) * 1000);
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
const bundle = new OSC.Bundle([message], ts);
|
||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||
osc.send(bundle);
|
||||
});
|
||||
};
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.1.1",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.1.1",
|
||||
"version": "0.3.0",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"scripts": {
|
||||
@@ -30,6 +30,6 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"osc-js": "^2.3.2"
|
||||
"osc-js": "^2.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
examples
|
||||
@@ -1,4 +1,43 @@
|
||||
# @strudel.cycles/react
|
||||
|
||||
This package contains react hooks and components for strudel.
|
||||
Example coming soon
|
||||
This package contains react hooks and components for strudel. It is used internally by the Strudel REPL.
|
||||
|
||||
## Install
|
||||
|
||||
```js
|
||||
npm i @strudel.cycles/react
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Here is a minimal example of how to set up a MiniRepl:
|
||||
|
||||
```jsx
|
||||
import { evalScope, controls } from '@strudel.cycles/core';
|
||||
import { MiniRepl } from '@strudel.cycles/react';
|
||||
import { prebake } from '../repl/src/prebake.mjs';
|
||||
|
||||
evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
/* probably import other strudel packages */
|
||||
);
|
||||
|
||||
prebake();
|
||||
|
||||
export function Repl({ tune }) {
|
||||
return <MiniRepl tune={tune} hideOutsideView={true} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
If you change something in here and want to see the changes in the repl, make sure to run `npm run build` inside this folder!
|
||||
|
||||
```js
|
||||
npm run dev # dev server
|
||||
npm run build # build package
|
||||
```
|
||||
|
||||
Vendored
+1
-3
File diff suppressed because one or more lines are too long
Vendored
+254
-538
@@ -1,594 +1,310 @@
|
||||
import React, { useCallback, useState, useEffect, useMemo, useRef, useLayoutEffect } from 'react';
|
||||
import _CodeMirror from '@uiw/react-codemirror';
|
||||
import { Decoration, EditorView } from '@codemirror/view';
|
||||
import { StateEffect, StateField } from '@codemirror/state';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { tags } from '@lezer/highlight';
|
||||
import { createTheme } from '@uiw/codemirror-themes';
|
||||
import { useInView } from 'react-hook-inview';
|
||||
import { evaluate } from '@strudel.cycles/eval';
|
||||
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
import { TimeSpan, State } from '@strudel.cycles/core';
|
||||
import { WebMidi, enableWebMidi } from '@strudel.cycles/midi';
|
||||
|
||||
var strudelTheme = createTheme({
|
||||
theme: 'dark',
|
||||
import n, { useCallback as N, useRef as x, useEffect as R, useState as w, useMemo as I, useLayoutEffect as K } from "react";
|
||||
import Q from "@uiw/react-codemirror";
|
||||
import { Decoration as E, EditorView as O } from "@codemirror/view";
|
||||
import { StateEffect as j, StateField as U } from "@codemirror/state";
|
||||
import { javascript as W } from "@codemirror/lang-javascript";
|
||||
import { tags as r } from "@lezer/highlight";
|
||||
import { createTheme as X } from "@uiw/codemirror-themes";
|
||||
import { useInView as Y } from "react-hook-inview";
|
||||
import { webaudioOutput as Z, getAudioContext as ee } from "@strudel.cycles/webaudio";
|
||||
import { repl as te } from "@strudel.cycles/core";
|
||||
import { transpiler as re } from "@strudel.cycles/transpiler";
|
||||
const oe = X({
|
||||
theme: "dark",
|
||||
settings: {
|
||||
background: '#222',
|
||||
foreground: '#75baff', // whats that?
|
||||
caret: '#ffcc00',
|
||||
selection: 'rgba(128, 203, 196, 0.5)',
|
||||
selectionMatch: '#036dd626',
|
||||
lineHighlight: '#8a91991a',
|
||||
gutterBackground: 'transparent',
|
||||
// gutterForeground: '#8a919966',
|
||||
gutterForeground: '#676e95',
|
||||
background: "#222",
|
||||
foreground: "#75baff",
|
||||
caret: "#ffcc00",
|
||||
selection: "rgba(128, 203, 196, 0.5)",
|
||||
selectionMatch: "#036dd626",
|
||||
lineHighlight: "#00000050",
|
||||
gutterBackground: "transparent",
|
||||
gutterForeground: "#8a919966"
|
||||
},
|
||||
styles: [
|
||||
{ tag: tags.keyword, color: '#c792ea' },
|
||||
{ tag: tags.operator, color: '#89ddff' },
|
||||
{ tag: tags.special(tags.variableName), color: '#eeffff' },
|
||||
{ tag: tags.typeName, color: '#f07178' },
|
||||
{ tag: tags.atom, color: '#f78c6c' },
|
||||
{ tag: tags.number, color: '#ff5370' },
|
||||
{ tag: tags.definition(tags.variableName), color: '#82aaff' },
|
||||
{ tag: tags.string, color: '#c3e88d' },
|
||||
{ tag: tags.special(tags.string), color: '#f07178' },
|
||||
{ tag: tags.comment, color: '#7d8799' },
|
||||
{ tag: tags.variableName, color: '#f07178' },
|
||||
{ tag: tags.tagName, color: '#ff5370' },
|
||||
{ tag: tags.bracket, color: '#a2a1a4' },
|
||||
{ tag: tags.meta, color: '#ffcb6b' },
|
||||
{ tag: tags.attributeName, color: '#c792ea' },
|
||||
{ tag: tags.propertyName, color: '#c792ea' },
|
||||
{ tag: tags.className, color: '#decb6b' },
|
||||
{ tag: tags.invalid, color: '#ffffff' },
|
||||
],
|
||||
{ tag: r.keyword, color: "#c792ea" },
|
||||
{ tag: r.operator, color: "#89ddff" },
|
||||
{ tag: r.special(r.variableName), color: "#eeffff" },
|
||||
{ tag: r.typeName, color: "#c3e88d" },
|
||||
{ tag: r.atom, color: "#f78c6c" },
|
||||
{ tag: r.number, color: "#c3e88d" },
|
||||
{ tag: r.definition(r.variableName), color: "#82aaff" },
|
||||
{ tag: r.string, color: "#c3e88d" },
|
||||
{ tag: r.special(r.string), color: "#c3e88d" },
|
||||
{ tag: r.comment, color: "#7d8799" },
|
||||
{ tag: r.variableName, color: "#c792ea" },
|
||||
{ tag: r.tagName, color: "#c3e88d" },
|
||||
{ tag: r.bracket, color: "#525154" },
|
||||
{ tag: r.meta, color: "#ffcb6b" },
|
||||
{ tag: r.attributeName, color: "#c792ea" },
|
||||
{ tag: r.propertyName, color: "#c792ea" },
|
||||
{ tag: r.className, color: "#decb6b" },
|
||||
{ tag: r.invalid, color: "#ffffff" }
|
||||
]
|
||||
});
|
||||
|
||||
var style = '';
|
||||
|
||||
const setFlash = StateEffect.define();
|
||||
const flashField = StateField.define({
|
||||
const T = j.define(), ne = U.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
return E.none;
|
||||
},
|
||||
update(flash2, tr) {
|
||||
update(e, t) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setFlash)) {
|
||||
if (e.value) {
|
||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||
flash2 = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||
} else {
|
||||
flash2 = Decoration.set([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flash2;
|
||||
} catch (err) {
|
||||
console.warn("flash error", err);
|
||||
return flash2;
|
||||
for (let o of t.effects)
|
||||
if (o.is(T))
|
||||
if (o.value) {
|
||||
const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } });
|
||||
e = E.set([a.range(0, t.newDoc.length)]);
|
||||
} else
|
||||
e = E.set([]);
|
||||
return e;
|
||||
} catch (o) {
|
||||
return console.warn("flash error", o), e;
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f)
|
||||
});
|
||||
const flash = (view) => {
|
||||
view.dispatch({ effects: setFlash.of(true) });
|
||||
setTimeout(() => {
|
||||
view.dispatch({ effects: setFlash.of(false) });
|
||||
provide: (e) => O.decorations.from(e)
|
||||
}), ae = (e) => {
|
||||
e.dispatch({ effects: T.of(!0) }), setTimeout(() => {
|
||||
e.dispatch({ effects: T.of(!1) });
|
||||
}, 200);
|
||||
};
|
||||
const setHighlights = StateEffect.define();
|
||||
const highlightField = StateField.define({
|
||||
}, A = j.define(), se = U.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
return E.none;
|
||||
},
|
||||
update(highlights, tr) {
|
||||
update(e, t) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setHighlights)) {
|
||||
const marks = e.value.map(
|
||||
(hap) => (hap.context.locations || []).map(({ start, end }) => {
|
||||
const color = hap.context.color || "#FFCA28";
|
||||
let from = tr.newDoc.line(start.line).from + start.column;
|
||||
let to = tr.newDoc.line(end.line).from + end.column;
|
||||
const l = tr.newDoc.length;
|
||||
if (from > l || to > l) {
|
||||
return;
|
||||
}
|
||||
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
|
||||
return mark.range(from, to);
|
||||
for (let o of t.effects)
|
||||
if (o.is(A)) {
|
||||
const a = o.value.map(
|
||||
(s) => (s.context.locations || []).map(({ start: m, end: l }) => {
|
||||
const d = s.context.color || "#FFCA28";
|
||||
let c = t.newDoc.line(m.line).from + m.column, i = t.newDoc.line(l.line).from + l.column;
|
||||
const g = t.newDoc.length;
|
||||
return c > g || i > g ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${d};` } }).range(c, i);
|
||||
})
|
||||
).flat().filter(Boolean) || [];
|
||||
highlights = Decoration.set(marks, true);
|
||||
e = E.set(a, !0);
|
||||
}
|
||||
}
|
||||
return highlights;
|
||||
} catch (err) {
|
||||
return Decoration.set([]);
|
||||
return e;
|
||||
} catch {
|
||||
return E.set([]);
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f)
|
||||
});
|
||||
const extensions = [javascript(), strudelTheme, highlightField, flashField];
|
||||
function CodeMirror({ value, onChange, onViewChanged, onSelectionChange, options, editorDidMount }) {
|
||||
const handleOnChange = useCallback(
|
||||
(value2) => {
|
||||
onChange?.(value2);
|
||||
provide: (e) => O.decorations.from(e)
|
||||
}), ce = [W(), oe, se, ne];
|
||||
function ie({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: m }) {
|
||||
const l = N(
|
||||
(i) => {
|
||||
t?.(i);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
const handleOnCreateEditor = useCallback(
|
||||
(view) => {
|
||||
onViewChanged?.(view);
|
||||
[t]
|
||||
), d = N(
|
||||
(i) => {
|
||||
o?.(i);
|
||||
},
|
||||
[onViewChanged]
|
||||
);
|
||||
const handleOnUpdate = useCallback(
|
||||
(viewUpdate) => {
|
||||
if (viewUpdate.selectionSet && onSelectionChange) {
|
||||
onSelectionChange?.(viewUpdate.state.selection);
|
||||
}
|
||||
[o]
|
||||
), c = N(
|
||||
(i) => {
|
||||
i.selectionSet && a && a?.(i.state.selection);
|
||||
},
|
||||
[onSelectionChange]
|
||||
[a]
|
||||
);
|
||||
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(_CodeMirror, {
|
||||
value,
|
||||
onChange: handleOnChange,
|
||||
onCreateEditor: handleOnCreateEditor,
|
||||
onUpdate: handleOnUpdate,
|
||||
extensions
|
||||
return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(Q, {
|
||||
value: e,
|
||||
onChange: l,
|
||||
onCreateEditor: d,
|
||||
onUpdate: c,
|
||||
extensions: ce
|
||||
}));
|
||||
}
|
||||
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
/* 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,
|
||||
};
|
||||
function B(...e) {
|
||||
return e.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
/*
|
||||
usePostMessage.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/usePostMessage.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/>.
|
||||
*/
|
||||
|
||||
function usePostMessage(listener) {
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', listener);
|
||||
return () => window.removeEventListener('message', listener);
|
||||
}, [listener]);
|
||||
return useCallback((data) => window.postMessage(data, '*'), []);
|
||||
}
|
||||
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
let s4 = () => {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
};
|
||||
const generateHash = (code) => encodeURIComponent(btoa(code));
|
||||
|
||||
function useRepl({ tune, defaultSynth, 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, velocity } = event.context;
|
||||
if (!onTrigger) {
|
||||
if (defaultSynth) {
|
||||
const note = getPlayableNoteValue(event);
|
||||
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
||||
} else {
|
||||
throw new Error('no defaultSynth passed to useRepl.');
|
||||
}
|
||||
/* console.warn('no instrument chosen', event);
|
||||
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
|
||||
} else {
|
||||
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, defaultSynth],
|
||||
),
|
||||
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), []),
|
||||
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) ;
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
cx.js - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/cx.js>
|
||||
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/>.
|
||||
*/
|
||||
|
||||
function cx(...classes) {
|
||||
// : Array<string | undefined>
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
let highlights = []; // actively highlighted events
|
||||
let lastEnd;
|
||||
|
||||
function useHighlighting({ view, pattern, active }) {
|
||||
useEffect(() => {
|
||||
if (view) {
|
||||
if (pattern && active) {
|
||||
let frame = requestAnimationFrame(updateHighlights);
|
||||
|
||||
function updateHighlights() {
|
||||
function le({ view: e, pattern: t, active: o, getTime: a }) {
|
||||
const s = x([]), m = x();
|
||||
R(() => {
|
||||
if (e)
|
||||
if (t && o) {
|
||||
let d = function() {
|
||||
try {
|
||||
const audioTime = Tone.getTransport().seconds;
|
||||
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
|
||||
// see https://github.com/tidalcycles/strudel/issues/108
|
||||
const begin = Math.max(lastEnd || audioTime, audioTime - 1 / 10);
|
||||
const span = [begin, audioTime + 1 / 60];
|
||||
lastEnd = audioTime + 1 / 60;
|
||||
highlights = highlights.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
|
||||
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
||||
highlights = highlights.concat(haps); // add potential new onsets
|
||||
view.dispatch({ effects: setHighlights.of(highlights) }); // highlight all still active + new active haps
|
||||
} catch (err) {
|
||||
// console.log('error in updateHighlights', err);
|
||||
view.dispatch({ effects: setHighlights.of([]) });
|
||||
const c = a(), g = [Math.max(m.current || c, c - 1 / 10, 0), c + 1 / 60];
|
||||
m.current = g[1], s.current = s.current.filter((p) => p.whole.end > c);
|
||||
const v = t.queryArc(...g).filter((p) => p.hasOnset());
|
||||
s.current = s.current.concat(v), e.dispatch({ effects: A.of(s.current) });
|
||||
} catch {
|
||||
e.dispatch({ effects: A.of([]) });
|
||||
}
|
||||
frame = requestAnimationFrame(updateHighlights);
|
||||
}
|
||||
|
||||
l = requestAnimationFrame(d);
|
||||
}, l = requestAnimationFrame(d);
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
cancelAnimationFrame(l);
|
||||
};
|
||||
} else {
|
||||
highlights = [];
|
||||
view.dispatch({ effects: setHighlights.of([]) });
|
||||
}
|
||||
}
|
||||
}, [pattern, active, view]);
|
||||
} else
|
||||
s.current = [], e.dispatch({ effects: A.of([]) });
|
||||
}, [t, o, e]);
|
||||
}
|
||||
|
||||
var tailwind = '';
|
||||
|
||||
const container = "_container_3i85k_1";
|
||||
const header = "_header_3i85k_5";
|
||||
const buttons = "_buttons_3i85k_9";
|
||||
const button = "_button_3i85k_9";
|
||||
const buttonDisabled = "_buttonDisabled_3i85k_17";
|
||||
const error = "_error_3i85k_21";
|
||||
const body = "_body_3i85k_25";
|
||||
var styles = {
|
||||
container: container,
|
||||
header: header,
|
||||
buttons: buttons,
|
||||
button: button,
|
||||
buttonDisabled: buttonDisabled,
|
||||
error: error,
|
||||
body: body
|
||||
const de = "_container_3i85k_1", ue = "_header_3i85k_5", fe = "_buttons_3i85k_9", me = "_button_3i85k_9", ge = "_buttonDisabled_3i85k_17", pe = "_error_3i85k_21", he = "_body_3i85k_25", b = {
|
||||
container: de,
|
||||
header: ue,
|
||||
buttons: fe,
|
||||
button: me,
|
||||
buttonDisabled: ge,
|
||||
error: pe,
|
||||
body: he
|
||||
};
|
||||
|
||||
function Icon({ type }) {
|
||||
return /* @__PURE__ */ React.createElement("svg", {
|
||||
function q({ type: e }) {
|
||||
return /* @__PURE__ */ n.createElement("svg", {
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
className: "sc-h-5 sc-w-5",
|
||||
viewBox: "0 0 20 20",
|
||||
fill: "currentColor"
|
||||
}, {
|
||||
refresh: /* @__PURE__ */ React.createElement("path", {
|
||||
refresh: /* @__PURE__ */ n.createElement("path", {
|
||||
fillRule: "evenodd",
|
||||
d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",
|
||||
clipRule: "evenodd"
|
||||
}),
|
||||
play: /* @__PURE__ */ React.createElement("path", {
|
||||
play: /* @__PURE__ */ n.createElement("path", {
|
||||
fillRule: "evenodd",
|
||||
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",
|
||||
clipRule: "evenodd"
|
||||
}),
|
||||
pause: /* @__PURE__ */ React.createElement("path", {
|
||||
pause: /* @__PURE__ */ n.createElement("path", {
|
||||
fillRule: "evenodd",
|
||||
d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",
|
||||
clipRule: "evenodd"
|
||||
})
|
||||
}[type]);
|
||||
}[e]);
|
||||
}
|
||||
|
||||
function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) {
|
||||
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } = useRepl({
|
||||
tune,
|
||||
defaultSynth,
|
||||
autolink: false,
|
||||
onEvent
|
||||
});
|
||||
useEffect(() => {
|
||||
init && evaluateOnly();
|
||||
}, [tune, init]);
|
||||
const [view, setView] = useState();
|
||||
const [ref, isVisible] = useInView({
|
||||
function ve({
|
||||
defaultOutput: e,
|
||||
interval: t,
|
||||
getTime: o,
|
||||
evalOnMount: a = !1,
|
||||
initialCode: s = "",
|
||||
autolink: m = !1,
|
||||
beforeEval: l,
|
||||
afterEval: d,
|
||||
onEvalError: c,
|
||||
onToggle: i
|
||||
}) {
|
||||
const [g, v] = w(), [p, D] = w(), [h, k] = w(s), [y, P] = w(h), [z, _] = w(), [C, H] = w(!1), F = h !== y, { scheduler: u, evaluate: L, start: $, stop: G, pause: J } = I(
|
||||
() => te({
|
||||
interval: t,
|
||||
defaultOutput: e,
|
||||
onSchedulerError: v,
|
||||
onEvalError: (f) => {
|
||||
D(f), c?.(f);
|
||||
},
|
||||
getTime: o,
|
||||
transpiler: re,
|
||||
beforeEval: ({ code: f }) => {
|
||||
k(f), l?.();
|
||||
},
|
||||
afterEval: ({ pattern: f, code: S }) => {
|
||||
P(S), _(f), D(), v(), m && (window.location.hash = "#" + encodeURIComponent(btoa(S))), d?.();
|
||||
},
|
||||
onToggle: (f) => {
|
||||
H(f), i?.(f);
|
||||
}
|
||||
}),
|
||||
[e, t, o]
|
||||
), M = N(async (f = !0) => L(h, f), [L, h]), V = x();
|
||||
return R(() => {
|
||||
!V.current && a && h && (V.current = !0, M());
|
||||
}, [M, a, h]), R(() => () => {
|
||||
u.stop();
|
||||
}, [u]), {
|
||||
code: h,
|
||||
setCode: k,
|
||||
error: g || p,
|
||||
schedulerError: g,
|
||||
scheduler: u,
|
||||
evalError: p,
|
||||
evaluate: L,
|
||||
activateCode: M,
|
||||
activeCode: y,
|
||||
isDirty: F,
|
||||
pattern: z,
|
||||
started: C,
|
||||
start: $,
|
||||
stop: G,
|
||||
pause: J,
|
||||
togglePlay: async () => {
|
||||
C ? u.pause() : await M();
|
||||
}
|
||||
};
|
||||
}
|
||||
const be = () => ee().currentTime;
|
||||
function Pe({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) {
|
||||
const {
|
||||
code: s,
|
||||
setCode: m,
|
||||
evaluate: l,
|
||||
activateCode: d,
|
||||
error: c,
|
||||
isDirty: i,
|
||||
activeCode: g,
|
||||
pattern: v,
|
||||
started: p,
|
||||
scheduler: D,
|
||||
togglePlay: h,
|
||||
stop: k
|
||||
} = ve({
|
||||
initialCode: e,
|
||||
defaultOutput: Z,
|
||||
getTime: be
|
||||
}), [y, P] = w(), [z, _] = Y({
|
||||
threshold: 0.01
|
||||
});
|
||||
const wasVisible = useRef();
|
||||
const show = useMemo(() => {
|
||||
if (isVisible || !hideOutsideView) {
|
||||
wasVisible.current = true;
|
||||
}
|
||||
return isVisible || wasVisible.current;
|
||||
}, [isVisible, hideOutsideView]);
|
||||
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes("strudel disable-highlighting") });
|
||||
useLayoutEffect(() => {
|
||||
if (enableKeyboard) {
|
||||
const handleKeyPress = async (e) => {
|
||||
if (e.ctrlKey || e.altKey) {
|
||||
if (e.code === "Enter") {
|
||||
e.preventDefault();
|
||||
flash(view);
|
||||
await activateCode();
|
||||
} else if (e.code === "Period") {
|
||||
cycle.stop();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}), C = x(), H = I(() => ((_ || !t) && (C.current = !0), _ || C.current), [_, t]);
|
||||
return le({
|
||||
view: y,
|
||||
pattern: v,
|
||||
active: p && !g?.includes("strudel disable-highlighting"),
|
||||
getTime: () => D.getPhase()
|
||||
}), K(() => {
|
||||
if (a) {
|
||||
const F = async (u) => {
|
||||
(u.ctrlKey || u.altKey) && (u.code === "Enter" ? (u.preventDefault(), ae(y), await d()) : u.code === "Period" && (k(), u.preventDefault()));
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyPress, true);
|
||||
return () => window.removeEventListener("keydown", handleKeyPress, true);
|
||||
return window.addEventListener("keydown", F, !0), () => window.removeEventListener("keydown", F, !0);
|
||||
}
|
||||
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
|
||||
return /* @__PURE__ */ React.createElement("div", {
|
||||
className: styles.container,
|
||||
ref
|
||||
}, /* @__PURE__ */ React.createElement("div", {
|
||||
className: styles.header
|
||||
}, /* @__PURE__ */ React.createElement("div", {
|
||||
className: styles.buttons
|
||||
}, /* @__PURE__ */ React.createElement("button", {
|
||||
className: cx(styles.button, cycle.started ? "sc-animate-pulse" : ""),
|
||||
onClick: () => togglePlay()
|
||||
}, /* @__PURE__ */ React.createElement(Icon, {
|
||||
type: cycle.started ? "pause" : "play"
|
||||
})), /* @__PURE__ */ React.createElement("button", {
|
||||
className: cx(dirty ? styles.button : styles.buttonDisabled),
|
||||
onClick: () => activateCode()
|
||||
}, /* @__PURE__ */ React.createElement(Icon, {
|
||||
}, [a, v, s, l, k, y]), /* @__PURE__ */ n.createElement("div", {
|
||||
className: b.container,
|
||||
ref: z
|
||||
}, /* @__PURE__ */ n.createElement("div", {
|
||||
className: b.header
|
||||
}, /* @__PURE__ */ n.createElement("div", {
|
||||
className: b.buttons
|
||||
}, /* @__PURE__ */ n.createElement("button", {
|
||||
className: B(b.button, p ? "sc-animate-pulse" : ""),
|
||||
onClick: () => h()
|
||||
}, /* @__PURE__ */ n.createElement(q, {
|
||||
type: p ? "pause" : "play"
|
||||
})), /* @__PURE__ */ n.createElement("button", {
|
||||
className: B(i ? b.button : b.buttonDisabled),
|
||||
onClick: () => d()
|
||||
}, /* @__PURE__ */ n.createElement(q, {
|
||||
type: "refresh"
|
||||
}))), error && /* @__PURE__ */ React.createElement("div", {
|
||||
className: styles.error
|
||||
}, error.message)), /* @__PURE__ */ React.createElement("div", {
|
||||
className: styles.body
|
||||
}, show && /* @__PURE__ */ React.createElement(CodeMirror, {
|
||||
value: code,
|
||||
onChange: setCode,
|
||||
onViewChanged: setView
|
||||
}))), c && /* @__PURE__ */ n.createElement("div", {
|
||||
className: b.error
|
||||
}, c.message)), /* @__PURE__ */ n.createElement("div", {
|
||||
className: b.body
|
||||
}, H && /* @__PURE__ */ n.createElement(ie, {
|
||||
value: s,
|
||||
onChange: m,
|
||||
onViewChanged: P
|
||||
})));
|
||||
}
|
||||
|
||||
/*
|
||||
useWebMidi.js - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useWebMidi.js>
|
||||
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/>.
|
||||
*/
|
||||
|
||||
function useWebMidi(props) {
|
||||
const { ready, connected, disconnected } = props;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [outputs, setOutputs] = useState(WebMidi?.outputs || []);
|
||||
useEffect(() => {
|
||||
enableWebMidi()
|
||||
.then(() => {
|
||||
// Reacting when a new device becomes available
|
||||
WebMidi.addListener('connected', (e) => {
|
||||
setOutputs([...WebMidi.outputs]);
|
||||
connected?.(WebMidi, e);
|
||||
});
|
||||
// Reacting when a device becomes unavailable
|
||||
WebMidi.addListener('disconnected', (e) => {
|
||||
setOutputs([...WebMidi.outputs]);
|
||||
disconnected?.(WebMidi, e);
|
||||
});
|
||||
ready?.(WebMidi);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
//throw new Error("Web Midi could not be enabled...");
|
||||
console.warn('Web Midi could not be enabled..');
|
||||
return;
|
||||
}
|
||||
});
|
||||
}, [ready, connected, disconnected, outputs]);
|
||||
const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||
return { loading, outputs, outputByName };
|
||||
function ze(e) {
|
||||
return R(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((t) => window.postMessage(t, "*"), []);
|
||||
}
|
||||
|
||||
export { CodeMirror, MiniRepl, cx, flash, useCycle, useHighlighting, usePostMessage, useRepl, useWebMidi };
|
||||
const He = (e) => K(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
|
||||
export {
|
||||
ie as CodeMirror,
|
||||
Pe as MiniRepl,
|
||||
B as cx,
|
||||
ae as flash,
|
||||
le as useHighlighting,
|
||||
He as useKeydown,
|
||||
ze as usePostMessage,
|
||||
ve as useStrudel
|
||||
};
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:16px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto}
|
||||
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:18px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
!dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:16px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.fixed{position:fixed}.absolute{position:absolute}.bottom-0{bottom:0px}.z-\[12\]{z-index:12}.flex{display:flex}.w-full{width:100%}.justify-center{justify-content:center}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.rounded-t-md{border-top-left-radius:.375rem;border-top-right-radius:.375rem}.bg-slate-500{--tw-bg-opacity: 1;background-color:rgb(100 116 139 / var(--tw-bg-opacity))}.px-2{padding-left:.5rem;padding-right:.5rem}body{background:#123}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{f as s,i as t,h as o,j as d,q as f,l as p,k as u,p as i,o as l,n as r,w as g}from"./index.ec9f9930.js";export{s as getAudioContext,t as getCachedBuffer,o as getDestination,d as getLoadedBuffer,f as getLoadedSamples,p as loadBuffer,u as loadGithubSamples,i as panic,l as resetLoadedSamples,r as samples,g as webaudioOutput};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
import{P as w}from"./index.ec9f9930.js";var i,f=!1;async function b(a=38400){if(!f){if(f=!0,i)return i;if("serial"in navigator){const r=await navigator.serial.requestPort();await r.open({baudRate:a});const o=new TextEncoderStream;o.readable.pipeTo(r.writable);const s=o.writable.getWriter();i=function(e){s.write(e)}}else throw"Webserial is not available in this browser."}}const g=.1;w.prototype.serial=function(...a){return this._withHap(r=>{i||b(...a);const o=(s,e,u)=>{var t="";if(typeof e.value=="object")if("action"in e.value){t+=e.value.action+"(";var c=!0;for(const[n,l]of Object.entries(e.value))n!=="action"&&(c?c=!1:t+=",",t+=`${n}:${l}`);t+=")"}else for(const[n,l]of Object.entries(e.value))t+=`${n}:${l};`;else t=e.value;const v=(s-u+g)*1e3;window.setTimeout(i,v,t)};return r.setContext({...r.context,onTrigger:o})})};export{b as getWriter};
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Strudel Nano REPL</title>
|
||||
<script type="module" crossorigin src="./assets/index.ec9f9930.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.75f8960b.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Strudel Nano REPL</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
+3520
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "nano-repl",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"postcss": "^8.4.16",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"vite": "^3.0.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { evalScope, controls } from '@strudel.cycles/core';
|
||||
import { getAudioContext, panic, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
import { useCallback, useState } from 'react';
|
||||
import CodeMirror, { flash } from '../../../src/components/CodeMirror6';
|
||||
import useKeydown from '../../../src/hooks/useKeydown.mjs';
|
||||
import useStrudel from '../../../src/hooks/useStrudel';
|
||||
import useHighlighting from '../../../src/hooks/useHighlighting';
|
||||
import './style.css';
|
||||
// import { prebake } from '../../../../../repl/src/prebake.mjs';
|
||||
|
||||
// TODO: only import stuff when play is pressed?
|
||||
evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
// import('@strudel.cycles/tone'),
|
||||
// import('@strudel.cycles/midi'), // TODO: find out why midi loads tone.js
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/xen'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/osc'),
|
||||
import('@strudel.cycles/webdirt'),
|
||||
import('@strudel.cycles/serial'),
|
||||
import('@strudel.cycles/soundfonts'),
|
||||
);
|
||||
|
||||
const defaultTune = `samples({
|
||||
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
|
||||
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
|
||||
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
|
||||
}, 'github:tidalcycles/Dirt-Samples/master/');
|
||||
stack(
|
||||
s("bd,[~ <sd!3 sd(3,4,2)>],hh(3,4)") // drums
|
||||
.speed(perlin.range(.7,.9)) // random sample speed variation
|
||||
//.hush()
|
||||
,"<a1 b1*2 a1(3,8) e2>" // bassline
|
||||
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.superimpose(add(.05)) // add second, slightly detuned voice
|
||||
.n() // wrap in "n"
|
||||
.decay(.15).sustain(0) // make each note of equal length
|
||||
.s('sawtooth') // waveform
|
||||
.gain(.4) // turn down
|
||||
.cutoff(sine.slow(7).range(300,5000)) // automate cutoff
|
||||
//.hush()
|
||||
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings() // chords
|
||||
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.n() // wrap in "n"
|
||||
.s('square') // waveform
|
||||
.gain(.16) // turn down
|
||||
.cutoff(500) // fixed cutoff
|
||||
.attack(1) // slowly fade in
|
||||
//.hush()
|
||||
,"a4 c5 <e6 a6>".struct("x(5,8)")
|
||||
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.n() // wrap in "n"
|
||||
.decay(.1).sustain(0) // make notes short
|
||||
.s('triangle') // waveform
|
||||
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
|
||||
.echoWith(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes
|
||||
//.hush()
|
||||
)
|
||||
.fast(2/3)`;
|
||||
|
||||
// await prebake();
|
||||
|
||||
const ctx = getAudioContext();
|
||||
const getTime = () => ctx.currentTime;
|
||||
function App() {
|
||||
const [code, setCode] = useState(defaultTune);
|
||||
const [view, setView] = useState();
|
||||
// const [code, setCode] = useState(`"c3".note().slow(2)`);
|
||||
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern } = useStrudel({
|
||||
code,
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime,
|
||||
});
|
||||
|
||||
useHighlighting({
|
||||
view,
|
||||
pattern,
|
||||
active: !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.getPhase(),
|
||||
});
|
||||
|
||||
const error = evalError || schedulerError;
|
||||
useKeydown(
|
||||
useCallback(
|
||||
async (e) => {
|
||||
if (e.ctrlKey || e.altKey) {
|
||||
if (e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
flash(view);
|
||||
await evaluate(code);
|
||||
if (e.shiftKey) {
|
||||
panic();
|
||||
scheduler.stop();
|
||||
scheduler.start();
|
||||
}
|
||||
if (!scheduler.started) {
|
||||
scheduler.start();
|
||||
}
|
||||
} else if (e.code === 'Period') {
|
||||
scheduler.pause();
|
||||
panic();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
[scheduler, evaluate, view],
|
||||
),
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
{/* <textarea value={code} onChange={(e) => setCode(e.target.value)} cols="64" rows="30" /> */}
|
||||
<nav className="z-[12] w-full flex justify-center absolute bottom-0">
|
||||
<div className="bg-slate-500 space-x-2 px-2 rounded-t-md">
|
||||
<button
|
||||
onClick={async () => {
|
||||
await evaluate(code);
|
||||
await getAudioContext().resume();
|
||||
scheduler.start();
|
||||
}}
|
||||
>
|
||||
start
|
||||
</button>
|
||||
<button onClick={() => scheduler.stop()}>stop</button>
|
||||
{isDirty && <button onClick={() => evaluate(code)}>eval</button>}
|
||||
</div>
|
||||
{error && <p>error {error.message}</p>}
|
||||
</nav>
|
||||
<CodeMirror value={code} onChange={setCode} onViewChanged={setView} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
background: #123;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
tailwind.config.js - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/tailwind.config.js>
|
||||
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/>.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// TODO: find out if leaving out tutorial path works now
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()]
|
||||
})
|
||||
+12
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/react",
|
||||
"version": "0.2.0",
|
||||
"version": "0.4.0",
|
||||
"description": "React components for strudel",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.es.js",
|
||||
@@ -17,6 +17,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"watch": "vite build --watch",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"repository": {
|
||||
@@ -37,12 +38,13 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.0.2",
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"@strudel.cycles/eval": "^0.2.0",
|
||||
"@strudel.cycles/tone": "^0.2.0",
|
||||
"@uiw/codemirror-themes": "^4.11.4",
|
||||
"@uiw/react-codemirror": "^4.11.4",
|
||||
"@codemirror/lang-javascript": "^6.1.1",
|
||||
"@strudel.cycles/core": "^0.4.0",
|
||||
"@strudel.cycles/tone": "^0.4.0",
|
||||
"@strudel.cycles/transpiler": "^0.4.0",
|
||||
"@strudel.cycles/webaudio": "^0.4.0",
|
||||
"@uiw/codemirror-themes": "^4.12.4",
|
||||
"@uiw/react-codemirror": "^4.12.4",
|
||||
"react-hook-inview": "^4.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -52,12 +54,12 @@
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@vitejs/plugin-react": "^1.3.0",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"postcss": "^8.4.13",
|
||||
"postcss": "^8.4.18",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"vite": "^2.9.9"
|
||||
"vite": "^3.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import React from 'react';
|
||||
import { MiniRepl } from './components/MiniRepl';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import { Tone, getDefaultSynth } from '@strudel.cycles/tone';
|
||||
import { evalScope } from '@strudel.cycles/eval';
|
||||
|
||||
const defaultSynth = getDefaultSynth();
|
||||
import { controls, evalScope } from '@strudel.cycles/core';
|
||||
|
||||
evalScope(
|
||||
Tone,
|
||||
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'),
|
||||
@@ -20,7 +17,7 @@ evalScope(
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<MiniRepl tune={`"c3"`} defaultSynth={defaultSynth} />
|
||||
<MiniRepl tune={`note("c3")`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,18 +7,33 @@ import 'tailwindcss/tailwind.css';
|
||||
import './style.css';
|
||||
import styles from './MiniRepl.module.css';
|
||||
import { Icon } from './Icon';
|
||||
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
import useStrudel from '../hooks/useStrudel.mjs';
|
||||
|
||||
export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) {
|
||||
const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } =
|
||||
useRepl({
|
||||
tune,
|
||||
defaultSynth,
|
||||
autolink: false,
|
||||
onEvent,
|
||||
});
|
||||
useEffect(() => {
|
||||
init && evaluateOnly();
|
||||
}, [tune, init]);
|
||||
const getTime = () => getAudioContext().currentTime;
|
||||
|
||||
export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }) {
|
||||
const {
|
||||
code,
|
||||
setCode,
|
||||
evaluate,
|
||||
activateCode,
|
||||
error,
|
||||
isDirty,
|
||||
activeCode,
|
||||
pattern,
|
||||
started,
|
||||
scheduler,
|
||||
togglePlay,
|
||||
stop,
|
||||
} = useStrudel({
|
||||
initialCode: tune,
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime,
|
||||
});
|
||||
/* useEffect(() => {
|
||||
init && activateCode();
|
||||
}, [init, activateCode]); */
|
||||
const [view, setView] = useState();
|
||||
const [ref, isVisible] = useInView({
|
||||
threshold: 0.01,
|
||||
@@ -31,7 +45,12 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, i
|
||||
}
|
||||
return isVisible || wasVisible.current;
|
||||
}, [isVisible, hideOutsideView]);
|
||||
useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes('strudel disable-highlighting') });
|
||||
useHighlighting({
|
||||
view,
|
||||
pattern,
|
||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.getPhase(),
|
||||
});
|
||||
|
||||
// set active pattern on ctrl+enter
|
||||
useLayoutEffect(() => {
|
||||
@@ -43,7 +62,7 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, i
|
||||
flash(view);
|
||||
await activateCode();
|
||||
} else if (e.code === 'Period') {
|
||||
cycle.stop();
|
||||
stop();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
@@ -51,16 +70,16 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, i
|
||||
window.addEventListener('keydown', handleKeyPress, true);
|
||||
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
||||
}
|
||||
}, [enableKeyboard, pattern, code, activateCode, cycle, view]);
|
||||
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={ref}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.buttons}>
|
||||
<button className={cx(styles.button, cycle.started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
|
||||
<Icon type={cycle.started ? 'pause' : 'play'} />
|
||||
<button className={cx(styles.button, started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
|
||||
<Icon type={started ? 'pause' : 'play'} />
|
||||
</button>
|
||||
<button className={cx(dirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
||||
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
||||
<Icon type="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user