mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-23 13:42:56 -04:00
Compare commits
424 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90a58858ca | |||
| 8149b85a55 | |||
| e79b6c514a | |||
| aaa894d74b | |||
| b55721f16f | |||
| b39d948d71 | |||
| 0fee279e3d | |||
| c7d8a9cb82 | |||
| b50075f102 | |||
| fb3ca9f960 | |||
| 058b971870 | |||
| 610ea414c1 | |||
| cc6997baec | |||
| d126c28b85 | |||
| c1649b9dc0 | |||
| b0a8d12112 | |||
| b5160d38cb | |||
| 360e9b5936 | |||
| 85a6cd828e | |||
| e698f19a64 | |||
| 83c1b580dd | |||
| 9273e1f8ed | |||
| 5287825279 | |||
| 5567959afd | |||
| eaab36b051 | |||
| 9a4f3af580 | |||
| b83664dfce | |||
| 349f7ae965 | |||
| 6f41716ab5 | |||
| abeb62dea8 | |||
| 7cfed80443 | |||
| 36aa7d9b25 | |||
| 2a449ad3c0 | |||
| a71c5957cc | |||
| eef6c7d55e | |||
| 3872322da1 | |||
| f1e850a62a | |||
| 8d9278419d | |||
| 0862827a51 | |||
| f37a220a05 | |||
| aa51c2b6dd | |||
| 92da70a7ae | |||
| a55862fc25 | |||
| 09b14a5702 | |||
| 260b831815 | |||
| 665cf6ac65 | |||
| 5e90021698 | |||
| 16aeb7e12c | |||
| b4cbec173b | |||
| 8c9e06c329 | |||
| e4a35fdd72 | |||
| 370d82e66c | |||
| 876e851517 | |||
| 190fe37a9f | |||
| 623661f8d2 | |||
| f31bc3f8c5 | |||
| 12fc48544c | |||
| 3b4e44a9fd | |||
| 7850fb727e | |||
| 9b3122837e | |||
| d4c59cc73b | |||
| fe36bf54b2 | |||
| 4d8308a763 | |||
| 11f26c1e2c | |||
| e10104d401 | |||
| 5e09c3c318 | |||
| 576b88221d | |||
| 7dc2a0091b | |||
| 89a4a85589 | |||
| a3baf07a5c | |||
| 8e6909478c | |||
| 2d2d0cabb3 | |||
| 6f04a1b8fa | |||
| a9ad3a56ad | |||
| 3069b14e9c | |||
| ae868c6372 | |||
| c287ff554c | |||
| 3ccbd35fad | |||
| 51cc69208f | |||
| 1fec2f7a46 | |||
| ffffdd2d7d | |||
| f8c3e3d5aa | |||
| eccce6fa84 | |||
| 44be6cee2c | |||
| 799afd511a | |||
| 37c4541468 | |||
| 119a66acd0 | |||
| 105193a452 | |||
| 02d793e9e0 | |||
| 90647a8ba8 | |||
| 06950a6bd8 | |||
| 0c2147d9c5 | |||
| 51e8174689 | |||
| 185318a70c | |||
| a88759ba94 | |||
| 96ac054392 | |||
| bd7f4dd73f | |||
| 2e22152089 | |||
| 98a89aea11 | |||
| 372e33c066 | |||
| 52641db590 | |||
| f9ad5f0d01 | |||
| 3db1c7a521 | |||
| b6d1f96930 | |||
| 61a9b01062 | |||
| f0faf5b42e | |||
| 11733d5e05 | |||
| c4a38d9008 | |||
| 0edd7e14b7 | |||
| 8b7bb7b6ae | |||
| 39d5955e58 | |||
| 536327f403 | |||
| 8679dc63be | |||
| 1e9979ae18 | |||
| d2dffe3186 | |||
| 9971867e2f | |||
| 8c93e578a0 | |||
| 1ba5d2e1ca | |||
| ed792fc0d4 | |||
| 4e575c44b3 | |||
| fc06181217 | |||
| 0d6fcf78d8 | |||
| 82225f0b81 | |||
| 2177e13b49 | |||
| b63f4eb503 | |||
| cd7bc09f9c | |||
| ccf775e976 | |||
| 811346e83d | |||
| d3240a75d0 | |||
| d93e396ce5 | |||
| d74cc234c4 | |||
| 0fda83d8de | |||
| a1ec8545fb | |||
| 087f574355 | |||
| 1de784b817 | |||
| db79c05d9a | |||
| 1fdb1f0da6 | |||
| e2bfaa6233 | |||
| 6089849d35 | |||
| 6e02bf59e9 | |||
| 12228c56d9 | |||
| 3b631cb6af | |||
| b27a58df7e | |||
| 308f139193 | |||
| 692fc4e11d | |||
| b52f65c4d7 | |||
| 299275e368 | |||
| bb71c519db | |||
| 9ec7109dc8 | |||
| 5b67fccb1b | |||
| 9807c4b7a1 | |||
| 9039600f6d | |||
| b5b0156e08 | |||
| 352e647d37 | |||
| e6b66f2525 | |||
| f5075906e2 | |||
| a6f57bced8 | |||
| d35bf9591c | |||
| d96f3f9dd4 | |||
| 1f4c2f8c5a | |||
| 95719654f3 | |||
| 4b921c47f5 | |||
| f069f53fae | |||
| 1494cc38fc | |||
| a035412956 | |||
| 08662ea831 | |||
| d97103268b | |||
| a959e2e34d | |||
| 602bfbdd79 | |||
| 0785bdd37a | |||
| 8b62b6e1b1 | |||
| d1c8ebd15b | |||
| 13088e4001 | |||
| 5c6fa1c82b | |||
| fd81867e7b | |||
| 78293e64dd | |||
| 930fc10aec | |||
| 0b2a4fda44 | |||
| efdce6d4cd | |||
| c0b0ea4493 | |||
| 0fcec0a0ca | |||
| c0701d8a35 | |||
| ae51092044 | |||
| b7fcccb8db | |||
| b21a52f5f3 | |||
| 2bdcfe13e6 | |||
| 12a97ec429 | |||
| 99298ce9f2 | |||
| a0ed25d373 | |||
| dc6c29c668 | |||
| 3a85f28b5c | |||
| 95c44b1ce2 | |||
| b88e0ecb78 | |||
| c26f6b72d9 | |||
| 6cd3cc96a3 | |||
| c138cf0b76 | |||
| 8edb086178 | |||
| 0f74b5a4b1 | |||
| 6de2feff05 | |||
| a9811ee955 | |||
| 98b6c11a82 | |||
| 3df2929d12 | |||
| 30d1f423cb | |||
| e0c5566f35 | |||
| c5a2ae47d7 | |||
| 17654b885b | |||
| acaf0ca0af | |||
| 6a201be1fd | |||
| 8617b9164e | |||
| 991e5f1b3c | |||
| 55c533c947 | |||
| d17543d5d9 | |||
| 73169563c4 | |||
| d351cb9f7f | |||
| 251be60630 | |||
| 18d7222804 | |||
| ff1c37d361 | |||
| 3abe6db3de | |||
| b9ca9d3b06 | |||
| 9bbc04843d | |||
| ddf61a6438 | |||
| ba35a81e9b | |||
| 13133583ca | |||
| e91b981740 | |||
| 1458ee64ec | |||
| 4e2a5e1663 | |||
| c98b0e9687 | |||
| 4e25990de9 | |||
| 4b51f1a3fc | |||
| 5490b01004 | |||
| 43176e1f1d | |||
| fffd3c07ca | |||
| 952bd38e70 | |||
| 8589f5cc89 | |||
| ec594d52f8 | |||
| 848cbf9c60 | |||
| e02be7dd9a | |||
| 33d35e47a6 | |||
| 18d62b32c2 | |||
| 58ed51b49c | |||
| 9f9fe529e4 | |||
| aad721890a | |||
| e63a5d10c8 | |||
| 15681262de | |||
| b5d3aa81f9 | |||
| a688061616 | |||
| f788d347ec | |||
| a33ce67dd4 | |||
| 8a22c5627a | |||
| 7b529a30f4 | |||
| 4910b34f26 | |||
| 74ad521fc9 | |||
| 6a73b43e3a | |||
| b59b26460d | |||
| aa324ae25d | |||
| ff412f6396 | |||
| ac148b2f32 | |||
| 6f5d096e6d | |||
| ff1e6714c6 | |||
| 41d82eaadf | |||
| be6b011204 | |||
| 65e48c05f0 | |||
| cee08ea67d | |||
| 35ef26c013 | |||
| b08a0b8102 | |||
| 220342154b | |||
| ef7182cd76 | |||
| 6059c69995 | |||
| 892d8ba152 | |||
| da11069f75 | |||
| bf72908dc9 | |||
| 81ab5c78e2 | |||
| de503c7d63 | |||
| 487ccb90a1 | |||
| 4bfeaa47bf | |||
| 9a4d8036a6 | |||
| a50bd6375d | |||
| 14508e91a9 | |||
| 2d5064bd5b | |||
| fdc4819e39 | |||
| efddbb616b | |||
| db4d0f26a8 | |||
| 06cffbc31b | |||
| 3cfb0f4c76 | |||
| 436ba067bc | |||
| f8724bf47e | |||
| 1f7e293204 | |||
| 86d2652258 | |||
| f5c0cc2e2d | |||
| 4463d4a417 | |||
| df35a5c9b1 | |||
| dda93b3c13 | |||
| 5de40b6bf5 | |||
| dcbe799d25 | |||
| a5641aef03 | |||
| 668f97a4c8 | |||
| e9d577496b | |||
| 66fba947ac | |||
| 18c7e02609 | |||
| 7e1351507a | |||
| 032acbd27c | |||
| 8b51f0fbd2 | |||
| ead5942ef0 | |||
| cfe369d9fe | |||
| 2750a3c130 | |||
| a7f6d6ecac | |||
| 3680282450 | |||
| 3b3435c999 | |||
| e2ae6278c2 | |||
| 37d6723e3c | |||
| 961bbf6f55 | |||
| fc71d349ed | |||
| 97c8ab0f71 | |||
| 28534ea547 | |||
| 1a75b79a33 | |||
| 75dfb0af35 | |||
| 073fa09648 | |||
| 3015e7b203 | |||
| 7ac0cdc0f9 | |||
| 49234183a3 | |||
| ed7763df92 | |||
| b818a02f76 | |||
| 886f8449fd | |||
| 5de6643604 | |||
| 6283273d81 | |||
| 5506d18bc5 | |||
| c8f8f02a46 | |||
| 4c5d6f1d6c | |||
| f349e36345 | |||
| 1574c4ab13 | |||
| 0dc9bc3ab6 | |||
| 0006d57a4a | |||
| 70f7e73b9a | |||
| b67b049802 | |||
| ff99dbcd22 | |||
| 0198697737 | |||
| a742a71f67 | |||
| 8f6c1531fa | |||
| d9f56f11cb | |||
| 7ff877464f | |||
| 7c367eb1e8 | |||
| 20848aac09 | |||
| 7716574076 | |||
| 89cd0c769b | |||
| a21b3d788f | |||
| 7994ba8b38 | |||
| 81b4cb2f16 | |||
| 14af1df6a3 | |||
| ac3bd7d7db | |||
| 632e8e9634 | |||
| 1e3f09f69b | |||
| f3f18ffca7 | |||
| ceb3aa0627 | |||
| 014555fe5d | |||
| fdb76867a7 | |||
| 57157fde9c | |||
| 4a3540cf2b | |||
| 540bd938f2 | |||
| c6fbebd996 | |||
| 18d3a7e23a | |||
| cbae355896 | |||
| 924e8a764a | |||
| 40e629cb0e | |||
| 99ec261f79 | |||
| 9daea5bbc2 | |||
| 4704c03d48 | |||
| 9dbf52bedb | |||
| cfd04ab7fa | |||
| 33dbf75e3b | |||
| 8ffaeeb171 | |||
| 451828e5d4 | |||
| 176877f2b8 | |||
| 61b0d08b5f | |||
| f50a6ce1ae | |||
| 8549d6ea8b | |||
| 90a90a87a5 | |||
| 760bdea521 | |||
| 0fd783e83f | |||
| 42caa9058b | |||
| 9ab315ac2c | |||
| a08ad07537 | |||
| c6ce763f7c | |||
| 70d1eb17a7 | |||
| 0393205eda | |||
| 5c05a5a63e | |||
| b01d9f849b | |||
| 0ab904275c | |||
| 5f82a86801 | |||
| 13a5d8de86 | |||
| f024b71808 | |||
| cd35a17904 | |||
| be3f68ed06 | |||
| 64b8fd9584 | |||
| b001d1d0f1 | |||
| 64ad75c79e | |||
| 12eb361d69 | |||
| dd51bd0a2f | |||
| 0fb478a7c8 | |||
| 9694ecf55b | |||
| fe80478b8a | |||
| a253c26bee | |||
| a500f983f5 | |||
| 3579b6f8f3 | |||
| 14cb954213 | |||
| 6170eef441 | |||
| ddb9af4d57 | |||
| 6db2c0bf93 | |||
| 46f70e6a5e | |||
| 0b994a1b0f | |||
| 0326244956 | |||
| ea1b2252fc | |||
| 0da25ea7cc | |||
| 3209afa4ff | |||
| 33e2631e5d | |||
| f02a41b675 | |||
| f497d7b212 | |||
| 0f18f5eb81 | |||
| 8da81104aa | |||
| 97cbdc6b0f | |||
| 7f53f4e36b | |||
| 3e727893bf | |||
| 4f0885d3af | |||
| 30b149053c | |||
| cdd40df9d3 |
@@ -16,3 +16,5 @@ vite.config.js
|
|||||||
**/*.tsx
|
**/*.tsx
|
||||||
**/*.ts
|
**/*.ts
|
||||||
**/*.json
|
**/*.json
|
||||||
|
**/dev-dist
|
||||||
|
**/dist
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
name: Tauri Builder
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
rust_target: x86_64-unknown-linux-gnu
|
||||||
|
- os: macos-latest
|
||||||
|
rust_target: x86_64-apple-darwin
|
||||||
|
- os: macos-latest
|
||||||
|
rust_target: aarch64-apple-darwin
|
||||||
|
- os: windows-latest
|
||||||
|
rust_target: x86_64-pc-windows-msvc
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8.6.2
|
||||||
|
|
||||||
|
- name: Node.js setup
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: latest
|
||||||
|
# node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
- name: Install Rust (Stable)
|
||||||
|
run:
|
||||||
|
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||||
|
|
||||||
|
- name: Install dependencies (ubuntu only)
|
||||||
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Install app dependencies from lockfile and build web
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build the app
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
# tauri-action replaces \_\_VERSION\_\_ with the app version
|
||||||
|
tagName: ${{ github.ref_name }}
|
||||||
|
releaseName: "Strudel v__VERSION__"
|
||||||
|
releaseBody: |
|
||||||
|
See the assets to download this version and install.
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: false
|
||||||
@@ -40,3 +40,7 @@ repl/stats.html
|
|||||||
coverage
|
coverage
|
||||||
public/icons/apple-splash-*
|
public/icons/apple-splash-*
|
||||||
dev-dist
|
dev-dist
|
||||||
|
Dirt-Samples
|
||||||
|
tidal-drum-machines
|
||||||
|
webaudiofontdata
|
||||||
|
src-tauri/target
|
||||||
@@ -9,3 +9,4 @@ packages/xen/tunejs.js
|
|||||||
paper
|
paper
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
pnpm-workspace.yaml
|
pnpm-workspace.yaml
|
||||||
|
**/dev-dist
|
||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
"jsxSingleQuote": false,
|
"jsxSingleQuote": false,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"jsxBracketSameLine": false,
|
"bracketSameLine": false,
|
||||||
"arrowParens": "always",
|
"arrowParens": "always",
|
||||||
"proseWrap": "preserve",
|
"proseWrap": "preserve",
|
||||||
"htmlWhitespaceSensitivity": "css",
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
|||||||
+31
-2
@@ -31,7 +31,7 @@ Use one of the Communication Channels listed above.
|
|||||||
|
|
||||||
## Improve the Docs
|
## Improve the Docs
|
||||||
|
|
||||||
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/learn/getting-started),
|
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/workshop/getting-started/),
|
||||||
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
|
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
|
||||||
|
|
||||||
## Propose a Feature
|
## Propose a Feature
|
||||||
@@ -82,6 +82,10 @@ Please report any problems you've had with the setup instructions!
|
|||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
To make sure the code changes only where it should, we are using prettier to unify the code style.
|
To make sure the code changes only where it should, we are using prettier to unify the code style.
|
||||||
|
|
||||||
|
- You can format all files at once by running `pnpm prettier` from the project root
|
||||||
|
- Run `pnpm format-check` from the project root to check if all files are well formatted
|
||||||
|
|
||||||
If you use VSCode, you can
|
If you use VSCode, you can
|
||||||
|
|
||||||
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||||
@@ -89,6 +93,24 @@ If you use VSCode, you can
|
|||||||
3. Choose "Configure Default Formatter..."
|
3. Choose "Configure Default Formatter..."
|
||||||
4. Select prettier
|
4. Select prettier
|
||||||
|
|
||||||
|
## ESLint
|
||||||
|
|
||||||
|
To prevent unwanted runtime errors, this project uses [eslint](https://eslint.org/).
|
||||||
|
|
||||||
|
- You can check for lint errors by running `pnpm lint`
|
||||||
|
|
||||||
|
There are also eslint extensions / plugins for most editors.
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
- Run all tests with `pnpm test`
|
||||||
|
- Run all tests with UI using `pnpm test-ui`
|
||||||
|
|
||||||
|
## Running all CI Checks
|
||||||
|
|
||||||
|
When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests.
|
||||||
|
You can run the same check with `pnpm check`
|
||||||
|
|
||||||
## Package Workflow
|
## Package Workflow
|
||||||
|
|
||||||
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
|
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
|
||||||
@@ -101,7 +123,14 @@ To publish all packages that have been changed since the last release, run:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm login
|
npm login
|
||||||
npx lerna publish
|
|
||||||
|
# this will increment all the versions in package.json files of non private packages to selected versions
|
||||||
|
npx lerna version --no-private
|
||||||
|
|
||||||
|
# publish all packages inside /packages using pnpm! don't use lerna to publish!!
|
||||||
|
pnpm --filter "./packages/**" publish --dry-run
|
||||||
|
|
||||||
|
# the last command was only a dry-run, make sure everything looks ok, if yes, run the same command without flag
|
||||||
```
|
```
|
||||||
|
|
||||||
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
|
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using w
|
|||||||
After cloning the project, you can run the REPL locally:
|
After cloning the project, you can run the REPL locally:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run setup
|
pnpm run setup
|
||||||
npm run repl
|
pnpm run repl
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using Strudel In Your Project
|
## Using Strudel In Your Project
|
||||||
|
|||||||
@@ -20,11 +20,8 @@ Example: <https://felixroos.github.io/strudel/swatch/>
|
|||||||
### 6. edit `website/astro.config.mjs` to use site: `https://<your-username>.github.io` and base `/strudel`, like this
|
### 6. edit `website/astro.config.mjs` to use site: `https://<your-username>.github.io` and base `/strudel`, like this
|
||||||
|
|
||||||
```js
|
```js
|
||||||
export default defineConfig({
|
const site = 'https://<your-username>.github.io';
|
||||||
/* ... rest of config ... */
|
const base = '/strudel';
|
||||||
site: 'https://<your-username>.github.io',
|
|
||||||
base: '/strudel',
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. commit & push the changes
|
### 7. commit & push the changes
|
||||||
|
|||||||
+13
-14
@@ -18,7 +18,6 @@
|
|||||||
"build": "npm run prebuild && cd website && npm run build",
|
"build": "npm run prebuild && cd website && npm run build",
|
||||||
"preview": "cd website && npm run preview",
|
"preview": "cd website && npm run preview",
|
||||||
"osc": "cd packages/osc && npm run server",
|
"osc": "cd packages/osc && npm run server",
|
||||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
|
|
||||||
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
||||||
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
|
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
|
||||||
"lint": "eslint . --ext mjs,js --quiet",
|
"lint": "eslint . --ext mjs,js --quiet",
|
||||||
@@ -46,28 +45,28 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"homepage": "https://strudel.tidalcycles.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dependency-tree": "^9.0.0",
|
|
||||||
"vitest": "^0.25.7",
|
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
"@strudel.cycles/mini": "workspace:*",
|
||||||
"@strudel.cycles/tonal": "workspace:*",
|
"@strudel.cycles/tonal": "workspace:*",
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
"@strudel.cycles/transpiler": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
"@strudel.cycles/xen": "workspace:*"
|
"@strudel.cycles/xen": "workspace:*",
|
||||||
|
"acorn": "^8.8.1",
|
||||||
|
"dependency-tree": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitest/ui": "^0.25.7",
|
"@tauri-apps/cli": "^1.4.0",
|
||||||
"c8": "^7.12.0",
|
"@vitest/ui": "^0.28.0",
|
||||||
"canvas": "^2.11.0",
|
"canvas": "^2.11.2",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.39.0",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"gh-pages": "^4.0.0",
|
"jsdoc": "^4.0.2",
|
||||||
"jsdoc": "^3.6.10",
|
|
||||||
"jsdoc-json": "^2.0.2",
|
"jsdoc-json": "^2.0.2",
|
||||||
"jsdoc-to-markdown": "^7.1.1",
|
"jsdoc-to-markdown": "^8.0.0",
|
||||||
"lerna": "^4.0.0",
|
"lerna": "^6.6.1",
|
||||||
"prettier": "^2.8.1",
|
"prettier": "^2.8.8",
|
||||||
"rollup-plugin-visualizer": "^5.8.1"
|
"rollup-plugin-visualizer": "^5.8.1",
|
||||||
|
"vitest": "^0.28.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# @strudel/codemirror
|
||||||
|
|
||||||
|
This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it.
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
|
import { EditorView, keymap, Decoration, lineNumbers, highlightActiveLineGutter } from '@codemirror/view';
|
||||||
|
import { defaultKeymap } from '@codemirror/commands';
|
||||||
|
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { StateField, StateEffect } from '@codemirror/state';
|
||||||
|
import { oneDark } from './themes/one-dark';
|
||||||
|
import { repl, Drawer } from '@strudel.cycles/core';
|
||||||
|
|
||||||
|
// https://codemirror.net/docs/guide/
|
||||||
|
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
|
||||||
|
let state = EditorState.create({
|
||||||
|
doc: initialCode,
|
||||||
|
extensions: [
|
||||||
|
theme,
|
||||||
|
javascript(),
|
||||||
|
lineNumbers(),
|
||||||
|
highlightField,
|
||||||
|
highlightActiveLineGutter(),
|
||||||
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
|
keymap.of(defaultKeymap),
|
||||||
|
flashField,
|
||||||
|
EditorView.updateListener.of((v) => onChange(v)),
|
||||||
|
keymap.of([
|
||||||
|
{
|
||||||
|
key: 'Ctrl-Enter',
|
||||||
|
run: () => onEvaluate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Ctrl-.',
|
||||||
|
run: () => onStop(),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return new EditorView({
|
||||||
|
state,
|
||||||
|
parent: root,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// highlighting
|
||||||
|
//
|
||||||
|
|
||||||
|
export const setHighlights = StateEffect.define();
|
||||||
|
export const highlightField = StateField.define({
|
||||||
|
create() {
|
||||||
|
return Decoration.none;
|
||||||
|
},
|
||||||
|
update(highlights, tr) {
|
||||||
|
try {
|
||||||
|
for (let e of tr.effects) {
|
||||||
|
if (e.is(setHighlights)) {
|
||||||
|
const { haps } = e.value;
|
||||||
|
const marks =
|
||||||
|
haps
|
||||||
|
.map((hap) =>
|
||||||
|
(hap.context.locations || []).map(({ start, end }) => {
|
||||||
|
// const color = hap.context.color || e.value.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; // dont mark outside of range, as it will throw an error
|
||||||
|
}
|
||||||
|
const mark = Decoration.mark({
|
||||||
|
attributes: { style: `outline: 2px solid #FFCA28;` },
|
||||||
|
});
|
||||||
|
return mark.range(from, to);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.flat()
|
||||||
|
.filter(Boolean) || [];
|
||||||
|
highlights = Decoration.set(marks, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return highlights;
|
||||||
|
} catch (err) {
|
||||||
|
// console.warn('highlighting error', err);
|
||||||
|
return Decoration.set([]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
|
});
|
||||||
|
|
||||||
|
// helper to simply trigger highlighting for given haps
|
||||||
|
export function highlightHaps(view, haps) {
|
||||||
|
view.dispatch({ effects: setHighlights.of({ haps }) });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// flash
|
||||||
|
//
|
||||||
|
|
||||||
|
export const setFlash = StateEffect.define();
|
||||||
|
const flashField = StateField.define({
|
||||||
|
create() {
|
||||||
|
return Decoration.none;
|
||||||
|
},
|
||||||
|
update(flash, tr) {
|
||||||
|
try {
|
||||||
|
for (let e of tr.effects) {
|
||||||
|
if (e.is(setFlash)) {
|
||||||
|
if (e.value) {
|
||||||
|
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||||
|
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||||
|
} else {
|
||||||
|
flash = Decoration.set([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flash;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('flash error', err);
|
||||||
|
return flash;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const flash = (view, ms = 200) => {
|
||||||
|
view.dispatch({ effects: setFlash.of(true) });
|
||||||
|
setTimeout(() => {
|
||||||
|
view.dispatch({ effects: setFlash.of(false) });
|
||||||
|
}, ms);
|
||||||
|
};
|
||||||
|
|
||||||
|
export class StrudelMirror {
|
||||||
|
constructor(options) {
|
||||||
|
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
|
||||||
|
this.code = initialCode;
|
||||||
|
|
||||||
|
this.drawer = new Drawer((haps, time) => {
|
||||||
|
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
|
||||||
|
this.highlight(currentFrame);
|
||||||
|
onDraw?.(haps, time, currentFrame);
|
||||||
|
}, drawTime);
|
||||||
|
|
||||||
|
const prebaked = prebake();
|
||||||
|
prebaked.then(async () => {
|
||||||
|
if (!onDraw) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { scheduler, evaluate } = await this.repl;
|
||||||
|
// draw first frame instantly
|
||||||
|
prebaked.then(async () => {
|
||||||
|
await evaluate(this.code, false);
|
||||||
|
this.drawer.invalidate(scheduler);
|
||||||
|
onDraw?.(this.drawer.visibleHaps, 0, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.repl = repl({
|
||||||
|
...replOptions,
|
||||||
|
onToggle: async (started) => {
|
||||||
|
replOptions?.onToggle?.(started);
|
||||||
|
const { scheduler } = await this.repl;
|
||||||
|
if (started) {
|
||||||
|
this.drawer.start(scheduler);
|
||||||
|
} else {
|
||||||
|
this.drawer.stop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeEval: async () => {
|
||||||
|
await prebaked;
|
||||||
|
},
|
||||||
|
afterEval: (options) => {
|
||||||
|
replOptions?.afterEval?.(options);
|
||||||
|
this.drawer.invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.editor = initEditor({
|
||||||
|
root,
|
||||||
|
initialCode,
|
||||||
|
onChange: (v) => {
|
||||||
|
this.code = v.state.doc.toString();
|
||||||
|
},
|
||||||
|
onEvaluate: () => this.evaluate(),
|
||||||
|
onStop: () => this.stop(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async evaluate() {
|
||||||
|
const { evaluate } = await this.repl;
|
||||||
|
this.flash();
|
||||||
|
await evaluate(this.code);
|
||||||
|
}
|
||||||
|
async stop() {
|
||||||
|
const { scheduler } = await this.repl;
|
||||||
|
scheduler.stop();
|
||||||
|
}
|
||||||
|
flash(ms) {
|
||||||
|
flash(this.editor, ms);
|
||||||
|
}
|
||||||
|
highlight(haps) {
|
||||||
|
highlightHaps(this.editor, haps);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "@strudel/codemirror",
|
||||||
|
"version": "0.8.4",
|
||||||
|
"description": "Codemirror Extensions for Strudel",
|
||||||
|
"main": "codemirror.mjs",
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"tidalcycles",
|
||||||
|
"strudel",
|
||||||
|
"pattern",
|
||||||
|
"livecoding",
|
||||||
|
"algorave"
|
||||||
|
],
|
||||||
|
"author": "Felix Roos <flix91@gmail.com>",
|
||||||
|
"contributors": [
|
||||||
|
"Alex McLean <alex@slab.org>"
|
||||||
|
],
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/commands": "^6.2.4",
|
||||||
|
"@codemirror/lang-javascript": "^6.1.7",
|
||||||
|
"@codemirror/language": "^6.6.0",
|
||||||
|
"@codemirror/state": "^6.2.0",
|
||||||
|
"@codemirror/view": "^6.10.0",
|
||||||
|
"@lezer/highlight": "^1.1.4",
|
||||||
|
"@strudel.cycles/core": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^4.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
|
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
|
||||||
|
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
|
||||||
|
|
||||||
|
const chalky = '#e5c07b',
|
||||||
|
coral = '#e06c75',
|
||||||
|
cyan = '#56b6c2',
|
||||||
|
invalid = '#ffffff',
|
||||||
|
ivory = '#abb2bf',
|
||||||
|
stone = '#7d8799', // Brightened compared to original to increase contrast
|
||||||
|
malibu = '#61afef',
|
||||||
|
sage = '#98c379',
|
||||||
|
whiskey = '#d19a66',
|
||||||
|
violet = '#c678dd',
|
||||||
|
darkBackground = '#21252b',
|
||||||
|
highlightBackground = '#2c313a',
|
||||||
|
background = '#282c34',
|
||||||
|
tooltipBackground = '#353a42',
|
||||||
|
selection = '#3E4451',
|
||||||
|
cursor = '#528bff';
|
||||||
|
|
||||||
|
/// The colors used in the theme, as CSS color strings.
|
||||||
|
export const color = {
|
||||||
|
chalky,
|
||||||
|
coral,
|
||||||
|
cyan,
|
||||||
|
invalid,
|
||||||
|
ivory,
|
||||||
|
stone,
|
||||||
|
malibu,
|
||||||
|
sage,
|
||||||
|
whiskey,
|
||||||
|
violet,
|
||||||
|
darkBackground,
|
||||||
|
highlightBackground,
|
||||||
|
background,
|
||||||
|
tooltipBackground,
|
||||||
|
selection,
|
||||||
|
cursor,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The editor theme styles for One Dark.
|
||||||
|
export const oneDarkTheme = EditorView.theme(
|
||||||
|
{
|
||||||
|
'&': {
|
||||||
|
color: ivory,
|
||||||
|
backgroundColor: background,
|
||||||
|
},
|
||||||
|
|
||||||
|
'.cm-content': {
|
||||||
|
caretColor: cursor,
|
||||||
|
},
|
||||||
|
|
||||||
|
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
|
||||||
|
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
|
||||||
|
{ backgroundColor: selection },
|
||||||
|
|
||||||
|
'.cm-panels': { backgroundColor: darkBackground, color: ivory },
|
||||||
|
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
||||||
|
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
||||||
|
|
||||||
|
'.cm-searchMatch': {
|
||||||
|
backgroundColor: '#72a1ff59',
|
||||||
|
outline: '1px solid #457dff',
|
||||||
|
},
|
||||||
|
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||||
|
backgroundColor: '#6199ff2f',
|
||||||
|
},
|
||||||
|
|
||||||
|
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
|
||||||
|
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
||||||
|
|
||||||
|
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||||
|
backgroundColor: '#bad0f847',
|
||||||
|
},
|
||||||
|
|
||||||
|
'.cm-gutters': {
|
||||||
|
backgroundColor: background,
|
||||||
|
color: stone,
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
|
||||||
|
'.cm-activeLineGutter': {
|
||||||
|
backgroundColor: highlightBackground,
|
||||||
|
},
|
||||||
|
|
||||||
|
'.cm-foldPlaceholder': {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
color: '#ddd',
|
||||||
|
},
|
||||||
|
|
||||||
|
'.cm-tooltip': {
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: tooltipBackground,
|
||||||
|
},
|
||||||
|
'.cm-tooltip .cm-tooltip-arrow:before': {
|
||||||
|
borderTopColor: 'transparent',
|
||||||
|
borderBottomColor: 'transparent',
|
||||||
|
},
|
||||||
|
'.cm-tooltip .cm-tooltip-arrow:after': {
|
||||||
|
borderTopColor: tooltipBackground,
|
||||||
|
borderBottomColor: tooltipBackground,
|
||||||
|
},
|
||||||
|
'.cm-tooltip-autocomplete': {
|
||||||
|
'& > ul > li[aria-selected]': {
|
||||||
|
backgroundColor: highlightBackground,
|
||||||
|
color: ivory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ dark: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The highlighting style for code in the One Dark theme.
|
||||||
|
export const oneDarkHighlightStyle = HighlightStyle.define([
|
||||||
|
{ tag: t.keyword, color: violet },
|
||||||
|
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
|
||||||
|
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
|
||||||
|
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
|
||||||
|
{ tag: [t.definition(t.name), t.separator], color: ivory },
|
||||||
|
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
|
||||||
|
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
|
||||||
|
{ tag: [t.meta, t.comment], color: stone },
|
||||||
|
{ tag: t.strong, fontWeight: 'bold' },
|
||||||
|
{ tag: t.emphasis, fontStyle: 'italic' },
|
||||||
|
{ tag: t.strikethrough, textDecoration: 'line-through' },
|
||||||
|
{ tag: t.link, color: stone, textDecoration: 'underline' },
|
||||||
|
{ tag: t.heading, fontWeight: 'bold', color: coral },
|
||||||
|
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
|
||||||
|
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
|
||||||
|
{ tag: t.invalid, color: invalid },
|
||||||
|
]);
|
||||||
|
|
||||||
|
/// Extension to enable the One Dark theme (both the editor theme and
|
||||||
|
/// the highlight style).
|
||||||
|
export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { dependencies } from './package.json';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, 'codemirror.mjs'),
|
||||||
|
formats: ['es', 'cjs'],
|
||||||
|
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [...Object.keys(dependencies)],
|
||||||
|
},
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
import { Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
||||||
|
import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
|
||||||
const { createParams } = controls;
|
const { createParams } = controls;
|
||||||
|
|
||||||
let clearColor = '#22222210';
|
let clearColor = '#22222210';
|
||||||
|
|||||||
+301
-316
File diff suppressed because it is too large
Load Diff
+23
-17
@@ -10,44 +10,49 @@ import { logger } from './logger.mjs';
|
|||||||
export class Cyclist {
|
export class Cyclist {
|
||||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||||
this.started = false;
|
this.started = false;
|
||||||
this.cps = 1; // TODO
|
this.cps = 1;
|
||||||
this.phase = 0;
|
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||||
this.getTime = getTime;
|
this.lastBegin = 0; // query begin of last tick
|
||||||
|
this.lastEnd = 0; // query end of last tick
|
||||||
|
this.getTime = getTime; // get absolute time
|
||||||
this.onToggle = onToggle;
|
this.onToggle = onToggle;
|
||||||
this.latency = latency;
|
this.latency = latency; // fixed trigger time offset
|
||||||
const round = (x) => Math.round(x * 1000) / 1000;
|
const round = (x) => Math.round(x * 1000) / 1000;
|
||||||
this.clock = createClock(
|
this.clock = createClock(
|
||||||
getTime,
|
getTime,
|
||||||
|
// called slightly before each cycle
|
||||||
(phase, duration, tick) => {
|
(phase, duration, tick) => {
|
||||||
if (tick === 0) {
|
if (tick === 0) {
|
||||||
this.origin = phase;
|
this.origin = phase;
|
||||||
}
|
}
|
||||||
const begin = round(phase - this.origin);
|
|
||||||
this.phase = begin - latency;
|
|
||||||
const end = round(begin + duration);
|
|
||||||
const time = getTime();
|
|
||||||
try {
|
try {
|
||||||
const haps = this.pattern.queryArc(begin, end); // get Haps
|
const time = getTime();
|
||||||
|
const begin = this.lastEnd;
|
||||||
|
this.lastBegin = begin;
|
||||||
|
const end = round(begin + duration * this.cps);
|
||||||
|
this.lastEnd = end;
|
||||||
|
const haps = this.pattern.queryArc(begin, end);
|
||||||
|
const tickdeadline = phase - time; // time left till phase begins
|
||||||
|
this.lastTick = time + tickdeadline;
|
||||||
|
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
if (hap.part.begin.equals(hap.whole.begin)) {
|
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||||
const deadline = hap.whole.begin + this.origin - time + latency;
|
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
|
||||||
const duration = hap.duration * 1;
|
const duration = hap.duration / this.cps;
|
||||||
onTrigger?.(hap, deadline, duration);
|
onTrigger?.(hap, deadline, duration, this.cps);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger(`[cyclist] error: ${e.message}`);
|
logger(`[cyclist] error: ${e.message}`);
|
||||||
onError?.(e);
|
onError?.(e);
|
||||||
}
|
}
|
||||||
}, // called slightly before each cycle
|
},
|
||||||
interval, // duration of each cycle
|
interval, // duration of each cycle
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
getPhase() {
|
|
||||||
return this.getTime() - this.origin - this.latency;
|
|
||||||
}
|
|
||||||
now() {
|
now() {
|
||||||
return this.getTime() - this.origin + this.clock.minLatency;
|
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
|
||||||
|
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
|
||||||
}
|
}
|
||||||
setStarted(v) {
|
setStarted(v) {
|
||||||
this.started = v;
|
this.started = v;
|
||||||
@@ -69,6 +74,7 @@ export class Cyclist {
|
|||||||
stop() {
|
stop() {
|
||||||
logger('[cyclist] stop');
|
logger('[cyclist] stop');
|
||||||
this.clock.stop();
|
this.clock.stop();
|
||||||
|
this.lastEnd = 0;
|
||||||
this.setStarted(false);
|
this.setStarted(false);
|
||||||
}
|
}
|
||||||
setPattern(pat, autostart = false) {
|
setPattern(pat, autostart = false) {
|
||||||
|
|||||||
+96
-2
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
draw.mjs - <short description TODO>
|
draw.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/draw.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.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/>.
|
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/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ export const getDrawContext = (id = 'test-canvas') => {
|
|||||||
canvas.id = id;
|
canvas.id = id;
|
||||||
canvas.width = window.innerWidth;
|
canvas.width = window.innerWidth;
|
||||||
canvas.height = window.innerHeight;
|
canvas.height = window.innerHeight;
|
||||||
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0;z-index:5';
|
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
|
||||||
document.body.prepend(canvas);
|
document.body.prepend(canvas);
|
||||||
}
|
}
|
||||||
return canvas.getContext('2d');
|
return canvas.getContext('2d');
|
||||||
@@ -65,3 +65,97 @@ Pattern.prototype.onPaint = function (onPaint) {
|
|||||||
this.context = { onPaint };
|
this.context = { onPaint };
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const round = (x) => Math.round(x * 1000) / 1000;
|
||||||
|
|
||||||
|
// encapsulates starting and stopping animation frames
|
||||||
|
export class Framer {
|
||||||
|
constructor(onFrame, onError) {
|
||||||
|
this.onFrame = onFrame;
|
||||||
|
this.onError = onError;
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
const self = this;
|
||||||
|
let frame = requestAnimationFrame(function updateHighlights(time) {
|
||||||
|
try {
|
||||||
|
self.onFrame(time);
|
||||||
|
} catch (err) {
|
||||||
|
self.onError(err);
|
||||||
|
}
|
||||||
|
frame = requestAnimationFrame(updateHighlights);
|
||||||
|
});
|
||||||
|
self.cancel = () => {
|
||||||
|
cancelAnimationFrame(frame);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
if (this.cancel) {
|
||||||
|
this.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncs animation frames to a cyclist scheduler
|
||||||
|
// see vite-vanilla-repl-cm6 for an example
|
||||||
|
export class Drawer {
|
||||||
|
constructor(onDraw, drawTime) {
|
||||||
|
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
|
||||||
|
lookbehind = Math.abs(lookbehind);
|
||||||
|
this.visibleHaps = [];
|
||||||
|
this.lastFrame = null;
|
||||||
|
this.drawTime = drawTime;
|
||||||
|
this.framer = new Framer(
|
||||||
|
() => {
|
||||||
|
if (!this.scheduler) {
|
||||||
|
console.warn('Drawer: no scheduler');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// calculate current frame time (think right side of screen for pianoroll)
|
||||||
|
const phase = this.scheduler.now() + lookahead;
|
||||||
|
// first frame just captures the phase
|
||||||
|
if (this.lastFrame === null) {
|
||||||
|
this.lastFrame = phase;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// query haps from last frame till now. take last 100ms max
|
||||||
|
const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase);
|
||||||
|
this.lastFrame = phase;
|
||||||
|
this.visibleHaps = (this.visibleHaps || [])
|
||||||
|
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
|
||||||
|
.filter((h) => h.whole.end >= phase - lookbehind - lookahead)
|
||||||
|
// add new haps with onset (think right edge bars scrolling in)
|
||||||
|
.concat(haps.filter((h) => h.hasOnset()));
|
||||||
|
const time = phase - lookahead;
|
||||||
|
onDraw(this.visibleHaps, time, this);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.warn('draw error', err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
invalidate(scheduler = this.scheduler) {
|
||||||
|
if (!scheduler) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.scheduler = scheduler;
|
||||||
|
const t = scheduler.now();
|
||||||
|
let [_, lookahead] = this.drawTime;
|
||||||
|
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
|
||||||
|
// remove all future haps
|
||||||
|
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
|
||||||
|
// query future haps
|
||||||
|
const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query..
|
||||||
|
// append future haps
|
||||||
|
this.visibleHaps = this.visibleHaps.concat(futureHaps);
|
||||||
|
}
|
||||||
|
start(scheduler) {
|
||||||
|
this.scheduler = scheduler;
|
||||||
|
this.invalidate();
|
||||||
|
this.framer.start();
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
if (this.framer) {
|
||||||
|
this.framer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,15 +11,9 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, timeCat, register, silence } from './pattern.mjs';
|
import { Pattern, timeCat, register, silence } from './pattern.mjs';
|
||||||
import { rotate, flatten } from './util.mjs';
|
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
|
|
||||||
const splitAt = function (index, value) {
|
|
||||||
return [value.slice(0, index), value.slice(index)];
|
|
||||||
};
|
|
||||||
|
|
||||||
const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
|
||||||
|
|
||||||
const left = function (n, x) {
|
const left = function (n, x) {
|
||||||
const [ons, offs] = n;
|
const [ons, offs] = n;
|
||||||
const [xs, ys] = x;
|
const [xs, ys] = x;
|
||||||
|
|||||||
@@ -6,12 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { isPattern } from './index.mjs';
|
import { isPattern } from './index.mjs';
|
||||||
|
|
||||||
let scoped = false;
|
|
||||||
export const evalScope = async (...args) => {
|
export const evalScope = async (...args) => {
|
||||||
if (scoped) {
|
|
||||||
console.warn('evalScope was called more than once.');
|
|
||||||
}
|
|
||||||
scoped = true;
|
|
||||||
const results = await Promise.allSettled(args);
|
const results = await Promise.allSettled(args);
|
||||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||||
results.forEach((result, i) => {
|
results.forEach((result, i) => {
|
||||||
@@ -42,9 +37,6 @@ function safeEval(str, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const evaluate = async (code, transpiler) => {
|
export const evaluate = async (code, transpiler) => {
|
||||||
if (!scoped) {
|
|
||||||
await evalScope(); // at least scope Pattern.prototype.boostrap
|
|
||||||
}
|
|
||||||
if (transpiler) {
|
if (transpiler) {
|
||||||
code = transpiler(code); // transform syntactically correct js code to semantically usable code
|
code = transpiler(code); // transform syntactically correct js code to semantically usable code
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# vite-vanilla-repl-cm6
|
||||||
|
|
||||||
|
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!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>
|
||||||
|
<main>
|
||||||
|
<nav>
|
||||||
|
<button id="play">eval</button>
|
||||||
|
<button id="stop">stop</button>
|
||||||
|
</nav>
|
||||||
|
<div class="container">
|
||||||
|
<div id="editor"></div>
|
||||||
|
<div id="output"></div>
|
||||||
|
</div>
|
||||||
|
<canvas id="roll"></canvas>
|
||||||
|
</main>
|
||||||
|
<script type="module" src="./main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { StrudelMirror } from '@strudel/codemirror';
|
||||||
|
import { funk42 } from './tunes';
|
||||||
|
import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
|
||||||
|
import './style.css';
|
||||||
|
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
||||||
|
import { transpiler } from '@strudel.cycles/transpiler';
|
||||||
|
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
|
||||||
|
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
|
||||||
|
|
||||||
|
// init canvas
|
||||||
|
const canvas = document.getElementById('roll');
|
||||||
|
canvas.width = canvas.width * 2;
|
||||||
|
canvas.height = canvas.height * 2;
|
||||||
|
const drawContext = canvas.getContext('2d');
|
||||||
|
const drawTime = [-2, 2]; // time window of drawn haps
|
||||||
|
|
||||||
|
const editor = new StrudelMirror({
|
||||||
|
defaultOutput: webaudioOutput,
|
||||||
|
getTime: () => getAudioContext().currentTime,
|
||||||
|
transpiler,
|
||||||
|
root: document.getElementById('editor'),
|
||||||
|
initialCode: funk42,
|
||||||
|
drawTime,
|
||||||
|
onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }),
|
||||||
|
prebake: async () => {
|
||||||
|
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
|
||||||
|
const loadModules = evalScope(
|
||||||
|
controls,
|
||||||
|
import('@strudel.cycles/core'),
|
||||||
|
import('@strudel.cycles/mini'),
|
||||||
|
import('@strudel.cycles/tonal'),
|
||||||
|
import('@strudel.cycles/webaudio'),
|
||||||
|
);
|
||||||
|
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('play').addEventListener('click', () => editor.evaluate());
|
||||||
|
document.getElementById('stop').addEventListener('click', () => editor.stop());
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-vanilla-repl-cm6",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^4.3.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@strudel/codemirror": "workspace:*",
|
||||||
|
"@strudel.cycles/core": "workspace:*",
|
||||||
|
"@strudel.cycles/mini": "workspace:*",
|
||||||
|
"@strudel.cycles/soundfonts": "workspace:*",
|
||||||
|
"@strudel.cycles/tonal": "workspace:*",
|
||||||
|
"@strudel.cycles/transpiler": "workspace:*",
|
||||||
|
"@strudel.cycles/webaudio": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
body,
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: #282c34;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
flex-grow: 1;
|
||||||
|
max-height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#roll {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
|
||||||
|
await samples('github:felixroos/samples/main')
|
||||||
|
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||||
|
|
||||||
|
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
|
||||||
|
// make it 22 edo
|
||||||
|
.fmap(v => Math.pow(2,v/22))
|
||||||
|
// mess with the base frequency
|
||||||
|
.mul("<300 [300@3 200]>/8").freq()
|
||||||
|
.layer(
|
||||||
|
// chords
|
||||||
|
x=>x.div(freq(2)).s("flute").euclidLegato("<3 2>",8)
|
||||||
|
.shape(.4).lpf(sine.range(800,4000).slow(8)),
|
||||||
|
// adlibs
|
||||||
|
x=>x.arp("{0 3 2 [1 3]}%1.5")
|
||||||
|
.s('xylo').mul(freq(2))
|
||||||
|
.delay(.5).delayfeedback(.4).juxBy(.5, rev)
|
||||||
|
.hpf(sine.range(200,3000).slow(8)),
|
||||||
|
// bass
|
||||||
|
x=>x.arp("[0 [2 1?]](5,8)").s('sawtooth').div(freq(4))
|
||||||
|
.lpf(sine.range(400,2000).slow(8)).lpq(8).shape(.4)
|
||||||
|
.off(1/8, x=>x.mul(freq(2)).degradeBy(.5)).gain(.3)
|
||||||
|
).clip(1).release(.2)
|
||||||
|
.stack(
|
||||||
|
// drums
|
||||||
|
s("bd sd:<2 1>, [~ hh]*2, [~ rim]").bank('RolandTR909')
|
||||||
|
.off(1/8, x=>x.speed(2).gain(.4)).sometimes(ply(2)).gain(.8)
|
||||||
|
.mask("<0@4 1@12>/4")
|
||||||
|
.reset("<x@15 [x(3,8) x*[4 8]]>")
|
||||||
|
// wait for it...
|
||||||
|
).fast(2/3)
|
||||||
|
//.crush(6) // remove "//" if you dare`;
|
||||||
|
|
||||||
|
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
|
||||||
|
|
||||||
|
await samples('github:felixroos/samples/main')
|
||||||
|
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||||
|
|
||||||
|
addVoicings('hip', {
|
||||||
|
m11: ['2M 3m 4P 7m'],
|
||||||
|
'^7#11': ['3M 4A 5P 7M'],
|
||||||
|
}, ['C4', 'C6'])
|
||||||
|
|
||||||
|
stack(
|
||||||
|
stack(
|
||||||
|
"<Bbm11 A^7#11>/2".voicings('hip').note()
|
||||||
|
.s("gm_epiano1:2")
|
||||||
|
.arp("[<[0 1 2 3] [3 2 1 0]> ~@5]/2")
|
||||||
|
.release(2).late(.25).lpf(2000),
|
||||||
|
"<Bb1 A1>/2".note().s('gm_acoustic_bass'),
|
||||||
|
n("<0 2 3>(3,8)".off(1/8, add(4)))
|
||||||
|
.scale("<Bb4:minor A4:lydian>/2")
|
||||||
|
.s('gm_electric_guitar_jazz')
|
||||||
|
.decay(sine.range(.05, .2).slow(32)).sustain(0)
|
||||||
|
.delay(.5).lpf(sine.range(100,5000).slow(64))
|
||||||
|
.gain(.7).room(.5).pan(sine.range(0,1).slow(11))
|
||||||
|
).add(perlin.range(0,.25).note()),
|
||||||
|
stack(
|
||||||
|
s("bd:1(3,8) rim").bank('RolandTR707').slow(2).room("<0 <.1 .6>>")
|
||||||
|
.when("<0@7 1>",x=>x.echoWith(3, .0625, (x,i) => x.speed(1+i*.24))),
|
||||||
|
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.late("[0 .05]*2").late(12)
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const funk42 = `// froos - how to funk in 42 lines of code
|
||||||
|
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
|
||||||
|
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
|
||||||
|
|
||||||
|
await samples('github:felixroos/samples/main')
|
||||||
|
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||||
|
|
||||||
|
setcps(.5)
|
||||||
|
|
||||||
|
let drums = stack(
|
||||||
|
s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"),
|
||||||
|
s("hh*4").begin(.2).release(.02).end(.25).release(.02)
|
||||||
|
.gain(.3).bank('RolandTR707').late(.02).room(.5),
|
||||||
|
//s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02)
|
||||||
|
).fast(2)
|
||||||
|
|
||||||
|
let wurli = note(\`<
|
||||||
|
[[a2,g3,[b3 c4],e4] ~ [g3,c4,e4](3,8)@4 ~@2]!3
|
||||||
|
[[e2,e3,a3,b3,e4]@3 [e2,e3,ab3,b3,e4]@5]>\`)
|
||||||
|
.s("gm_epiano1:5").decay(.2).sustain("<[1 0@7]!3 1>")
|
||||||
|
.gain("<[.8@2 .4@14]!3 .7>").room(.3)
|
||||||
|
|
||||||
|
let organ = note("<[~@3 [a3,d4,f#4]@2 [[a3,c4,e4]@2 ~] ~@2]!3 ~>".add(12))
|
||||||
|
.s("gm_percussive_organ:2").gain(.6).lpf(1800).pan(.2).room(.3);
|
||||||
|
|
||||||
|
let clav = note(\`<
|
||||||
|
[~@3 a2 [g3,[b3 c4],e4]@2 ~ a2 [g3,b3,e4] ~@2 [g3,c4,e4] ~@4]!3
|
||||||
|
[~@3 e3 [[a3 b3],c3,e3]@2 ~ e2 [e3,a3]@3 [b3,e3] ~@2 [b3,e3]@2]>\`)
|
||||||
|
.s("gm_clavinet:1").decay("<.25!3 [.25 .4]>").sustain(0)
|
||||||
|
.gain(.7).pan(.8).room(.2);
|
||||||
|
|
||||||
|
let bass = note(\`<
|
||||||
|
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
|
||||||
|
[a1 [~ [g2 a2]] [e3 d3] [c3 [g3 a3]]]
|
||||||
|
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
|
||||||
|
[e2@6 e1@5 e1 [[d2 e3] g1]@4]
|
||||||
|
>\`).s("gm_electric_bass_pick:1").release(.1)
|
||||||
|
|
||||||
|
stack(
|
||||||
|
drums
|
||||||
|
,wurli
|
||||||
|
,organ
|
||||||
|
,clav
|
||||||
|
,bass
|
||||||
|
)`;
|
||||||
@@ -6,5 +6,3 @@ This folder demonstrates how to set up a strudel repl using vite and vanilla JS.
|
|||||||
npm i
|
npm i
|
||||||
npm run dev
|
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)
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^3.2.0"
|
"vite": "^4.3.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
|
|||||||
+18
-1
@@ -32,7 +32,11 @@ export class Hap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get duration() {
|
get duration() {
|
||||||
return this.whole.end.sub(this.whole.begin);
|
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
get endClipped() {
|
||||||
|
return this.whole.begin.add(this.duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
wholeOrPart() {
|
wholeOrPart() {
|
||||||
@@ -126,6 +130,19 @@ export class Hap {
|
|||||||
setContext(context) {
|
setContext(context) {
|
||||||
return new Hap(this.whole, this.part, this.value, context);
|
return new Hap(this.whole, this.part, this.value, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureObjectValue() {
|
||||||
|
/* if (isNote(hap.value)) {
|
||||||
|
// supports primitive hap values that look like notes
|
||||||
|
hap.value = { note: hap.value };
|
||||||
|
} */
|
||||||
|
if (typeof this.value !== 'object') {
|
||||||
|
throw new Error(
|
||||||
|
`expected hap.value to be an object, but got "${this.value}". Hint: append .note() or .s() to the end`,
|
||||||
|
'error',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Hap;
|
export default Hap;
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ export * from './util.mjs';
|
|||||||
export * from './speak.mjs';
|
export * from './speak.mjs';
|
||||||
export * from './evaluate.mjs';
|
export * from './evaluate.mjs';
|
||||||
export * from './repl.mjs';
|
export * from './repl.mjs';
|
||||||
|
export * from './cyclist.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './time.mjs';
|
export * from './time.mjs';
|
||||||
export * from './draw.mjs';
|
export * from './draw.mjs';
|
||||||
export * from './animate.mjs';
|
export * from './animate.mjs';
|
||||||
export * from './pianoroll.mjs';
|
export * from './pianoroll.mjs';
|
||||||
|
export * from './spiral.mjs';
|
||||||
export * from './ui.mjs';
|
export * from './ui.mjs';
|
||||||
export { default as drawLine } from './drawLine.mjs';
|
export { default as drawLine } from './drawLine.mjs';
|
||||||
export { default as gist } from './gist.js';
|
export { default as gist } from './gist.js';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/core",
|
"name": "@strudel.cycles/core",
|
||||||
"version": "0.6.8",
|
"version": "0.8.2",
|
||||||
"description": "Port of Tidal Cycles to JavaScript",
|
"description": "Port of Tidal Cycles to JavaScript",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^3.2.2",
|
"vite": "^4.3.3",
|
||||||
"vitest": "^0.25.7"
|
"vitest": "^0.28.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+176
-14
@@ -27,6 +27,7 @@ export class Pattern {
|
|||||||
* Create a pattern. As an end user, you will most likely not create a Pattern directly.
|
* Create a pattern. As an end user, you will most likely not create a Pattern directly.
|
||||||
*
|
*
|
||||||
* @param {function} query - The function that maps a {@link State} to an array of {@link Hap}.
|
* @param {function} query - The function that maps a {@link State} to an array of {@link Hap}.
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
constructor(query) {
|
constructor(query) {
|
||||||
this.query = query;
|
this.query = query;
|
||||||
@@ -51,6 +52,7 @@ export class Pattern {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* see {@link Pattern#withValue}
|
* see {@link Pattern#withValue}
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
fmap(func) {
|
fmap(func) {
|
||||||
return this.withValue(func);
|
return this.withValue(func);
|
||||||
@@ -62,6 +64,7 @@ export class Pattern {
|
|||||||
* pattern of functions.
|
* pattern of functions.
|
||||||
* @param {Function} whole_func
|
* @param {Function} whole_func
|
||||||
* @param {Function} func
|
* @param {Function} func
|
||||||
|
* @noAutocomplete
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
appWhole(whole_func, pat_val) {
|
appWhole(whole_func, pat_val) {
|
||||||
@@ -97,6 +100,7 @@ export class Pattern {
|
|||||||
* are not the same but do intersect, the resulting hap has a timespan of the
|
* are not the same but do intersect, the resulting hap has a timespan of the
|
||||||
* intersection. This applies to both the part and the whole timespan.
|
* intersection. This applies to both the part and the whole timespan.
|
||||||
* @param {Pattern} pat_val
|
* @param {Pattern} pat_val
|
||||||
|
* @noAutocomplete
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
appBoth(pat_val) {
|
appBoth(pat_val) {
|
||||||
@@ -117,6 +121,7 @@ export class Pattern {
|
|||||||
* are preserved from the pattern of functions (often referred to as the left
|
* are preserved from the pattern of functions (often referred to as the left
|
||||||
* hand or inner pattern).
|
* hand or inner pattern).
|
||||||
* @param {Pattern} pat_val
|
* @param {Pattern} pat_val
|
||||||
|
* @noAutocomplete
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
appLeft(pat_val) {
|
appLeft(pat_val) {
|
||||||
@@ -147,6 +152,7 @@ export class Pattern {
|
|||||||
* pattern of values, i.e. structure is preserved from the right hand/outer
|
* pattern of values, i.e. structure is preserved from the right hand/outer
|
||||||
* pattern.
|
* pattern.
|
||||||
* @param {Pattern} pat_val
|
* @param {Pattern} pat_val
|
||||||
|
* @noAutocomplete
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
appRight(pat_val) {
|
appRight(pat_val) {
|
||||||
@@ -332,6 +338,7 @@ export class Pattern {
|
|||||||
* const haps = pattern.queryArc(0, 1)
|
* const haps = pattern.queryArc(0, 1)
|
||||||
* console.log(haps)
|
* console.log(haps)
|
||||||
* silence
|
* silence
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
queryArc(begin, end) {
|
queryArc(begin, end) {
|
||||||
try {
|
try {
|
||||||
@@ -347,6 +354,7 @@ export class Pattern {
|
|||||||
* some calculations easier to express, as all haps are then constrained to
|
* some calculations easier to express, as all haps are then constrained to
|
||||||
* happen within a cycle.
|
* happen within a cycle.
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
splitQueries() {
|
splitQueries() {
|
||||||
const pat = this;
|
const pat = this;
|
||||||
@@ -361,6 +369,7 @@ export class Pattern {
|
|||||||
* timespan before passing it to the original pattern.
|
* timespan before passing it to the original pattern.
|
||||||
* @param {Function} func the function to apply
|
* @param {Function} func the function to apply
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
withQuerySpan(func) {
|
withQuerySpan(func) {
|
||||||
return new Pattern((state) => this.query(state.withSpan(func)));
|
return new Pattern((state) => this.query(state.withSpan(func)));
|
||||||
@@ -382,6 +391,7 @@ export class Pattern {
|
|||||||
* begin and end time of the query timespan.
|
* begin and end time of the query timespan.
|
||||||
* @param {Function} func the function to apply
|
* @param {Function} func the function to apply
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
withQueryTime(func) {
|
withQueryTime(func) {
|
||||||
return new Pattern((state) => this.query(state.withSpan((span) => span.withTime(func))));
|
return new Pattern((state) => this.query(state.withSpan((span) => span.withTime(func))));
|
||||||
@@ -393,6 +403,7 @@ export class Pattern {
|
|||||||
* present, `whole` timespans).
|
* present, `whole` timespans).
|
||||||
* @param {Function} func
|
* @param {Function} func
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
withHapSpan(func) {
|
withHapSpan(func) {
|
||||||
return new Pattern((state) => this.query(state).map((hap) => hap.withSpan(func)));
|
return new Pattern((state) => this.query(state).map((hap) => hap.withSpan(func)));
|
||||||
@@ -403,6 +414,7 @@ export class Pattern {
|
|||||||
* begin and end time of the hap timespans.
|
* begin and end time of the hap timespans.
|
||||||
* @param {Function} func the function to apply
|
* @param {Function} func the function to apply
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
withHapTime(func) {
|
withHapTime(func) {
|
||||||
return this.withHapSpan((span) => span.withTime(func));
|
return this.withHapSpan((span) => span.withTime(func));
|
||||||
@@ -412,6 +424,7 @@ export class Pattern {
|
|||||||
* Returns a new pattern with the given function applied to the list of haps returned by every query.
|
* Returns a new pattern with the given function applied to the list of haps returned by every query.
|
||||||
* @param {Function} func
|
* @param {Function} func
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
withHaps(func) {
|
withHaps(func) {
|
||||||
return new Pattern((state) => func(this.query(state)));
|
return new Pattern((state) => func(this.query(state)));
|
||||||
@@ -421,6 +434,7 @@ export class Pattern {
|
|||||||
* As with {@link Pattern#withHaps}, but applies the function to every hap, rather than every list of haps.
|
* As with {@link Pattern#withHaps}, but applies the function to every hap, rather than every list of haps.
|
||||||
* @param {Function} func
|
* @param {Function} func
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
withHap(func) {
|
withHap(func) {
|
||||||
return this.withHaps((haps) => haps.map(func));
|
return this.withHaps((haps) => haps.map(func));
|
||||||
@@ -430,6 +444,7 @@ export class Pattern {
|
|||||||
* Returns a new pattern with the context field set to every hap set to the given value.
|
* Returns a new pattern with the context field set to every hap set to the given value.
|
||||||
* @param {*} context
|
* @param {*} context
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
setContext(context) {
|
setContext(context) {
|
||||||
return this.withHap((hap) => hap.setContext(context));
|
return this.withHap((hap) => hap.setContext(context));
|
||||||
@@ -439,6 +454,7 @@ export class Pattern {
|
|||||||
* Returns a new pattern with the given function applied to the context field of every hap.
|
* Returns a new pattern with the given function applied to the context field of every hap.
|
||||||
* @param {Function} func
|
* @param {Function} func
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
withContext(func) {
|
withContext(func) {
|
||||||
return this.withHap((hap) => hap.setContext(func(hap.context)));
|
return this.withHap((hap) => hap.setContext(func(hap.context)));
|
||||||
@@ -447,6 +463,7 @@ export class Pattern {
|
|||||||
/**
|
/**
|
||||||
* Returns a new pattern with the context field of every hap set to an empty object.
|
* Returns a new pattern with the context field of every hap set to an empty object.
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
stripContext() {
|
stripContext() {
|
||||||
return this.withHap((hap) => hap.setContext({}));
|
return this.withHap((hap) => hap.setContext({}));
|
||||||
@@ -458,6 +475,7 @@ export class Pattern {
|
|||||||
* @param {Number} start
|
* @param {Number} start
|
||||||
* @param {Number} end
|
* @param {Number} end
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
withLocation(start, end) {
|
withLocation(start, end) {
|
||||||
const location = {
|
const location = {
|
||||||
@@ -500,6 +518,7 @@ export class Pattern {
|
|||||||
* Returns a new Pattern, which only returns haps that meet the given test.
|
* Returns a new Pattern, which only returns haps that meet the given test.
|
||||||
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
|
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
filterHaps(hap_test) {
|
filterHaps(hap_test) {
|
||||||
return new Pattern((state) => this.query(state).filter(hap_test));
|
return new Pattern((state) => this.query(state).filter(hap_test));
|
||||||
@@ -510,6 +529,7 @@ export class Pattern {
|
|||||||
* inside haps.
|
* inside haps.
|
||||||
* @param {Function} value_test
|
* @param {Function} value_test
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
filterValues(value_test) {
|
filterValues(value_test) {
|
||||||
return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value)));
|
return new Pattern((state) => this.query(state).filter((hap) => value_test(hap.value)));
|
||||||
@@ -519,6 +539,7 @@ export class Pattern {
|
|||||||
* Returns a new pattern, with haps containing undefined values removed from
|
* Returns a new pattern, with haps containing undefined values removed from
|
||||||
* query results.
|
* query results.
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
removeUndefineds() {
|
removeUndefineds() {
|
||||||
return this.filterValues((val) => val != undefined);
|
return this.filterValues((val) => val != undefined);
|
||||||
@@ -529,6 +550,7 @@ export class Pattern {
|
|||||||
* with an onset is one with a `whole` timespan that begins at the same time
|
* with an onset is one with a `whole` timespan that begins at the same time
|
||||||
* as its `part` timespan.
|
* as its `part` timespan.
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
onsetsOnly() {
|
onsetsOnly() {
|
||||||
// Returns a new pattern that will only return haps where the start
|
// Returns a new pattern that will only return haps where the start
|
||||||
@@ -541,6 +563,7 @@ export class Pattern {
|
|||||||
* Returns a new pattern, with 'continuous' haps (those without 'whole'
|
* Returns a new pattern, with 'continuous' haps (those without 'whole'
|
||||||
* timespans) removed from query results.
|
* timespans) removed from query results.
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
discreteOnly() {
|
discreteOnly() {
|
||||||
// removes continuous haps that don't have a 'whole' timespan
|
// removes continuous haps that don't have a 'whole' timespan
|
||||||
@@ -550,6 +573,7 @@ export class Pattern {
|
|||||||
/**
|
/**
|
||||||
* Combines adjacent haps with the same value and whole. Only
|
* Combines adjacent haps with the same value and whole. Only
|
||||||
* intended for use in tests.
|
* intended for use in tests.
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
defragmentHaps() {
|
defragmentHaps() {
|
||||||
// remove continuous haps
|
// remove continuous haps
|
||||||
@@ -604,6 +628,7 @@ export class Pattern {
|
|||||||
* @param {Boolean} with_context - set to true, otherwise the context field
|
* @param {Boolean} with_context - set to true, otherwise the context field
|
||||||
* will be stripped from the resulting haps.
|
* will be stripped from the resulting haps.
|
||||||
* @returns [Hap]
|
* @returns [Hap]
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
firstCycle(with_context = false) {
|
firstCycle(with_context = false) {
|
||||||
var self = this;
|
var self = this;
|
||||||
@@ -615,6 +640,7 @@ export class Pattern {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Accessor for a list of values returned by querying the first cycle.
|
* Accessor for a list of values returned by querying the first cycle.
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
get firstCycleValues() {
|
get firstCycleValues() {
|
||||||
return this.firstCycle().map((hap) => hap.value);
|
return this.firstCycle().map((hap) => hap.value);
|
||||||
@@ -622,6 +648,7 @@ export class Pattern {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* More human-readable version of the {@link Pattern#firstCycleValues} accessor.
|
* More human-readable version of the {@link Pattern#firstCycleValues} accessor.
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
get showFirstCycle() {
|
get showFirstCycle() {
|
||||||
return this.firstCycle().map(
|
return this.firstCycle().map(
|
||||||
@@ -633,6 +660,7 @@ export class Pattern {
|
|||||||
* Returns a new pattern, which returns haps sorted in temporal order. Mainly
|
* Returns a new pattern, which returns haps sorted in temporal order. Mainly
|
||||||
* of use when comparing two patterns for equality, in tests.
|
* of use when comparing two patterns for equality, in tests.
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
sortHapsByPart() {
|
sortHapsByPart() {
|
||||||
return this.withHaps((haps) =>
|
return this.withHaps((haps) =>
|
||||||
@@ -812,7 +840,7 @@ export class Pattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// functions relating to chords/patterns of lists
|
// functions relating to chords/patterns of lists/lists of patterns
|
||||||
|
|
||||||
// returns Array<Hap[]> where each list of haps satisfies eq
|
// returns Array<Hap[]> where each list of haps satisfies eq
|
||||||
function groupHapsBy(eq, haps) {
|
function groupHapsBy(eq, haps) {
|
||||||
@@ -861,6 +889,35 @@ Pattern.prototype.arp = function (pat) {
|
|||||||
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
|
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
|
||||||
|
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
|
||||||
|
* @name weave
|
||||||
|
* @memberof Pattern
|
||||||
|
* @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd"))
|
||||||
|
* @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd"))
|
||||||
|
|
||||||
|
addToPrototype('weave', function (t, ...pats) {
|
||||||
|
return this.weaveWith(t, ...pats.map((x) => set.out(x)));
|
||||||
|
});
|
||||||
|
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* Like 'weave', but accepts functions rather than patterns, which are applied to the pattern.
|
||||||
|
* @name weaveWith
|
||||||
|
* @memberof Pattern
|
||||||
|
|
||||||
|
addToPrototype('weaveWith', function (t, ...funcs) {
|
||||||
|
const pat = this;
|
||||||
|
const l = funcs.length;
|
||||||
|
t = Fraction(t);
|
||||||
|
if (l == 0) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
|
return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// compose matrix functions
|
// compose matrix functions
|
||||||
|
|
||||||
@@ -1070,6 +1127,8 @@ function _composeOp(a, b, func) {
|
|||||||
export const polyrhythm = stack;
|
export const polyrhythm = stack;
|
||||||
export const pr = stack;
|
export const pr = stack;
|
||||||
|
|
||||||
|
export const pm = polymeter;
|
||||||
|
|
||||||
// methods that create patterns, which are added to patternified Pattern methods
|
// methods that create patterns, which are added to patternified Pattern methods
|
||||||
// TODO: remove? this is only used in old transpiler (shapeshifter)
|
// TODO: remove? this is only used in old transpiler (shapeshifter)
|
||||||
Pattern.prototype.factories = {
|
Pattern.prototype.factories = {
|
||||||
@@ -1103,6 +1162,7 @@ export const silence = new Pattern(() => []);
|
|||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* pure('e4') // "e4"
|
* pure('e4') // "e4"
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
export function pure(value) {
|
export function pure(value) {
|
||||||
function query(state) {
|
function query(state) {
|
||||||
@@ -1226,6 +1286,20 @@ export function timeCat(...timepats) {
|
|||||||
return stack(...pats);
|
return stack(...pats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to arrange multiple patterns together over multiple cycles.
|
||||||
|
* Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use.
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
* @example
|
||||||
|
* arrange([4, "<c a f e>(3,8)"],[2, "<g a>(5,8)"]).note()
|
||||||
|
*/
|
||||||
|
export function arrange(...sections) {
|
||||||
|
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
|
||||||
|
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
|
||||||
|
return timeCat(...sections).slow(total);
|
||||||
|
}
|
||||||
|
|
||||||
export function fastcat(...pats) {
|
export function fastcat(...pats) {
|
||||||
return slowcat(...pats)._fast(pats.length);
|
return slowcat(...pats)._fast(pats.length);
|
||||||
}
|
}
|
||||||
@@ -1302,11 +1376,6 @@ export function polymeter(...args) {
|
|||||||
return polymeterSteps(0, ...args);
|
return polymeterSteps(0, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// alias
|
|
||||||
export function pm(...args) {
|
|
||||||
polymeter(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mask = curry((a, b) => reify(b).mask(a));
|
export const mask = curry((a, b) => reify(b).mask(a));
|
||||||
export const struct = curry((a, b) => reify(b).struct(a));
|
export const struct = curry((a, b) => reify(b).struct(a));
|
||||||
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
|
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
|
||||||
@@ -1343,19 +1412,21 @@ export const func = curry((a, b) => reify(b).func(a));
|
|||||||
*
|
*
|
||||||
* @param {string} name name of the function
|
* @param {string} name name of the function
|
||||||
* @param {function} func function with 1 or more params, where last is the current pattern
|
* @param {function} func function with 1 or more params, where last is the current pattern
|
||||||
|
* @noAutocomplete
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function register(name, func) {
|
export function register(name, func, patternify = true) {
|
||||||
if (Array.isArray(name)) {
|
if (Array.isArray(name)) {
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const name_item of name) {
|
for (const name_item of name) {
|
||||||
result[name_item] = register(name_item, func);
|
result[name_item] = register(name_item, func, patternify);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
const arity = func.length;
|
const arity = func.length;
|
||||||
var pfunc; // the patternified function
|
var pfunc; // the patternified function
|
||||||
|
|
||||||
|
if (patternify) {
|
||||||
pfunc = function (...args) {
|
pfunc = function (...args) {
|
||||||
args = args.map(reify);
|
args = args.map(reify);
|
||||||
const pat = args[args.length - 1];
|
const pat = args[args.length - 1];
|
||||||
@@ -1375,9 +1446,14 @@ export function register(name, func) {
|
|||||||
mapFn = curry(mapFn, null, arity - 1);
|
mapFn = curry(mapFn, null, arity - 1);
|
||||||
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
|
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
pfunc = function (...args) {
|
||||||
|
args = args.map(reify);
|
||||||
|
return func(...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Pattern.prototype[name] = function (...args) {
|
Pattern.prototype[name] = function (...args) {
|
||||||
args = args.map(reify);
|
|
||||||
// For methods that take a single argument (plus 'this'), allow
|
// For methods that take a single argument (plus 'this'), allow
|
||||||
// multiple arguments but sequence them
|
// multiple arguments but sequence them
|
||||||
if (arity === 2 && args.length !== 1) {
|
if (arity === 2 && args.length !== 1) {
|
||||||
@@ -1385,6 +1461,7 @@ export function register(name, func) {
|
|||||||
} else if (arity !== args.length + 1) {
|
} else if (arity !== args.length + 1) {
|
||||||
throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`);
|
throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`);
|
||||||
}
|
}
|
||||||
|
args = args.map(reify);
|
||||||
return pfunc(...args, this);
|
return pfunc(...args, this);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1448,15 +1525,17 @@ export const ceil = register('ceil', function (pat) {
|
|||||||
* Assumes a numerical pattern, containing unipolar values in the range 0 ..
|
* Assumes a numerical pattern, containing unipolar values in the range 0 ..
|
||||||
* 1. Returns a new pattern with values scaled to the bipolar range -1 .. 1
|
* 1. Returns a new pattern with values scaled to the bipolar range -1 .. 1
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
export const toBipolar = register('toBipolar', function (pat) {
|
export const toBipolar = register('toBipolar', function (pat) {
|
||||||
return pat.fmap((x) => x * 2 - 1);
|
return pat.fmap((x) => x * 2 - 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assumes a numerical pattern, containing bipolar values in the range -1 ..
|
* Assumes a numerical pattern, containing bipolar values in the range -1 .. 1
|
||||||
* 1. Returns a new pattern with values scaled to the unipolar range 0 .. 1
|
* Returns a new pattern with values scaled to the unipolar range 0 .. 1
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
export const fromBipolar = register('fromBipolar', function (pat) {
|
export const fromBipolar = register('fromBipolar', function (pat) {
|
||||||
return pat.fmap((x) => (x + 1) / 2);
|
return pat.fmap((x) => (x + 1) / 2);
|
||||||
@@ -1503,6 +1582,24 @@ export const range2 = register('range2', function (min, max, pat) {
|
|||||||
return pat.fromBipolar()._range(min, max);
|
return pat.fromBipolar()._range(min, max);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows dividing numbers via list notation using ":".
|
||||||
|
* Returns a new pattern with just numbers.
|
||||||
|
* @name ratio
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* ratio("1, 5:4, 3:2").mul(110).freq().s("piano").slow(2)
|
||||||
|
*/
|
||||||
|
export const ratio = register('ratio', (pat) =>
|
||||||
|
pat.fmap((v) => {
|
||||||
|
if (!Array.isArray(v)) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
return v.slice(1).reduce((acc, n) => acc / n, v[0]);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// Structural and temporal transformations
|
// Structural and temporal transformations
|
||||||
|
|
||||||
@@ -1598,6 +1695,9 @@ export const ply = register('ply', function (factor, pat) {
|
|||||||
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
|
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
|
||||||
*/
|
*/
|
||||||
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
|
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
|
||||||
|
if (factor === 0) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
factor = Fraction(factor);
|
factor = Fraction(factor);
|
||||||
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
||||||
return fastQuery.withHapTime((t) => t.div(factor));
|
return fastQuery.withHapTime((t) => t.div(factor));
|
||||||
@@ -1624,6 +1724,9 @@ export const hurry = register('hurry', function (r, pat) {
|
|||||||
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
|
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
|
||||||
*/
|
*/
|
||||||
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
|
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
|
||||||
|
if (factor === 0) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
return pat._fast(Fraction(1).div(factor));
|
return pat._fast(Fraction(1).div(factor));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1950,7 +2053,7 @@ export const jux = register('jux', function (func, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* "<0 [2 4]>"
|
* "<0 [2 4]>"
|
||||||
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
|
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
|
||||||
* .scale('C minor').note().legato(.2)
|
* .scale('C minor').note().clip(.2)
|
||||||
*/
|
*/
|
||||||
export const { echoWith, echowith, stutWith, stutwith } = register(
|
export const { echoWith, echowith, stutWith, stutwith } = register(
|
||||||
['echoWith', 'echowith', 'stutWith', 'stutwith'],
|
['echoWith', 'echowith', 'stutWith', 'stutwith'],
|
||||||
@@ -2100,6 +2203,7 @@ export const velocity = register('velocity', function (velocity, pat) {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Multiplies the hap duration with the given factor.
|
* Multiplies the hap duration with the given factor.
|
||||||
|
* With samples, `clip` might be a better function to use ([more info](https://github.com/tidalcycles/strudel/pull/598))
|
||||||
* @name legato
|
* @name legato
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @example
|
* @example
|
||||||
@@ -2162,7 +2266,65 @@ const _loopAt = function (factor, pat, cps = 1) {
|
|||||||
.slow(factor);
|
.slow(factor);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
|
/*
|
||||||
|
* Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers.
|
||||||
|
* @name slice
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* await samples('github:tidalcycles/Dirt-Samples/master')
|
||||||
|
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const slice = register(
|
||||||
|
'slice',
|
||||||
|
function (npat, ipat, opat) {
|
||||||
|
return npat.innerBind((n) =>
|
||||||
|
ipat.outerBind((i) =>
|
||||||
|
opat.outerBind((o) => {
|
||||||
|
// If it's not an object, assume it's a string and make it a 's' control parameter
|
||||||
|
o = o instanceof Object ? o : { s: o };
|
||||||
|
// Remember we must stay pure and avoid editing the object directly
|
||||||
|
const toAdd = { begin: i / n, end: (i + 1) / n, _slices: n };
|
||||||
|
return pure({ ...toAdd, ...o });
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
false, // turns off auto-patternification
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
|
||||||
|
* @name splice
|
||||||
|
* @memberof Pattern
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* await samples('github:tidalcycles/Dirt-Samples/master')
|
||||||
|
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const splice = register(
|
||||||
|
'splice',
|
||||||
|
function (npat, ipat, opat) {
|
||||||
|
const sliced = slice(npat, ipat, opat);
|
||||||
|
return sliced.withHap(function (hap) {
|
||||||
|
return hap.withValue((v) => ({
|
||||||
|
...{
|
||||||
|
speed: (1 / v._slices / hap.whole.duration) * (v.speed || 1),
|
||||||
|
unit: 'c',
|
||||||
|
},
|
||||||
|
...v,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
false, // turns off auto-patternification
|
||||||
|
);
|
||||||
|
|
||||||
|
// this function will be redefined in repl.mjs to use the correct cps value.
|
||||||
|
// It is still here to work in cases where repl.mjs is not used
|
||||||
|
|
||||||
|
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
|
||||||
return _loopAt(factor, pat, 1);
|
return _loopAt(factor, pat, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2179,6 +2341,6 @@ const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat)
|
|||||||
* s("rhodes").loopAtCps(4,1.5).cps(1.5)
|
* s("rhodes").loopAtCps(4,1.5).cps(1.5)
|
||||||
*/
|
*/
|
||||||
// TODO - global cps clock
|
// TODO - global cps clock
|
||||||
const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
|
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
|
||||||
return _loopAt(factor, pat, cps);
|
return _loopAt(factor, pat, cps);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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/>.
|
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, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
||||||
|
|
||||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||||
const getValue = (e) => {
|
const getValue = (e) => {
|
||||||
@@ -18,7 +18,7 @@ const getValue = (e) => {
|
|||||||
}
|
}
|
||||||
note = note ?? n;
|
note = note ?? n;
|
||||||
if (typeof note === 'string') {
|
if (typeof note === 'string') {
|
||||||
return toMidi(note);
|
return noteToMidi(note);
|
||||||
}
|
}
|
||||||
if (typeof note === 'number') {
|
if (typeof note === 'number') {
|
||||||
return note;
|
return note;
|
||||||
@@ -50,6 +50,7 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
timeframe: timeframeProp,
|
timeframe: timeframeProp,
|
||||||
fold = 0,
|
fold = 0,
|
||||||
vertical = 0,
|
vertical = 0,
|
||||||
|
labels = 0,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const ctx = getDrawContext();
|
const ctx = getDrawContext();
|
||||||
const w = ctx.canvas.width;
|
const w = ctx.canvas.width;
|
||||||
@@ -82,12 +83,12 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
ctx.fillRect(0, 0, w, h);
|
ctx.fillRect(0, 0, w, h);
|
||||||
}
|
}
|
||||||
const inFrame = (event) =>
|
const inFrame = (event) =>
|
||||||
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
|
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
|
||||||
events.filter(inFrame).forEach((event) => {
|
events.filter(inFrame).forEach((event) => {
|
||||||
const isActive = event.whole.begin <= t && event.whole.end > t;
|
const isActive = event.whole.begin <= t && event.endClipped > t;
|
||||||
ctx.fillStyle = event.context?.color || inactive;
|
ctx.fillStyle = event.context?.color || inactive;
|
||||||
ctx.strokeStyle = event.context?.color || active;
|
ctx.strokeStyle = event.context?.color || active;
|
||||||
ctx.globalAlpha = event.context.velocity ?? 1;
|
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
|
||||||
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
||||||
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||||
const value = getValue(event);
|
const value = getValue(event);
|
||||||
@@ -114,6 +115,14 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
||||||
|
if (labels) {
|
||||||
|
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
|
||||||
|
ctx.font = `${barThickness * 0.75}px monospace`;
|
||||||
|
ctx.strokeStyle = 'black';
|
||||||
|
ctx.fillStyle = isActive ? 'white' : 'black';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
ctx.fillText(label, ...coords);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
ctx.globalAlpha = 1; // reset!
|
ctx.globalAlpha = 1; // reset!
|
||||||
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||||
@@ -181,6 +190,7 @@ export function pianoroll({
|
|||||||
timeframe: timeframeProp,
|
timeframe: timeframeProp,
|
||||||
fold = 0,
|
fold = 0,
|
||||||
vertical = 0,
|
vertical = 0,
|
||||||
|
labels = false,
|
||||||
ctx,
|
ctx,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const w = ctx.canvas.width;
|
const w = ctx.canvas.width;
|
||||||
@@ -240,7 +250,7 @@ export function pianoroll({
|
|||||||
const color = event.value?.color || event.context?.color;
|
const color = event.value?.color || event.context?.color;
|
||||||
ctx.fillStyle = color || inactive;
|
ctx.fillStyle = color || inactive;
|
||||||
ctx.strokeStyle = color || active;
|
ctx.strokeStyle = color || active;
|
||||||
ctx.globalAlpha = event.context.velocity ?? 1;
|
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
|
||||||
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
||||||
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||||
const value = getValue(event);
|
const value = getValue(event);
|
||||||
@@ -267,6 +277,14 @@ export function pianoroll({
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
||||||
|
if (labels) {
|
||||||
|
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
|
||||||
|
ctx.font = `${barThickness * 0.75}px monospace`;
|
||||||
|
ctx.strokeStyle = 'black';
|
||||||
|
ctx.fillStyle = isActive ? 'white' : 'black';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
ctx.fillText(label, ...coords);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
ctx.globalAlpha = 1; // reset!
|
ctx.globalAlpha = 1; // reset!
|
||||||
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||||
@@ -284,7 +302,7 @@ export function pianoroll({
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOptions(drawTime, options = {}) {
|
export function getDrawOptions(drawTime, options = {}) {
|
||||||
let [lookbehind, lookahead] = drawTime;
|
let [lookbehind, lookahead] = drawTime;
|
||||||
lookbehind = Math.abs(lookbehind);
|
lookbehind = Math.abs(lookbehind);
|
||||||
const cycles = lookahead + lookbehind;
|
const cycles = lookahead + lookbehind;
|
||||||
@@ -293,5 +311,18 @@ function getOptions(drawTime, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.punchcard = function (options) {
|
Pattern.prototype.punchcard = function (options) {
|
||||||
return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
|
return this.onPaint((ctx, time, haps, drawTime) =>
|
||||||
|
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, options) }),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Pattern.prototype.pianoroll = function (options) {
|
||||||
|
return this.onPaint((ctx, time, haps, drawTime) =>
|
||||||
|
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),
|
||||||
|
);
|
||||||
|
}; */
|
||||||
|
|
||||||
|
export function drawPianoroll(options) {
|
||||||
|
const { drawTime, ...rest } = options;
|
||||||
|
pianoroll({ ...getDrawOptions(drawTime), ...rest });
|
||||||
|
}
|
||||||
|
|||||||
+53
-18
@@ -2,6 +2,8 @@ import { Cyclist } from './cyclist.mjs';
|
|||||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { setTime } from './time.mjs';
|
import { setTime } from './time.mjs';
|
||||||
|
import { evalScope } from './evaluate.mjs';
|
||||||
|
import { register } from './pattern.mjs';
|
||||||
|
|
||||||
export function repl({
|
export function repl({
|
||||||
interval,
|
interval,
|
||||||
@@ -17,36 +19,26 @@ export function repl({
|
|||||||
}) {
|
}) {
|
||||||
const scheduler = new Cyclist({
|
const scheduler = new Cyclist({
|
||||||
interval,
|
interval,
|
||||||
onTrigger: async (hap, deadline, duration) => {
|
onTrigger: getTrigger({ defaultOutput, getTime }),
|
||||||
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,
|
onError: onSchedulerError,
|
||||||
getTime,
|
getTime,
|
||||||
onToggle,
|
onToggle,
|
||||||
});
|
});
|
||||||
|
const setPattern = (pattern, autostart = true) => {
|
||||||
|
pattern = editPattern?.(pattern) || pattern;
|
||||||
|
scheduler.setPattern(pattern, autostart);
|
||||||
|
};
|
||||||
setTime(() => scheduler.now()); // TODO: refactor?
|
setTime(() => scheduler.now()); // TODO: refactor?
|
||||||
const evaluate = async (code, autostart = true) => {
|
const evaluate = async (code, autostart = true) => {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error('no code to evaluate');
|
throw new Error('no code to evaluate');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
beforeEval?.({ code });
|
await beforeEval?.({ code });
|
||||||
let { pattern } = await _evaluate(code, transpiler);
|
let { pattern } = await _evaluate(code, transpiler);
|
||||||
|
|
||||||
logger(`[eval] code updated`);
|
logger(`[eval] code updated`);
|
||||||
pattern = editPattern?.(pattern) || pattern;
|
setPattern(pattern, autostart);
|
||||||
scheduler.setPattern(pattern, autostart);
|
|
||||||
afterEval?.({ code, pattern });
|
afterEval?.({ code, pattern });
|
||||||
return pattern;
|
return pattern;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -58,5 +50,48 @@ export function repl({
|
|||||||
const stop = () => scheduler.stop();
|
const stop = () => scheduler.stop();
|
||||||
const start = () => scheduler.start();
|
const start = () => scheduler.start();
|
||||||
const pause = () => scheduler.pause();
|
const pause = () => scheduler.pause();
|
||||||
return { scheduler, evaluate, start, stop, pause };
|
const setCps = (cps) => scheduler.setCps(cps);
|
||||||
|
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
||||||
|
|
||||||
|
// the following functions use the cps value, which is why they are defined here..
|
||||||
|
const loopAt = register('loopAt', (cycles, pat) => {
|
||||||
|
return pat.loopAtCps(cycles, scheduler.cps);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fit = register('fit', (pat) =>
|
||||||
|
pat.withHap((hap) =>
|
||||||
|
hap.withValue((v) => ({
|
||||||
|
...v,
|
||||||
|
speed: scheduler.cps / hap.whole.duration, // overwrite speed completely?
|
||||||
|
unit: 'c',
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
evalScope({
|
||||||
|
loopAt,
|
||||||
|
fit,
|
||||||
|
setCps,
|
||||||
|
setcps: setCps,
|
||||||
|
setCpm,
|
||||||
|
setcpm: setCpm,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getTrigger =
|
||||||
|
({ getTime, defaultOutput }) =>
|
||||||
|
async (hap, deadline, duration, cps) => {
|
||||||
|
try {
|
||||||
|
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||||
|
await defaultOutput(hap, deadline, duration, cps);
|
||||||
|
}
|
||||||
|
if (hap.context.onTrigger) {
|
||||||
|
// call signature of output / onTrigger is different...
|
||||||
|
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const isaw2 = isaw.toBipolar();
|
|||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
|
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
|
||||||
* @example
|
* @example
|
||||||
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { Pattern } from './index.mjs';
|
||||||
|
|
||||||
|
// polar coords -> xy
|
||||||
|
function fromPolar(angle, radius, cx, cy) {
|
||||||
|
const radians = ((angle - 90) * Math.PI) / 180;
|
||||||
|
return [cx + Math.cos(radians) * radius, cy + Math.sin(radians) * radius];
|
||||||
|
}
|
||||||
|
|
||||||
|
const xyOnSpiral = (angle, margin, cx, cy, rotate = 0) => fromPolar((angle + rotate) * 360, margin * angle, cx, cy); // TODO: logSpiral
|
||||||
|
|
||||||
|
// draw spiral / segment of spiral
|
||||||
|
function spiralSegment(options) {
|
||||||
|
let {
|
||||||
|
ctx,
|
||||||
|
from = 0,
|
||||||
|
to = 3,
|
||||||
|
margin = 50,
|
||||||
|
cx = 100,
|
||||||
|
cy = 100,
|
||||||
|
rotate = 0,
|
||||||
|
thickness = margin / 2,
|
||||||
|
color = '#0000ff30',
|
||||||
|
cap = 'round',
|
||||||
|
stretch = 1,
|
||||||
|
fromOpacity = 1,
|
||||||
|
toOpacity = 1,
|
||||||
|
} = options;
|
||||||
|
from *= stretch;
|
||||||
|
to *= stretch;
|
||||||
|
rotate *= stretch;
|
||||||
|
ctx.lineWidth = thickness;
|
||||||
|
ctx.lineCap = cap;
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.globalAlpha = fromOpacity;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
let [sx, sy] = xyOnSpiral(from, margin, cx, cy, rotate);
|
||||||
|
ctx.moveTo(sx, sy);
|
||||||
|
|
||||||
|
const increment = 1 / 60;
|
||||||
|
let angle = from;
|
||||||
|
while (angle <= to) {
|
||||||
|
const [x, y] = xyOnSpiral(angle, margin, cx, cy, rotate);
|
||||||
|
//ctx.lineWidth = angle*thickness;
|
||||||
|
ctx.globalAlpha = ((angle - from) / (to - from)) * toOpacity;
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
angle += increment;
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern.prototype.spiral = function (options = {}) {
|
||||||
|
const {
|
||||||
|
stretch = 1,
|
||||||
|
size = 80,
|
||||||
|
thickness = size / 2,
|
||||||
|
cap = 'butt', // round butt squar,
|
||||||
|
inset = 3, // start angl,
|
||||||
|
playheadColor = '#ffffff90',
|
||||||
|
playheadLength = 0.02,
|
||||||
|
playheadThickness = thickness,
|
||||||
|
padding = 0,
|
||||||
|
steady = 1,
|
||||||
|
inactiveColor = '#ffffff20',
|
||||||
|
colorizeInactive = 0,
|
||||||
|
fade = true,
|
||||||
|
// logSpiral = true,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
function spiral({ ctx, time, haps, drawTime }) {
|
||||||
|
ctx.clearRect(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight);
|
||||||
|
const [cx, cy] = [ctx.canvas.width / 2, ctx.canvas.height / 2];
|
||||||
|
const settings = {
|
||||||
|
margin: size / stretch,
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
stretch,
|
||||||
|
cap,
|
||||||
|
thickness,
|
||||||
|
};
|
||||||
|
|
||||||
|
const playhead = {
|
||||||
|
...settings,
|
||||||
|
thickness: playheadThickness,
|
||||||
|
from: inset - playheadLength,
|
||||||
|
to: inset,
|
||||||
|
color: playheadColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [min] = drawTime;
|
||||||
|
const rotate = steady * time;
|
||||||
|
haps.forEach((hap) => {
|
||||||
|
const isActive = hap.whole.begin <= time && hap.endClipped > time;
|
||||||
|
const from = hap.whole.begin - time + inset;
|
||||||
|
const to = hap.endClipped - time + inset - padding;
|
||||||
|
const { color } = hap.context;
|
||||||
|
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
|
||||||
|
spiralSegment({
|
||||||
|
ctx,
|
||||||
|
...settings,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
rotate,
|
||||||
|
color: colorizeInactive || isActive ? color : inactiveColor,
|
||||||
|
fromOpacity: opacity,
|
||||||
|
toOpacity: opacity,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
spiralSegment({
|
||||||
|
ctx,
|
||||||
|
...playhead,
|
||||||
|
rotate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
controls.test.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/controls.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 controls from '../controls.mjs';
|
||||||
|
import { mini } from '../../mini/mini.mjs';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('controls', () => {
|
||||||
|
it('should support controls', () => {
|
||||||
|
expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
|
||||||
|
});
|
||||||
|
it('should support compound controls', () => {
|
||||||
|
expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
|
||||||
|
expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
|
||||||
|
{ s: 'bd', n: 3 },
|
||||||
|
{ s: 'sd', n: 4, gain: 1.4 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('should support ignore extra elements in compound controls', () => {
|
||||||
|
expect(controls.s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
|
||||||
|
{ s: 'bd', n: 3, gain: 0.4 },
|
||||||
|
{ s: 'sd', n: 4, gain: 0.5 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -959,4 +959,48 @@ describe('Pattern', () => {
|
|||||||
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
|
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
/*describe('composable functions', () => {
|
||||||
|
it('Can compose functions', () => {
|
||||||
|
sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
|
||||||
|
});
|
||||||
|
it('Can compose by method chaining operators with controls', () => {
|
||||||
|
sameFirst(s('bd').apply(set.n(3).fast(2)), s('bd').set.n(3).fast(2));
|
||||||
|
});
|
||||||
|
it('Can compose by method chaining operators and alignments with controls', () => {
|
||||||
|
sameFirst(s('bd').apply(set.in.n(3).fast(2)), s('bd').set.n(3).fast(2));
|
||||||
|
// sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('weave', () => {
|
||||||
|
it('Can distribute patterns along a pattern', () => {
|
||||||
|
sameFirst(n(0, 1).weave(2, s('bd', silence), s(silence, 'sd')), sequence(s('bd').n(0), s('sd').n(1)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
describe('slice', () => {
|
||||||
|
it('Can slice a sample', () => {
|
||||||
|
sameFirst(
|
||||||
|
s('break').slice(4, sequence(0, 1, 2, 3)),
|
||||||
|
sequence(
|
||||||
|
{ begin: 0, end: 0.25, s: 'break', _slices: 4 },
|
||||||
|
{ begin: 0.25, end: 0.5, s: 'break', _slices: 4 },
|
||||||
|
{ begin: 0.5, end: 0.75, s: 'break', _slices: 4 },
|
||||||
|
{ begin: 0.75, end: 1, s: 'break', _slices: 4 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('splice', () => {
|
||||||
|
it('Can splice a sample', () => {
|
||||||
|
sameFirst(
|
||||||
|
s('break').splice(4, sequence(0, 1, 2, 3)),
|
||||||
|
sequence(
|
||||||
|
{ begin: 0, end: 0.25, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||||
|
{ begin: 0.25, end: 0.5, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||||
|
{ begin: 0.5, end: 0.75, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||||
|
{ begin: 0.75, end: 1, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*test for issue 302 support alternative solmization types */
|
||||||
|
import { sol2note } from '../util.mjs';
|
||||||
|
import { test } from 'vitest';
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
|
test('solmization - letters', () => {
|
||||||
|
const result = sol2note(60, 'letters');
|
||||||
|
const expected = 'C4';
|
||||||
|
assert.equal(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('solmization - solfeggio', () => {
|
||||||
|
const result = sol2note(60, 'solfeggio');
|
||||||
|
const expected = 'Do4';
|
||||||
|
assert.equal(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('solmization - indian', () => {
|
||||||
|
const result = sol2note(60, 'indian');
|
||||||
|
const expected = 'Sa4';
|
||||||
|
assert.equal(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('solmization - german', () => {
|
||||||
|
const result = sol2note(60, 'german');
|
||||||
|
const expected = 'C4';
|
||||||
|
assert.equal(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('solmization - byzantine', () => {
|
||||||
|
const result = sol2note(60, 'byzantine');
|
||||||
|
const expected = 'Ni4';
|
||||||
|
assert.equal(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('solmization - japanese', () => {
|
||||||
|
const result = sol2note(60, 'japanese');
|
||||||
|
const expected = 'I4';
|
||||||
|
assert.equal(result, expected);
|
||||||
|
});
|
||||||
@@ -8,8 +8,8 @@ import { pure } from '../pattern.mjs';
|
|||||||
import {
|
import {
|
||||||
isNote,
|
isNote,
|
||||||
tokenizeNote,
|
tokenizeNote,
|
||||||
toMidi,
|
noteToMidi,
|
||||||
fromMidi,
|
midiToFreq,
|
||||||
freqToMidi,
|
freqToMidi,
|
||||||
_mod,
|
_mod,
|
||||||
compose,
|
compose,
|
||||||
@@ -75,27 +75,27 @@ describe('isNote', () => {
|
|||||||
expect(tokenizeNote(123)).toStrictEqual([]);
|
expect(tokenizeNote(123)).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('toMidi', () => {
|
describe('noteToMidi', () => {
|
||||||
it('should turn notes into midi', () => {
|
it('should turn notes into midi', () => {
|
||||||
expect(toMidi('A4')).toEqual(69);
|
expect(noteToMidi('A4')).toEqual(69);
|
||||||
expect(toMidi('C4')).toEqual(60);
|
expect(noteToMidi('C4')).toEqual(60);
|
||||||
expect(toMidi('Db4')).toEqual(61);
|
expect(noteToMidi('Db4')).toEqual(61);
|
||||||
expect(toMidi('C3')).toEqual(48);
|
expect(noteToMidi('C3')).toEqual(48);
|
||||||
expect(toMidi('Cb3')).toEqual(47);
|
expect(noteToMidi('Cb3')).toEqual(47);
|
||||||
expect(toMidi('Cbb3')).toEqual(46);
|
expect(noteToMidi('Cbb3')).toEqual(46);
|
||||||
expect(toMidi('C#3')).toEqual(49);
|
expect(noteToMidi('C#3')).toEqual(49);
|
||||||
expect(toMidi('C#3')).toEqual(49);
|
expect(noteToMidi('C#3')).toEqual(49);
|
||||||
expect(toMidi('C##3')).toEqual(50);
|
expect(noteToMidi('C##3')).toEqual(50);
|
||||||
});
|
});
|
||||||
it('should throw an error when given a non-note', () => {
|
it('should throw an error when given a non-note', () => {
|
||||||
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
|
expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||||
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
|
expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('fromMidi', () => {
|
describe('midiToFreq', () => {
|
||||||
it('should turn midi into frequency', () => {
|
it('should turn midi into frequency', () => {
|
||||||
expect(fromMidi(69)).toEqual(440);
|
expect(midiToFreq(69)).toEqual(440);
|
||||||
expect(fromMidi(57)).toEqual(220);
|
expect(midiToFreq(57)).toEqual(220);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('freqToMidi', () => {
|
describe('freqToMidi', () => {
|
||||||
|
|||||||
+81
-14
@@ -6,12 +6,12 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
// returns true if the given string is a note
|
// returns true if the given string is a note
|
||||||
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
||||||
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]?$/.test(name);
|
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
|
||||||
export const tokenizeNote = (note) => {
|
export const tokenizeNote = (note) => {
|
||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
|
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9])?$/)?.slice(1) || [];
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -19,16 +19,16 @@ export const tokenizeNote = (note) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// turns the given note into its midi number representation
|
// turns the given note into its midi number representation
|
||||||
export const toMidi = (note) => {
|
export const noteToMidi = (note) => {
|
||||||
const [pc, acc, oct = 3] = tokenizeNote(note);
|
const [pc, acc, oct = 3] = tokenizeNote(note);
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
throw new Error('not a note: "' + note + '"');
|
throw new Error('not a note: "' + note + '"');
|
||||||
}
|
}
|
||||||
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
|
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
|
||||||
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
|
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1, f: -1 }[char], 0) || 0;
|
||||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
export const fromMidi = (n) => {
|
export const midiToFreq = (n) => {
|
||||||
return Math.pow(2, (n - 69) / 12) * 440;
|
return Math.pow(2, (n - 69) / 12) * 440;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ export const valueToMidi = (value, fallbackValue) => {
|
|||||||
return freqToMidi(freq);
|
return freqToMidi(freq);
|
||||||
}
|
}
|
||||||
if (typeof note === 'string') {
|
if (typeof note === 'string') {
|
||||||
return toMidi(note);
|
return noteToMidi(note);
|
||||||
}
|
}
|
||||||
if (typeof note === 'number') {
|
if (typeof note === 'number') {
|
||||||
return note;
|
return note;
|
||||||
@@ -58,20 +58,23 @@ export const valueToMidi = (value, fallbackValue) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
export const getFreq = (noteOrMidi) => {
|
export const getFreq = (noteOrMidi) => {
|
||||||
if (typeof noteOrMidi === 'number') {
|
if (typeof noteOrMidi === 'number') {
|
||||||
return fromMidi(noteOrMidi);
|
return midiToFreq(noteOrMidi);
|
||||||
}
|
}
|
||||||
return fromMidi(toMidi(noteOrMidi));
|
return midiToFreq(noteToMidi(noteOrMidi));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
||||||
/**
|
/**
|
||||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
* @deprecated only used in workshop (first-notes)
|
||||||
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
export const midi2note = (n) => {
|
export const midi2note = (n) => {
|
||||||
const oct = Math.floor(n / 12) - 1;
|
const oct = Math.floor(n / 12) - 1;
|
||||||
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
|
const pc = pcs[n % 12];
|
||||||
return pc + oct;
|
return pc + oct;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,7 +92,7 @@ export const getPlayableNoteValue = (hap) => {
|
|||||||
}
|
}
|
||||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||||
note = fromMidi(hap.value);
|
note = midiToFreq(hap.value);
|
||||||
} else if (typeof note === 'number' && context.type === 'frequency') {
|
} else if (typeof note === 'number' && context.type === 'frequency') {
|
||||||
note = hap.value; // legacy workaround.. will be removed in the future
|
note = hap.value; // legacy workaround.. will be removed in the future
|
||||||
} else if (typeof note !== 'string' || !isNote(note)) {
|
} else if (typeof note !== 'string' || !isNote(note)) {
|
||||||
@@ -108,9 +111,9 @@ export const getFrequency = (hap) => {
|
|||||||
return getFreq(value.note || value.n || value.value);
|
return getFreq(value.note || value.n || value.value);
|
||||||
}
|
}
|
||||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||||
value = fromMidi(hap.value);
|
value = midiToFreq(hap.value);
|
||||||
} else if (typeof value === 'string' && isNote(value)) {
|
} else if (typeof value === 'string' && isNote(value)) {
|
||||||
value = fromMidi(toMidi(hap.value));
|
value = midiToFreq(noteToMidi(hap.value));
|
||||||
} else if (typeof value !== 'number') {
|
} else if (typeof value !== 'number') {
|
||||||
throw new Error('not a note or frequency: ' + value);
|
throw new Error('not a note or frequency: ' + value);
|
||||||
}
|
}
|
||||||
@@ -168,7 +171,7 @@ export function parseNumeral(numOrString) {
|
|||||||
return asNumber;
|
return asNumber;
|
||||||
}
|
}
|
||||||
if (isNote(numOrString)) {
|
if (isNote(numOrString)) {
|
||||||
return toMidi(numOrString);
|
return noteToMidi(numOrString);
|
||||||
}
|
}
|
||||||
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
||||||
}
|
}
|
||||||
@@ -204,3 +207,67 @@ export function parseFractional(numOrString) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
|
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
|
||||||
|
|
||||||
|
export const splitAt = function (index, value) {
|
||||||
|
return [value.slice(0, index), value.slice(index)];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||||
|
|
||||||
|
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||||
|
|
||||||
|
/* solmization, not used yet */
|
||||||
|
const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/
|
||||||
|
const indian = [
|
||||||
|
'Sa',
|
||||||
|
'Re',
|
||||||
|
'Ga',
|
||||||
|
'Ma',
|
||||||
|
'Pa',
|
||||||
|
'Dha',
|
||||||
|
'Ni',
|
||||||
|
]; /*indian musical notes, seems like they do not use flats or sharps*/
|
||||||
|
const german = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Hb', 'H']; /*german & dutch musical notes*/
|
||||||
|
const byzantine = [
|
||||||
|
'Ni',
|
||||||
|
'Pab',
|
||||||
|
'Pa',
|
||||||
|
'Voub',
|
||||||
|
'Vou',
|
||||||
|
'Ga',
|
||||||
|
'Dib',
|
||||||
|
'Di',
|
||||||
|
'Keb',
|
||||||
|
'Ke',
|
||||||
|
'Zob',
|
||||||
|
'Zo',
|
||||||
|
]; /*byzantine musical notes*/
|
||||||
|
const japanese = [
|
||||||
|
'I',
|
||||||
|
'Ro',
|
||||||
|
'Ha',
|
||||||
|
'Ni',
|
||||||
|
'Ho',
|
||||||
|
'He',
|
||||||
|
'To',
|
||||||
|
]; /*traditional japanese musical notes, seems like they do not use falts or sharps*/
|
||||||
|
|
||||||
|
const english = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
||||||
|
|
||||||
|
export const sol2note = (n, notation = 'letters') => {
|
||||||
|
const pc =
|
||||||
|
notation === 'solfeggio'
|
||||||
|
? solfeggio /*check if its is any of the following*/
|
||||||
|
: notation === 'indian'
|
||||||
|
? indian
|
||||||
|
: notation === 'german'
|
||||||
|
? german
|
||||||
|
: notation === 'byzantine'
|
||||||
|
? byzantine
|
||||||
|
: notation === 'japanese'
|
||||||
|
? japanese
|
||||||
|
: english; /*if not use standard version*/
|
||||||
|
const note = pc[n % 12]; /*calculating the midi value to the note*/
|
||||||
|
const oct = Math.floor(n / 12) - 1;
|
||||||
|
return note + oct;
|
||||||
|
};
|
||||||
|
|||||||
@@ -44,6 +44,6 @@ function createClock(
|
|||||||
};
|
};
|
||||||
const getPhase = () => phase;
|
const getPhase = () => phase;
|
||||||
// setCallback
|
// setCallback
|
||||||
return { setDuration, start, stop, pause, duration, getPhase, minLatency };
|
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
|
||||||
}
|
}
|
||||||
export default createClock;
|
export default createClock;
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
logger('[csound] not loaded yet', 'warning');
|
logger('[csound] not loaded yet', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof hap.value !== 'object') {
|
hap.ensureObjectValue();
|
||||||
throw new Error('csound only support objects as hap values');
|
|
||||||
}
|
|
||||||
let { gain = 0.8 } = hap.value;
|
let { gain = 0.8 } = hap.value;
|
||||||
gain *= 0.2;
|
gain *= 0.2;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/csound",
|
"name": "@strudel.cycles/csound",
|
||||||
"version": "0.6.2",
|
"version": "0.8.0",
|
||||||
"description": "csound bindings for strudel",
|
"description": "csound bindings for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -32,11 +32,11 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@csound/browser": "6.18.5",
|
"@csound/browser": "6.18.7",
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*"
|
"@strudel.cycles/webaudio": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^3.2.2"
|
"vite": "^4.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/eval",
|
"name": "@strudel.cycles/eval",
|
||||||
"version": "0.6.2",
|
"version": "0.8.0",
|
||||||
"description": "Code evaluator for strudel",
|
"description": "Code evaluator for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
"@strudel.cycles/mini": "workspace:*",
|
||||||
"vite": "^3.2.2",
|
"vite": "^4.3.3",
|
||||||
"vitest": "^0.25.7"
|
"vitest": "^0.28.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-13
@@ -5,8 +5,9 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _WebMidi from 'webmidi';
|
import * as _WebMidi from 'webmidi';
|
||||||
import { Pattern, isPattern, isNote, getPlayableNoteValue, logger } from '@strudel.cycles/core';
|
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
|
||||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||||
|
import { noteToMidi } from '@strudel.cycles/core';
|
||||||
|
|
||||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
export const { WebMidi } = _WebMidi;
|
export const { WebMidi } = _WebMidi;
|
||||||
@@ -63,7 +64,7 @@ function getDevice(output, outputs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||||
Pattern.prototype.midi = function (output, channel = 1) {
|
Pattern.prototype.midi = function (output) {
|
||||||
if (!supportsMidi()) {
|
if (!supportsMidi()) {
|
||||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||||
}
|
}
|
||||||
@@ -90,11 +91,6 @@ Pattern.prototype.midi = function (output, channel = 1) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.onTrigger((time, hap) => {
|
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 (!midiReady) {
|
if (!midiReady) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -106,15 +102,34 @@ Pattern.prototype.midi = function (output, channel = 1) {
|
|||||||
.join(' | ')}`,
|
.join(' | ')}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// console.log('midi', value, output);
|
hap.ensureObjectValue();
|
||||||
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
|
|
||||||
|
// calculate time
|
||||||
|
const timingOffset = WebMidi.time - getAudioContext().getOutputTimestamp().contextTime * 1000;
|
||||||
time = time * 1000 + timingOffset;
|
time = time * 1000 + timingOffset;
|
||||||
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
|
||||||
// await enableWebMidi()
|
// destructure value
|
||||||
device.playNote(note, channel, {
|
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
|
||||||
|
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
|
||||||
|
const duration = hap.duration.valueOf() * 1000 - 5;
|
||||||
|
|
||||||
|
if (note) {
|
||||||
|
const midiNumber = noteToMidi(note);
|
||||||
|
device.playNote(midiNumber, midichan, {
|
||||||
time,
|
time,
|
||||||
duration: hap.duration.valueOf() * 1000 - 5,
|
duration,
|
||||||
attack: velocity,
|
attack: velocity,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
if (ccv && ccn) {
|
||||||
|
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
||||||
|
throw new Error('expected ccv to be a number between 0 and 1');
|
||||||
|
}
|
||||||
|
if (!['string', 'number'].includes(typeof ccn)) {
|
||||||
|
throw new Error('expected ccn to be a number or a string');
|
||||||
|
}
|
||||||
|
const scaled = Math.round(ccv * 127);
|
||||||
|
device.sendControlChange(ccn, scaled, midichan, { time });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/midi",
|
"name": "@strudel.cycles/midi",
|
||||||
"version": "0.6.0",
|
"version": "0.8.0",
|
||||||
"description": "Midi API for strudel",
|
"description": "Midi API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -31,9 +31,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
"webmidi": "^3.0.21"
|
"webmidi": "^3.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^3.2.2"
|
"vite": "^4.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+169
-132
@@ -1,4 +1,4 @@
|
|||||||
// Generated by Peggy 2.0.1.
|
// Generated by Peggy 3.0.2.
|
||||||
//
|
//
|
||||||
// https://peggyjs.org/
|
// https://peggyjs.org/
|
||||||
|
|
||||||
@@ -43,16 +43,19 @@ peg$SyntaxError.prototype.format = function(sources) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var s = this.location.start;
|
var s = this.location.start;
|
||||||
var loc = this.location.source + ":" + s.line + ":" + s.column;
|
var offset_s = (this.location.source && (typeof this.location.source.offset === "function"))
|
||||||
|
? this.location.source.offset(s)
|
||||||
|
: s;
|
||||||
|
var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
|
||||||
if (src) {
|
if (src) {
|
||||||
var e = this.location.end;
|
var e = this.location.end;
|
||||||
var filler = peg$padEnd("", s.line.toString().length, ' ');
|
var filler = peg$padEnd("", offset_s.line.toString().length, ' ');
|
||||||
var line = src[s.line - 1];
|
var line = src[s.line - 1];
|
||||||
var last = s.line === e.line ? e.column : line.length + 1;
|
var last = s.line === e.line ? e.column : line.length + 1;
|
||||||
var hatLen = (last - s.column) || 1;
|
var hatLen = (last - s.column) || 1;
|
||||||
str += "\n --> " + loc + "\n"
|
str += "\n --> " + loc + "\n"
|
||||||
+ filler + " |\n"
|
+ filler + " |\n"
|
||||||
+ s.line + " | " + line + "\n"
|
+ offset_s.line + " | " + line + "\n"
|
||||||
+ filler + " | " + peg$padEnd("", s.column - 1, ' ')
|
+ filler + " | " + peg$padEnd("", s.column - 1, ' ')
|
||||||
+ peg$padEnd("", hatLen, "^");
|
+ peg$padEnd("", hatLen, "^");
|
||||||
} else {
|
} else {
|
||||||
@@ -182,21 +185,21 @@ function peg$parse(input, options) {
|
|||||||
var peg$c8 = "#";
|
var peg$c8 = "#";
|
||||||
var peg$c9 = "^";
|
var peg$c9 = "^";
|
||||||
var peg$c10 = "_";
|
var peg$c10 = "_";
|
||||||
var peg$c11 = ":";
|
var peg$c11 = "[";
|
||||||
var peg$c12 = "[";
|
var peg$c12 = "]";
|
||||||
var peg$c13 = "]";
|
var peg$c13 = "{";
|
||||||
var peg$c14 = "{";
|
var peg$c14 = "}";
|
||||||
var peg$c15 = "}";
|
var peg$c15 = "%";
|
||||||
var peg$c16 = "%";
|
var peg$c16 = "<";
|
||||||
var peg$c17 = "<";
|
var peg$c17 = ">";
|
||||||
var peg$c18 = ">";
|
var peg$c18 = "@";
|
||||||
var peg$c19 = "@";
|
var peg$c19 = "!";
|
||||||
var peg$c20 = "!";
|
var peg$c20 = "(";
|
||||||
var peg$c21 = "(";
|
var peg$c21 = ")";
|
||||||
var peg$c22 = ")";
|
var peg$c22 = "/";
|
||||||
var peg$c23 = "/";
|
var peg$c23 = "*";
|
||||||
var peg$c24 = "*";
|
var peg$c24 = "?";
|
||||||
var peg$c25 = "?";
|
var peg$c25 = ":";
|
||||||
var peg$c26 = "struct";
|
var peg$c26 = "struct";
|
||||||
var peg$c27 = "target";
|
var peg$c27 = "target";
|
||||||
var peg$c28 = "euclid";
|
var peg$c28 = "euclid";
|
||||||
@@ -237,21 +240,21 @@ function peg$parse(input, options) {
|
|||||||
var peg$e15 = peg$literalExpectation("#", false);
|
var peg$e15 = peg$literalExpectation("#", false);
|
||||||
var peg$e16 = peg$literalExpectation("^", false);
|
var peg$e16 = peg$literalExpectation("^", false);
|
||||||
var peg$e17 = peg$literalExpectation("_", false);
|
var peg$e17 = peg$literalExpectation("_", false);
|
||||||
var peg$e18 = peg$literalExpectation(":", false);
|
var peg$e18 = peg$literalExpectation("[", false);
|
||||||
var peg$e19 = peg$literalExpectation("[", false);
|
var peg$e19 = peg$literalExpectation("]", false);
|
||||||
var peg$e20 = peg$literalExpectation("]", false);
|
var peg$e20 = peg$literalExpectation("{", false);
|
||||||
var peg$e21 = peg$literalExpectation("{", false);
|
var peg$e21 = peg$literalExpectation("}", false);
|
||||||
var peg$e22 = peg$literalExpectation("}", false);
|
var peg$e22 = peg$literalExpectation("%", false);
|
||||||
var peg$e23 = peg$literalExpectation("%", false);
|
var peg$e23 = peg$literalExpectation("<", false);
|
||||||
var peg$e24 = peg$literalExpectation("<", false);
|
var peg$e24 = peg$literalExpectation(">", false);
|
||||||
var peg$e25 = peg$literalExpectation(">", false);
|
var peg$e25 = peg$literalExpectation("@", false);
|
||||||
var peg$e26 = peg$literalExpectation("@", false);
|
var peg$e26 = peg$literalExpectation("!", false);
|
||||||
var peg$e27 = peg$literalExpectation("!", false);
|
var peg$e27 = peg$literalExpectation("(", false);
|
||||||
var peg$e28 = peg$literalExpectation("(", false);
|
var peg$e28 = peg$literalExpectation(")", false);
|
||||||
var peg$e29 = peg$literalExpectation(")", false);
|
var peg$e29 = peg$literalExpectation("/", false);
|
||||||
var peg$e30 = peg$literalExpectation("/", false);
|
var peg$e30 = peg$literalExpectation("*", false);
|
||||||
var peg$e31 = peg$literalExpectation("*", false);
|
var peg$e31 = peg$literalExpectation("?", false);
|
||||||
var peg$e32 = peg$literalExpectation("?", false);
|
var peg$e32 = peg$literalExpectation(":", false);
|
||||||
var peg$e33 = peg$literalExpectation("struct", false);
|
var peg$e33 = peg$literalExpectation("struct", false);
|
||||||
var peg$e34 = peg$literalExpectation("target", false);
|
var peg$e34 = peg$literalExpectation("target", false);
|
||||||
var peg$e35 = peg$literalExpectation("euclid", false);
|
var peg$e35 = peg$literalExpectation("euclid", false);
|
||||||
@@ -279,36 +282,37 @@ function peg$parse(input, options) {
|
|||||||
var peg$f8 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
|
var peg$f8 = function(p, s, r) { return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) };
|
||||||
var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
|
var peg$f9 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) };
|
||||||
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
|
var peg$f10 = function(a) { return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) };
|
||||||
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) };
|
var peg$f11 = function(a) { return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) };
|
||||||
var peg$f12 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
var peg$f12 = function(s) { return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) };
|
||||||
|
var peg$f13 = function(s, ops) { const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||||
for (const op of ops) {
|
for (const op of ops) {
|
||||||
op(result);
|
op(result);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
var peg$f13 = function(s) { return new PatternStub(s, 'fastcat'); };
|
var peg$f14 = function(s) { return new PatternStub(s, 'fastcat'); };
|
||||||
var peg$f14 = function(tail) { return { alignment: 'stack', list: tail }; };
|
var peg$f15 = function(tail) { return { alignment: 'stack', list: tail }; };
|
||||||
var peg$f15 = function(tail) { return { alignment: 'rand', list: tail }; };
|
var peg$f16 = function(tail) { return { alignment: 'rand', list: tail, seed: seed++ }; };
|
||||||
var peg$f16 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } };
|
var peg$f17 = function(head, tail) { if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } };
|
||||||
var peg$f17 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
var peg$f18 = function(head, tail) { return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); };
|
||||||
var peg$f18 = function(sc) { return sc; };
|
var peg$f19 = function(sc) { return sc; };
|
||||||
var peg$f19 = function(s) { return { name: "struct", args: { mini:s }}};
|
var peg$f20 = function(s) { return { name: "struct", args: { mini:s }}};
|
||||||
var peg$f20 = function(s) { return { name: "target", args : { name:s}}};
|
var peg$f21 = function(s) { return { name: "target", args : { name:s}}};
|
||||||
var peg$f21 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
|
var peg$f22 = function(p, s, r) { return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}};
|
||||||
var peg$f22 = function(a) { return { name: "stretch", args :{ amount: a}}};
|
var peg$f23 = function(a) { return { name: "stretch", args :{ amount: a}}};
|
||||||
var peg$f23 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
|
var peg$f24 = function(a) { return { name: "shift", args :{ amount: "-"+a}}};
|
||||||
var peg$f24 = function(a) { return { name: "shift", args :{ amount: a}}};
|
var peg$f25 = function(a) { return { name: "shift", args :{ amount: a}}};
|
||||||
var peg$f25 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
|
var peg$f26 = function(a) { return { name: "stretch", args :{ amount: "1/"+a}}};
|
||||||
var peg$f26 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
|
var peg$f27 = function(s) { return { name: "scale", args :{ scale: s.join("")}}};
|
||||||
var peg$f27 = function(s, v) { return v};
|
var peg$f28 = function(s, v) { return v};
|
||||||
var peg$f28 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
|
var peg$f29 = function(s, ss) { ss.unshift(s); return new PatternStub(ss, 'slowcat'); };
|
||||||
var peg$f29 = function(sg) {return sg};
|
var peg$f30 = function(sg) {return sg};
|
||||||
var peg$f30 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
|
var peg$f31 = function(o, soc) { return new OperatorStub(o.name,o.args,soc)};
|
||||||
var peg$f31 = function(sc) { return sc };
|
var peg$f32 = function(sc) { return sc };
|
||||||
var peg$f32 = function(c) { return c };
|
var peg$f33 = function(c) { return c };
|
||||||
var peg$f33 = function(v) { return new CommandStub("setcps", { value: v})};
|
var peg$f34 = function(v) { return new CommandStub("setcps", { value: v})};
|
||||||
var peg$f34 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
|
var peg$f35 = function(v) { return new CommandStub("setcps", { value: (v/120/2)})};
|
||||||
var peg$f35 = function() { return new CommandStub("hush")};
|
var peg$f36 = function() { return new CommandStub("hush")};
|
||||||
var peg$currPos = 0;
|
var peg$currPos = 0;
|
||||||
var peg$savedPos = 0;
|
var peg$savedPos = 0;
|
||||||
var peg$posDetailsCache = [{ line: 1, column: 1 }];
|
var peg$posDetailsCache = [{ line: 1, column: 1 }];
|
||||||
@@ -421,11 +425,11 @@ function peg$parse(input, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function peg$computeLocation(startPos, endPos) {
|
function peg$computeLocation(startPos, endPos, offset) {
|
||||||
var startPosDetails = peg$computePosDetails(startPos);
|
var startPosDetails = peg$computePosDetails(startPos);
|
||||||
var endPosDetails = peg$computePosDetails(endPos);
|
var endPosDetails = peg$computePosDetails(endPos);
|
||||||
|
|
||||||
return {
|
var res = {
|
||||||
source: peg$source,
|
source: peg$source,
|
||||||
start: {
|
start: {
|
||||||
offset: startPos,
|
offset: startPos,
|
||||||
@@ -438,6 +442,11 @@ function peg$parse(input, options) {
|
|||||||
column: endPosDetails.column
|
column: endPosDetails.column
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if (offset && peg$source && (typeof peg$source.offset === "function")) {
|
||||||
|
res.start = peg$source.offset(res.start);
|
||||||
|
res.end = peg$source.offset(res.end);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
function peg$fail(expected) {
|
function peg$fail(expected) {
|
||||||
@@ -848,15 +857,6 @@ function peg$parse(input, options) {
|
|||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e17); }
|
if (peg$silentFails === 0) { peg$fail(peg$e17); }
|
||||||
}
|
}
|
||||||
if (s0 === peg$FAILED) {
|
|
||||||
if (input.charCodeAt(peg$currPos) === 58) {
|
|
||||||
s0 = peg$c11;
|
|
||||||
peg$currPos++;
|
|
||||||
} else {
|
|
||||||
s0 = peg$FAILED;
|
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -899,11 +899,11 @@ function peg$parse(input, options) {
|
|||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
s1 = peg$parsews();
|
s1 = peg$parsews();
|
||||||
if (input.charCodeAt(peg$currPos) === 91) {
|
if (input.charCodeAt(peg$currPos) === 91) {
|
||||||
s2 = peg$c12;
|
s2 = peg$c11;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s2 = peg$FAILED;
|
s2 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||||
}
|
}
|
||||||
if (s2 !== peg$FAILED) {
|
if (s2 !== peg$FAILED) {
|
||||||
s3 = peg$parsews();
|
s3 = peg$parsews();
|
||||||
@@ -911,11 +911,11 @@ function peg$parse(input, options) {
|
|||||||
if (s4 !== peg$FAILED) {
|
if (s4 !== peg$FAILED) {
|
||||||
s5 = peg$parsews();
|
s5 = peg$parsews();
|
||||||
if (input.charCodeAt(peg$currPos) === 93) {
|
if (input.charCodeAt(peg$currPos) === 93) {
|
||||||
s6 = peg$c13;
|
s6 = peg$c12;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s6 = peg$FAILED;
|
s6 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||||
}
|
}
|
||||||
if (s6 !== peg$FAILED) {
|
if (s6 !== peg$FAILED) {
|
||||||
s7 = peg$parsews();
|
s7 = peg$parsews();
|
||||||
@@ -943,11 +943,11 @@ function peg$parse(input, options) {
|
|||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
s1 = peg$parsews();
|
s1 = peg$parsews();
|
||||||
if (input.charCodeAt(peg$currPos) === 123) {
|
if (input.charCodeAt(peg$currPos) === 123) {
|
||||||
s2 = peg$c14;
|
s2 = peg$c13;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s2 = peg$FAILED;
|
s2 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e21); }
|
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
||||||
}
|
}
|
||||||
if (s2 !== peg$FAILED) {
|
if (s2 !== peg$FAILED) {
|
||||||
s3 = peg$parsews();
|
s3 = peg$parsews();
|
||||||
@@ -955,11 +955,11 @@ function peg$parse(input, options) {
|
|||||||
if (s4 !== peg$FAILED) {
|
if (s4 !== peg$FAILED) {
|
||||||
s5 = peg$parsews();
|
s5 = peg$parsews();
|
||||||
if (input.charCodeAt(peg$currPos) === 125) {
|
if (input.charCodeAt(peg$currPos) === 125) {
|
||||||
s6 = peg$c15;
|
s6 = peg$c14;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s6 = peg$FAILED;
|
s6 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e22); }
|
if (peg$silentFails === 0) { peg$fail(peg$e21); }
|
||||||
}
|
}
|
||||||
if (s6 !== peg$FAILED) {
|
if (s6 !== peg$FAILED) {
|
||||||
s7 = peg$parsepolymeter_steps();
|
s7 = peg$parsepolymeter_steps();
|
||||||
@@ -990,11 +990,11 @@ function peg$parse(input, options) {
|
|||||||
|
|
||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
if (input.charCodeAt(peg$currPos) === 37) {
|
if (input.charCodeAt(peg$currPos) === 37) {
|
||||||
s1 = peg$c16;
|
s1 = peg$c15;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s1 = peg$FAILED;
|
s1 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
if (peg$silentFails === 0) { peg$fail(peg$e22); }
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
s2 = peg$parseslice();
|
s2 = peg$parseslice();
|
||||||
@@ -1019,11 +1019,11 @@ function peg$parse(input, options) {
|
|||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
s1 = peg$parsews();
|
s1 = peg$parsews();
|
||||||
if (input.charCodeAt(peg$currPos) === 60) {
|
if (input.charCodeAt(peg$currPos) === 60) {
|
||||||
s2 = peg$c17;
|
s2 = peg$c16;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s2 = peg$FAILED;
|
s2 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
||||||
}
|
}
|
||||||
if (s2 !== peg$FAILED) {
|
if (s2 !== peg$FAILED) {
|
||||||
s3 = peg$parsews();
|
s3 = peg$parsews();
|
||||||
@@ -1031,11 +1031,11 @@ function peg$parse(input, options) {
|
|||||||
if (s4 !== peg$FAILED) {
|
if (s4 !== peg$FAILED) {
|
||||||
s5 = peg$parsews();
|
s5 = peg$parsews();
|
||||||
if (input.charCodeAt(peg$currPos) === 62) {
|
if (input.charCodeAt(peg$currPos) === 62) {
|
||||||
s6 = peg$c18;
|
s6 = peg$c17;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s6 = peg$FAILED;
|
s6 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e25); }
|
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
||||||
}
|
}
|
||||||
if (s6 !== peg$FAILED) {
|
if (s6 !== peg$FAILED) {
|
||||||
s7 = peg$parsews();
|
s7 = peg$parsews();
|
||||||
@@ -1088,6 +1088,9 @@ function peg$parse(input, options) {
|
|||||||
s0 = peg$parseop_replicate();
|
s0 = peg$parseop_replicate();
|
||||||
if (s0 === peg$FAILED) {
|
if (s0 === peg$FAILED) {
|
||||||
s0 = peg$parseop_degrade();
|
s0 = peg$parseop_degrade();
|
||||||
|
if (s0 === peg$FAILED) {
|
||||||
|
s0 = peg$parseop_tail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1102,11 +1105,11 @@ function peg$parse(input, options) {
|
|||||||
|
|
||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
if (input.charCodeAt(peg$currPos) === 64) {
|
if (input.charCodeAt(peg$currPos) === 64) {
|
||||||
s1 = peg$c19;
|
s1 = peg$c18;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s1 = peg$FAILED;
|
s1 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
if (peg$silentFails === 0) { peg$fail(peg$e25); }
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
s2 = peg$parsenumber();
|
s2 = peg$parsenumber();
|
||||||
@@ -1130,11 +1133,11 @@ function peg$parse(input, options) {
|
|||||||
|
|
||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
if (input.charCodeAt(peg$currPos) === 33) {
|
if (input.charCodeAt(peg$currPos) === 33) {
|
||||||
s1 = peg$c20;
|
s1 = peg$c19;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s1 = peg$FAILED;
|
s1 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e27); }
|
if (peg$silentFails === 0) { peg$fail(peg$e26); }
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
s2 = peg$parsenumber();
|
s2 = peg$parsenumber();
|
||||||
@@ -1158,11 +1161,11 @@ function peg$parse(input, options) {
|
|||||||
|
|
||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
if (input.charCodeAt(peg$currPos) === 40) {
|
if (input.charCodeAt(peg$currPos) === 40) {
|
||||||
s1 = peg$c21;
|
s1 = peg$c20;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s1 = peg$FAILED;
|
s1 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
if (peg$silentFails === 0) { peg$fail(peg$e27); }
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
s2 = peg$parsews();
|
s2 = peg$parsews();
|
||||||
@@ -1186,11 +1189,11 @@ function peg$parse(input, options) {
|
|||||||
}
|
}
|
||||||
s12 = peg$parsews();
|
s12 = peg$parsews();
|
||||||
if (input.charCodeAt(peg$currPos) === 41) {
|
if (input.charCodeAt(peg$currPos) === 41) {
|
||||||
s13 = peg$c22;
|
s13 = peg$c21;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s13 = peg$FAILED;
|
s13 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||||
}
|
}
|
||||||
if (s13 !== peg$FAILED) {
|
if (s13 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
@@ -1224,11 +1227,11 @@ function peg$parse(input, options) {
|
|||||||
|
|
||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
if (input.charCodeAt(peg$currPos) === 47) {
|
if (input.charCodeAt(peg$currPos) === 47) {
|
||||||
s1 = peg$c23;
|
s1 = peg$c22;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s1 = peg$FAILED;
|
s1 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e30); }
|
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
s2 = peg$parseslice();
|
s2 = peg$parseslice();
|
||||||
@@ -1252,11 +1255,11 @@ function peg$parse(input, options) {
|
|||||||
|
|
||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
if (input.charCodeAt(peg$currPos) === 42) {
|
if (input.charCodeAt(peg$currPos) === 42) {
|
||||||
s1 = peg$c24;
|
s1 = peg$c23;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s1 = peg$FAILED;
|
s1 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e31); }
|
if (peg$silentFails === 0) { peg$fail(peg$e30); }
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
s2 = peg$parseslice();
|
s2 = peg$parseslice();
|
||||||
@@ -1280,11 +1283,11 @@ function peg$parse(input, options) {
|
|||||||
|
|
||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
if (input.charCodeAt(peg$currPos) === 63) {
|
if (input.charCodeAt(peg$currPos) === 63) {
|
||||||
s1 = peg$c25;
|
s1 = peg$c24;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s1 = peg$FAILED;
|
s1 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e32); }
|
if (peg$silentFails === 0) { peg$fail(peg$e31); }
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
s2 = peg$parsenumber();
|
s2 = peg$parsenumber();
|
||||||
@@ -1301,6 +1304,34 @@ function peg$parse(input, options) {
|
|||||||
return s0;
|
return s0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function peg$parseop_tail() {
|
||||||
|
var s0, s1, s2;
|
||||||
|
|
||||||
|
s0 = peg$currPos;
|
||||||
|
if (input.charCodeAt(peg$currPos) === 58) {
|
||||||
|
s1 = peg$c25;
|
||||||
|
peg$currPos++;
|
||||||
|
} else {
|
||||||
|
s1 = peg$FAILED;
|
||||||
|
if (peg$silentFails === 0) { peg$fail(peg$e32); }
|
||||||
|
}
|
||||||
|
if (s1 !== peg$FAILED) {
|
||||||
|
s2 = peg$parseslice();
|
||||||
|
if (s2 !== peg$FAILED) {
|
||||||
|
peg$savedPos = s0;
|
||||||
|
s0 = peg$f12(s2);
|
||||||
|
} else {
|
||||||
|
peg$currPos = s0;
|
||||||
|
s0 = peg$FAILED;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
peg$currPos = s0;
|
||||||
|
s0 = peg$FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s0;
|
||||||
|
}
|
||||||
|
|
||||||
function peg$parseslice_with_ops() {
|
function peg$parseslice_with_ops() {
|
||||||
var s0, s1, s2, s3;
|
var s0, s1, s2, s3;
|
||||||
|
|
||||||
@@ -1314,7 +1345,7 @@ function peg$parse(input, options) {
|
|||||||
s3 = peg$parseslice_op();
|
s3 = peg$parseslice_op();
|
||||||
}
|
}
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f12(s1, s2);
|
s0 = peg$f13(s1, s2);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -1339,7 +1370,7 @@ function peg$parse(input, options) {
|
|||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s1 = peg$f13(s1);
|
s1 = peg$f14(s1);
|
||||||
}
|
}
|
||||||
s0 = s1;
|
s0 = s1;
|
||||||
|
|
||||||
@@ -1388,7 +1419,7 @@ function peg$parse(input, options) {
|
|||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s1 = peg$f14(s1);
|
s1 = peg$f15(s1);
|
||||||
}
|
}
|
||||||
s0 = s1;
|
s0 = s1;
|
||||||
|
|
||||||
@@ -1437,7 +1468,7 @@ function peg$parse(input, options) {
|
|||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s1 = peg$f15(s1);
|
s1 = peg$f16(s1);
|
||||||
}
|
}
|
||||||
s0 = s1;
|
s0 = s1;
|
||||||
|
|
||||||
@@ -1458,7 +1489,7 @@ function peg$parse(input, options) {
|
|||||||
s2 = null;
|
s2 = null;
|
||||||
}
|
}
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f16(s1, s2);
|
s0 = peg$f17(s1, s2);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -1478,7 +1509,7 @@ function peg$parse(input, options) {
|
|||||||
s2 = null;
|
s2 = null;
|
||||||
}
|
}
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f17(s1, s2);
|
s0 = peg$f18(s1, s2);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -1488,18 +1519,20 @@ function peg$parse(input, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function peg$parsemini() {
|
function peg$parsemini() {
|
||||||
var s0, s1, s2, s3, s4;
|
var s0, s1, s2, s3, s4, s5, s6;
|
||||||
|
|
||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
s1 = peg$parsews();
|
s1 = peg$parsews();
|
||||||
s2 = peg$parsequote();
|
s2 = peg$parsequote();
|
||||||
if (s2 !== peg$FAILED) {
|
if (s2 !== peg$FAILED) {
|
||||||
s3 = peg$parsestack_or_choose();
|
s3 = peg$parsews();
|
||||||
if (s3 !== peg$FAILED) {
|
s4 = peg$parsestack_or_choose();
|
||||||
s4 = peg$parsequote();
|
|
||||||
if (s4 !== peg$FAILED) {
|
if (s4 !== peg$FAILED) {
|
||||||
|
s5 = peg$parsews();
|
||||||
|
s6 = peg$parsequote();
|
||||||
|
if (s6 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f18(s3);
|
s0 = peg$f19(s4);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -1561,7 +1594,7 @@ function peg$parse(input, options) {
|
|||||||
s3 = peg$parsemini_or_operator();
|
s3 = peg$parsemini_or_operator();
|
||||||
if (s3 !== peg$FAILED) {
|
if (s3 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f19(s3);
|
s0 = peg$f20(s3);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -1594,7 +1627,7 @@ function peg$parse(input, options) {
|
|||||||
s5 = peg$parsequote();
|
s5 = peg$parsequote();
|
||||||
if (s5 !== peg$FAILED) {
|
if (s5 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f20(s4);
|
s0 = peg$f21(s4);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -1639,7 +1672,7 @@ function peg$parse(input, options) {
|
|||||||
s7 = null;
|
s7 = null;
|
||||||
}
|
}
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f21(s3, s5, s7);
|
s0 = peg$f22(s3, s5, s7);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -1672,7 +1705,7 @@ function peg$parse(input, options) {
|
|||||||
s3 = peg$parsenumber();
|
s3 = peg$parsenumber();
|
||||||
if (s3 !== peg$FAILED) {
|
if (s3 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f22(s3);
|
s0 = peg$f23(s3);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -1701,7 +1734,7 @@ function peg$parse(input, options) {
|
|||||||
s3 = peg$parsenumber();
|
s3 = peg$parsenumber();
|
||||||
if (s3 !== peg$FAILED) {
|
if (s3 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f23(s3);
|
s0 = peg$f24(s3);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -1730,7 +1763,7 @@ function peg$parse(input, options) {
|
|||||||
s3 = peg$parsenumber();
|
s3 = peg$parsenumber();
|
||||||
if (s3 !== peg$FAILED) {
|
if (s3 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f24(s3);
|
s0 = peg$f25(s3);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -1759,7 +1792,7 @@ function peg$parse(input, options) {
|
|||||||
s3 = peg$parsenumber();
|
s3 = peg$parsenumber();
|
||||||
if (s3 !== peg$FAILED) {
|
if (s3 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f25(s3);
|
s0 = peg$f26(s3);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -1801,7 +1834,7 @@ function peg$parse(input, options) {
|
|||||||
s5 = peg$parsequote();
|
s5 = peg$parsequote();
|
||||||
if (s5 !== peg$FAILED) {
|
if (s5 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f26(s4);
|
s0 = peg$f27(s4);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -1876,11 +1909,11 @@ function peg$parse(input, options) {
|
|||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
s2 = peg$parsews();
|
s2 = peg$parsews();
|
||||||
if (input.charCodeAt(peg$currPos) === 91) {
|
if (input.charCodeAt(peg$currPos) === 91) {
|
||||||
s3 = peg$c12;
|
s3 = peg$c11;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s3 = peg$FAILED;
|
s3 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||||
}
|
}
|
||||||
if (s3 !== peg$FAILED) {
|
if (s3 !== peg$FAILED) {
|
||||||
s4 = peg$parsews();
|
s4 = peg$parsews();
|
||||||
@@ -1893,7 +1926,7 @@ function peg$parse(input, options) {
|
|||||||
s9 = peg$parsemini_or_operator();
|
s9 = peg$parsemini_or_operator();
|
||||||
if (s9 !== peg$FAILED) {
|
if (s9 !== peg$FAILED) {
|
||||||
peg$savedPos = s7;
|
peg$savedPos = s7;
|
||||||
s7 = peg$f27(s5, s9);
|
s7 = peg$f28(s5, s9);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s7;
|
peg$currPos = s7;
|
||||||
s7 = peg$FAILED;
|
s7 = peg$FAILED;
|
||||||
@@ -1910,7 +1943,7 @@ function peg$parse(input, options) {
|
|||||||
s9 = peg$parsemini_or_operator();
|
s9 = peg$parsemini_or_operator();
|
||||||
if (s9 !== peg$FAILED) {
|
if (s9 !== peg$FAILED) {
|
||||||
peg$savedPos = s7;
|
peg$savedPos = s7;
|
||||||
s7 = peg$f27(s5, s9);
|
s7 = peg$f28(s5, s9);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s7;
|
peg$currPos = s7;
|
||||||
s7 = peg$FAILED;
|
s7 = peg$FAILED;
|
||||||
@@ -1922,15 +1955,15 @@ function peg$parse(input, options) {
|
|||||||
}
|
}
|
||||||
s7 = peg$parsews();
|
s7 = peg$parsews();
|
||||||
if (input.charCodeAt(peg$currPos) === 93) {
|
if (input.charCodeAt(peg$currPos) === 93) {
|
||||||
s8 = peg$c13;
|
s8 = peg$c12;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s8 = peg$FAILED;
|
s8 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||||
}
|
}
|
||||||
if (s8 !== peg$FAILED) {
|
if (s8 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f28(s5, s6);
|
s0 = peg$f29(s5, s6);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -1976,7 +2009,7 @@ function peg$parse(input, options) {
|
|||||||
s4 = peg$parsecomment();
|
s4 = peg$parsecomment();
|
||||||
}
|
}
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f29(s1);
|
s0 = peg$f30(s1);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -1998,7 +2031,7 @@ function peg$parse(input, options) {
|
|||||||
s5 = peg$parsemini_or_operator();
|
s5 = peg$parsemini_or_operator();
|
||||||
if (s5 !== peg$FAILED) {
|
if (s5 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f30(s1, s5);
|
s0 = peg$f31(s1, s5);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -2023,7 +2056,7 @@ function peg$parse(input, options) {
|
|||||||
s1 = peg$parsemini_or_operator();
|
s1 = peg$parsemini_or_operator();
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s1 = peg$f31(s1);
|
s1 = peg$f32(s1);
|
||||||
}
|
}
|
||||||
s0 = s1;
|
s0 = s1;
|
||||||
if (s0 === peg$FAILED) {
|
if (s0 === peg$FAILED) {
|
||||||
@@ -2056,7 +2089,7 @@ function peg$parse(input, options) {
|
|||||||
if (s2 !== peg$FAILED) {
|
if (s2 !== peg$FAILED) {
|
||||||
s3 = peg$parsews();
|
s3 = peg$parsews();
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f32(s2);
|
s0 = peg$f33(s2);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -2081,7 +2114,7 @@ function peg$parse(input, options) {
|
|||||||
s3 = peg$parsenumber();
|
s3 = peg$parsenumber();
|
||||||
if (s3 !== peg$FAILED) {
|
if (s3 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f33(s3);
|
s0 = peg$f34(s3);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -2110,7 +2143,7 @@ function peg$parse(input, options) {
|
|||||||
s3 = peg$parsenumber();
|
s3 = peg$parsenumber();
|
||||||
if (s3 !== peg$FAILED) {
|
if (s3 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s0 = peg$f34(s3);
|
s0 = peg$f35(s3);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
@@ -2136,7 +2169,7 @@ function peg$parse(input, options) {
|
|||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
s1 = peg$f35();
|
s1 = peg$f36();
|
||||||
}
|
}
|
||||||
s0 = s1;
|
s0 = s1;
|
||||||
|
|
||||||
@@ -2162,10 +2195,13 @@ function peg$parse(input, options) {
|
|||||||
this.location_ = location();
|
this.location_ = location();
|
||||||
}
|
}
|
||||||
|
|
||||||
var PatternStub = function(source, alignment)
|
var PatternStub = function(source, alignment, seed)
|
||||||
{
|
{
|
||||||
this.type_ = "pattern";
|
this.type_ = "pattern";
|
||||||
this.arguments_ = { alignment: alignment };
|
this.arguments_ = { alignment: alignment };
|
||||||
|
if (seed !== undefined) {
|
||||||
|
this.arguments_.seed = seed;
|
||||||
|
}
|
||||||
this.source_ = source;
|
this.source_ = source;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2191,6 +2227,7 @@ function peg$parse(input, options) {
|
|||||||
this.options_ = options;
|
this.options_ = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var seed = 0;
|
||||||
|
|
||||||
peg$result = peg$startRuleFunction();
|
peg$result = peg$startRuleFunction();
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
this.location_ = location();
|
this.location_ = location();
|
||||||
}
|
}
|
||||||
|
|
||||||
var PatternStub = function(source, alignment)
|
var PatternStub = function(source, alignment, seed)
|
||||||
{
|
{
|
||||||
this.type_ = "pattern";
|
this.type_ = "pattern";
|
||||||
this.arguments_ = { alignment: alignment };
|
this.arguments_ = { alignment: alignment };
|
||||||
|
if (seed !== undefined) {
|
||||||
|
this.arguments_.seed = seed;
|
||||||
|
}
|
||||||
this.source_ = source;
|
this.source_ = source;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +51,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
this.options_ = options;
|
this.options_ = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var seed = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
start = statement
|
start = statement
|
||||||
@@ -96,7 +100,7 @@ quote = '"' / "'"
|
|||||||
// ------------------ steps and cycles ---------------------------
|
// ------------------ steps and cycles ---------------------------
|
||||||
|
|
||||||
// single step definition (e.g bd)
|
// single step definition (e.g bd)
|
||||||
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
|
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_"
|
||||||
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
|
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
|
||||||
|
|
||||||
// define a sub cycle e.g. [1 2, 3 [4]]
|
// define a sub cycle e.g. [1 2, 3 [4]]
|
||||||
@@ -119,7 +123,7 @@ slice = step / sub_cycle / polymeter / slow_sequence
|
|||||||
|
|
||||||
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
||||||
// at this point, we assume we can represent them as regular sequence operators
|
// at this point, we assume we can represent them as regular sequence operators
|
||||||
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade
|
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail
|
||||||
|
|
||||||
op_weight = "@" a:number
|
op_weight = "@" a:number
|
||||||
{ return x => x.options_['weight'] = a }
|
{ return x => x.options_['weight'] = a }
|
||||||
@@ -137,7 +141,10 @@ op_fast = "*"a:slice
|
|||||||
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
|
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
|
||||||
|
|
||||||
op_degrade = "?"a:number?
|
op_degrade = "?"a:number?
|
||||||
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
|
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) }
|
||||||
|
|
||||||
|
op_tail = ":" s:slice
|
||||||
|
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
|
||||||
|
|
||||||
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
||||||
slice_with_ops = s:slice ops:slice_op*
|
slice_with_ops = s:slice ops:slice_op*
|
||||||
@@ -159,12 +166,12 @@ stack_tail = tail:(comma @sequence)+
|
|||||||
// a choose is a series of pipe-separated sequence, one of which is
|
// a choose is a series of pipe-separated sequence, one of which is
|
||||||
// chosen at random, each cycle
|
// chosen at random, each cycle
|
||||||
choose_tail = tail:(pipe @sequence)+
|
choose_tail = tail:(pipe @sequence)+
|
||||||
{ return { alignment: 'rand', list: tail }; }
|
{ return { alignment: 'rand', list: tail, seed: seed++ }; }
|
||||||
|
|
||||||
// if the stack contains only one element, we don't create a stack but return the
|
// if the stack contains only one element, we don't create a stack but return the
|
||||||
// underlying element
|
// underlying element
|
||||||
stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
|
stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
|
||||||
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
|
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
|
||||||
|
|
||||||
polymeter_stack = head:sequence tail:stack_tail?
|
polymeter_stack = head:sequence tail:stack_tail?
|
||||||
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
|
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
|
||||||
@@ -175,7 +182,7 @@ polymeter_stack = head:sequence tail:stack_tail?
|
|||||||
// Experimental haskellish parser begins
|
// Experimental haskellish parser begins
|
||||||
|
|
||||||
// mini-notation = a quoted stack
|
// mini-notation = a quoted stack
|
||||||
mini = ws quote sc:stack_or_choose quote
|
mini = ws quote ws sc:stack_or_choose ws quote
|
||||||
{ return sc; }
|
{ return sc; }
|
||||||
|
|
||||||
// ------------------ operators ---------------------------
|
// ------------------ operators ---------------------------
|
||||||
|
|||||||
+16
-30
@@ -7,17 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import * as krill from './krill-parser.js';
|
import * as krill from './krill-parser.js';
|
||||||
import * as strudel from '@strudel.cycles/core';
|
import * as strudel from '@strudel.cycles/core';
|
||||||
|
|
||||||
/* var _seedState = 0;
|
const randOffset = 0.0003;
|
||||||
const randOffset = 0.0002;
|
|
||||||
|
|
||||||
function _nextSeed() {
|
|
||||||
return _seedState++;
|
|
||||||
} */
|
|
||||||
|
|
||||||
const applyOptions = (parent, code) => (pat, i) => {
|
const applyOptions = (parent, code) => (pat, i) => {
|
||||||
const ast = parent.source_[i];
|
const ast = parent.source_[i];
|
||||||
const options = ast.options_;
|
const options = ast.options_;
|
||||||
const ops = options?.ops;
|
const ops = options?.ops;
|
||||||
|
|
||||||
if (ops) {
|
if (ops) {
|
||||||
for (const op of ops) {
|
for (const op of ops) {
|
||||||
switch (op.type_) {
|
switch (op.type_) {
|
||||||
@@ -43,27 +39,14 @@ const applyOptions = (parent, code) => (pat, i) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'degradeBy': {
|
case 'degradeBy': {
|
||||||
// TODO: find out what is right here
|
pat = strudel
|
||||||
// example:
|
.reify(pat)
|
||||||
/*
|
._degradeByWith(strudel.rand.early(randOffset * op.arguments_.seed), op.arguments_.amount ?? 0.5);
|
||||||
stack(
|
break;
|
||||||
s("hh*8").degrade(),
|
}
|
||||||
s("[ht*8]?")
|
case 'tail': {
|
||||||
)
|
const friend = patternifyAST(op.arguments_.element, code);
|
||||||
*/
|
pat = pat.fmap((a) => (b) => Array.isArray(a) ? [...a, b] : [a, b]).appLeft(friend);
|
||||||
// above example will only be in sync when _degradeBy is used...
|
|
||||||
// it also seems that the nextSeed will create undeterministic behaviour
|
|
||||||
// as it uses a global _seedState. This is probably the reason for
|
|
||||||
// https://github.com/tidalcycles/strudel/issues/245
|
|
||||||
|
|
||||||
// this is how it was:
|
|
||||||
/*
|
|
||||||
return strudel.reify(pat)._degradeByWith(
|
|
||||||
strudel.rand.early(randOffset * _nextSeed()).segment(1),
|
|
||||||
op.arguments_.amount ?? 0.5,
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -108,9 +91,7 @@ export function patternifyAST(ast, code) {
|
|||||||
return strudel.stack(...aligned);
|
return strudel.stack(...aligned);
|
||||||
}
|
}
|
||||||
if (alignment === 'rand') {
|
if (alignment === 'rand') {
|
||||||
// https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422
|
return strudel.chooseInWith(strudel.rand.early(randOffset * ast.arguments_.seed).segment(1), children);
|
||||||
// return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
|
|
||||||
return strudel.chooseCycles(...children);
|
|
||||||
}
|
}
|
||||||
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
||||||
if (!weightedChildren && alignment === 'slowcat') {
|
if (!weightedChildren && alignment === 'slowcat') {
|
||||||
@@ -209,3 +190,8 @@ export function minify(thing) {
|
|||||||
}
|
}
|
||||||
return strudel.reify(thing);
|
return strudel.reify(thing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calling this function will cause patterns to parse strings as mini notation by default
|
||||||
|
export function miniAllStrings() {
|
||||||
|
strudel.setStringParser(mini);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/mini",
|
"name": "@strudel.cycles/mini",
|
||||||
"version": "0.6.0",
|
"version": "0.8.2",
|
||||||
"description": "Mini notation for strudel",
|
"description": "Mini notation for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
"@strudel.cycles/core": "workspace:*"
|
"@strudel.cycles/core": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"peggy": "^2.0.1",
|
"peggy": "^3.0.2",
|
||||||
"vite": "^3.2.2",
|
"vite": "^4.3.3",
|
||||||
"vitest": "^0.25.7"
|
"vitest": "^0.28.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,22 @@ describe('mini', () => {
|
|||||||
expect(haps.length < 230).toBe(true);
|
expect(haps.length < 230).toBe(true);
|
||||||
// 'Had too many cycles remaining after degradeBy 0.8');
|
// 'Had too many cycles remaining after degradeBy 0.8');
|
||||||
});
|
});
|
||||||
/*it('supports the random choice operator ("|") with nesting', () => {
|
it('supports multiple independent uses of the random choice operator ("|")', () => {
|
||||||
|
const numCycles = 1000;
|
||||||
|
const values = mini('[a|b] [a|b]')
|
||||||
|
.queryArc(0, numCycles)
|
||||||
|
.map((e) => e.value);
|
||||||
|
const observed = { aa: 0, ab: 0, ba: 0, bb: 0 };
|
||||||
|
for (let i = 0; i < values.length; i += 2) {
|
||||||
|
const chunk = values.slice(i, i + 2);
|
||||||
|
observed[chunk.join('')]++;
|
||||||
|
}
|
||||||
|
for (const count of Object.values(observed)) {
|
||||||
|
// Should fall within 99% confidence interval for binomial with p=0.25.
|
||||||
|
expect(215 <= count && count <= 286).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('supports the random choice operator ("|") with nesting', () => {
|
||||||
const numCycles = 900;
|
const numCycles = 900;
|
||||||
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
|
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
|
||||||
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
|
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
|
||||||
@@ -165,6 +180,8 @@ describe('mini', () => {
|
|||||||
// 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
|
// 15.086 is the chisq for 5 degrees of freedom at 99%, so for 99% of uniformly-distributed
|
||||||
// PRNG, this test should succeed
|
// PRNG, this test should succeed
|
||||||
expect(chisq <= 15.086).toBe(true);
|
expect(chisq <= 15.086).toBe(true);
|
||||||
// assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
|
});
|
||||||
});*/
|
it('supports lists', () => {
|
||||||
|
expect(minV('a:b c:d:[e:f] g')).toEqual([['a', 'b'], ['c', 'd', ['e', 'f']], 'g']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,14 +39,16 @@ let startedAt = -1;
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
|
* Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software.
|
||||||
|
* For more info, read [MIDI & OSC in the docs](https://strudel.tidalcycles.org/learn/input-output)
|
||||||
*
|
*
|
||||||
* @name osc
|
* @name osc
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.osc = async function () {
|
Pattern.prototype.osc = function () {
|
||||||
|
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
|
||||||
|
hap.ensureObjectValue();
|
||||||
const osc = await connect();
|
const osc = await connect();
|
||||||
return this.onTrigger((time, hap, currentTime, cps = 1) => {
|
|
||||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
const cycle = hap.wholeOrPart().begin.valueOf();
|
||||||
const delta = hap.duration.valueOf();
|
const delta = hap.duration.valueOf();
|
||||||
// time should be audio time of onset
|
// time should be audio time of onset
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/osc",
|
"name": "@strudel.cycles/osc",
|
||||||
"version": "0.6.0",
|
"version": "0.8.0",
|
||||||
"description": "OSC messaging for strudel",
|
"description": "OSC messaging for strudel",
|
||||||
"main": "osc.mjs",
|
"main": "osc.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"osc-js": "^2.4.0"
|
"osc-js": "^2.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pkg": "^5.7.0",
|
"pkg": "^5.8.1",
|
||||||
"vite": "^3.2.2"
|
"vite": "^4.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,24 +13,43 @@ npm i @strudel.cycles/react
|
|||||||
Here is a minimal example of how to set up a MiniRepl:
|
Here is a minimal example of how to set up a MiniRepl:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
import { evalScope, controls } from '@strudel.cycles/core';
|
import * as React from 'react';
|
||||||
|
import '@strudel.cycles/react/dist/style.css';
|
||||||
import { MiniRepl } from '@strudel.cycles/react';
|
import { MiniRepl } from '@strudel.cycles/react';
|
||||||
import { prebake } from '../repl/src/prebake.mjs';
|
import { evalScope, controls } from '@strudel.cycles/core';
|
||||||
|
import { samples, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
||||||
|
|
||||||
evalScope(
|
async function prebake() {
|
||||||
|
await samples(
|
||||||
|
'https://strudel.tidalcycles.org/tidal-drum-machines.json',
|
||||||
|
'github:ritchse/tidal-drum-machines/main/machines/'
|
||||||
|
);
|
||||||
|
await samples(
|
||||||
|
'https://strudel.tidalcycles.org/EmuSP12.json',
|
||||||
|
'https://strudel.tidalcycles.org/EmuSP12/'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
await evalScope(
|
||||||
controls,
|
controls,
|
||||||
import('@strudel.cycles/core'),
|
import('@strudel.cycles/core'),
|
||||||
import('@strudel.cycles/tonal'),
|
|
||||||
import('@strudel.cycles/mini'),
|
import('@strudel.cycles/mini'),
|
||||||
import('@strudel.cycles/webaudio'),
|
import('@strudel.cycles/webaudio'),
|
||||||
/* probably import other strudel packages */
|
import('@strudel.cycles/tonal')
|
||||||
);
|
);
|
||||||
|
await prebake();
|
||||||
|
initAudioOnFirstClick();
|
||||||
|
}
|
||||||
|
|
||||||
prebake();
|
if (typeof window !== 'undefined') {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
export function Repl({ tune }) {
|
export default function App() {
|
||||||
return <MiniRepl tune={tune} hideOutsideView={true} />;
|
return <MiniRepl tune={`s("bd sd,hh*4")`} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
For a more sophisticated example, check out the [nano-repl](./examples/nano-repl/)!
|
- Open [example on stackblitz](https://stackblitz.com/edit/react-ts-saaair?file=tune.tsx,App.tsx)
|
||||||
|
- Also check out the [nano-repl](./examples/nano-repl/) for a more sophisticated example
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# nano-repl
|
||||||
|
|
||||||
|
this is an example of how to create a repl with strudel and react.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
after cloning the strudel repo:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm i
|
||||||
|
cd packages/react/examples/nano-repl
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
you should now have a repl running at `http://localhost:5173/`
|
||||||
@@ -15,17 +15,18 @@
|
|||||||
"@strudel.cycles/osc": "workspace:*",
|
"@strudel.cycles/osc": "workspace:*",
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
"@strudel.cycles/mini": "workspace:*",
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
"@strudel.cycles/transpiler": "workspace:*",
|
||||||
|
"@strudel.cycles/soundfonts": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
"@strudel.cycles/tonal": "workspace:*",
|
"@strudel.cycles/tonal": "workspace:*",
|
||||||
"@strudel.cycles/react": "workspace:*"
|
"@strudel.cycles/react": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.2.0",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.2.1",
|
||||||
"@vitejs/plugin-react": "^2.0.1",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"autoprefixer": "^10.4.8",
|
"autoprefixer": "^10.4.14",
|
||||||
"postcss": "^8.4.16",
|
"postcss": "^8.4.23",
|
||||||
"tailwindcss": "^3.1.8",
|
"tailwindcss": "^3.3.2",
|
||||||
"vite": "^3.0.7"
|
"vite": "^4.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
import { controls, evalScope } from '@strudel.cycles/core';
|
import { controls, evalScope } from '@strudel.cycles/core';
|
||||||
import { CodeMirror, useHighlighting, useKeydown, useStrudel, flash } from '@strudel.cycles/react';
|
import { CodeMirror, useHighlighting, useKeydown, useStrudel, flash } from '@strudel.cycles/react';
|
||||||
import { getAudioContext, initAudioOnFirstClick, panic, webaudioOutput } from '@strudel.cycles/webaudio';
|
import {
|
||||||
|
getAudioContext,
|
||||||
|
initAudioOnFirstClick,
|
||||||
|
panic,
|
||||||
|
webaudioOutput,
|
||||||
|
registerSynthSounds,
|
||||||
|
} from '@strudel.cycles/webaudio';
|
||||||
|
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
// import { prebake } from '../../../../../repl/src/prebake.mjs';
|
// import { prebake } from '../../../../../repl/src/prebake.mjs';
|
||||||
|
|
||||||
initAudioOnFirstClick();
|
initAudioOnFirstClick();
|
||||||
|
|
||||||
|
async function init() {
|
||||||
// TODO: only import stuff when play is pressed?
|
// TODO: only import stuff when play is pressed?
|
||||||
evalScope(
|
const loadModules = evalScope(
|
||||||
controls,
|
controls,
|
||||||
import('@strudel.cycles/core'),
|
import('@strudel.cycles/core'),
|
||||||
import('@strudel.cycles/tonal'),
|
import('@strudel.cycles/tonal'),
|
||||||
@@ -18,6 +26,10 @@ evalScope(
|
|||||||
import('@strudel.cycles/osc'),
|
import('@strudel.cycles/osc'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
|
||||||
|
}
|
||||||
|
init();
|
||||||
|
|
||||||
const defaultTune = `samples({
|
const defaultTune = `samples({
|
||||||
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
|
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'],
|
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
|
||||||
@@ -31,7 +43,7 @@ stack(
|
|||||||
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
|
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
|
||||||
.add(perlin.range(0,.5)) // random pitch variation
|
.add(perlin.range(0,.5)) // random pitch variation
|
||||||
.superimpose(add(.05)) // add second, slightly detuned voice
|
.superimpose(add(.05)) // add second, slightly detuned voice
|
||||||
.n() // wrap in "n"
|
.note() // wrap in "note"
|
||||||
.decay(.15).sustain(0) // make each note of equal length
|
.decay(.15).sustain(0) // make each note of equal length
|
||||||
.s('sawtooth') // waveform
|
.s('sawtooth') // waveform
|
||||||
.gain(.4) // turn down
|
.gain(.4) // turn down
|
||||||
@@ -40,7 +52,7 @@ stack(
|
|||||||
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
|
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
|
||||||
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
||||||
.add(perlin.range(0,.5)) // random pitch variation
|
.add(perlin.range(0,.5)) // random pitch variation
|
||||||
.n() // wrap in "n"
|
.note() // wrap in "n"
|
||||||
.s('square') // waveform
|
.s('square') // waveform
|
||||||
.gain(.16) // turn down
|
.gain(.16) // turn down
|
||||||
.cutoff(500) // fixed cutoff
|
.cutoff(500) // fixed cutoff
|
||||||
@@ -49,7 +61,7 @@ stack(
|
|||||||
,"a4 c5 <e6 a6>".struct("x(5,8)")
|
,"a4 c5 <e6 a6>".struct("x(5,8)")
|
||||||
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
||||||
.add(perlin.range(0,.5)) // random pitch variation
|
.add(perlin.range(0,.5)) // random pitch variation
|
||||||
.n() // wrap in "n"
|
.note() // wrap in "note"
|
||||||
.decay(.1).sustain(0) // make notes short
|
.decay(.1).sustain(0) // make notes short
|
||||||
.s('triangle') // waveform
|
.s('triangle') // waveform
|
||||||
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
|
.degradeBy(perlin.range(0,.5)) // randomly controlled random removal :)
|
||||||
@@ -66,7 +78,7 @@ function App() {
|
|||||||
const [code, setCode] = useState(defaultTune);
|
const [code, setCode] = useState(defaultTune);
|
||||||
const [view, setView] = useState();
|
const [view, setView] = useState();
|
||||||
// const [code, setCode] = useState(`"c3".note().slow(2)`);
|
// const [code, setCode] = useState(`"c3".note().slow(2)`);
|
||||||
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern } = useStrudel({
|
const { scheduler, evaluate, schedulerError, evalError, isDirty, activeCode, pattern, started } = useStrudel({
|
||||||
code,
|
code,
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: webaudioOutput,
|
||||||
getTime,
|
getTime,
|
||||||
@@ -75,8 +87,8 @@ function App() {
|
|||||||
useHighlighting({
|
useHighlighting({
|
||||||
view,
|
view,
|
||||||
pattern,
|
pattern,
|
||||||
active: !activeCode?.includes('strudel disable-highlighting'),
|
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||||
getTime: () => scheduler.getPhase(),
|
getTime: () => scheduler.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const error = evalError || schedulerError;
|
const error = evalError || schedulerError;
|
||||||
@@ -103,7 +115,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[scheduler, evaluate, view],
|
[scheduler, evaluate, view, code],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,6 +2,18 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: #222;
|
||||||
|
--lineBackground: #22222250;
|
||||||
|
--foreground: #fff;
|
||||||
|
--caret: #ffcc00;
|
||||||
|
--selection: rgba(128, 203, 196, 0.5);
|
||||||
|
--selectionMatch: #036dd626;
|
||||||
|
--lineHighlight: #00000050;
|
||||||
|
--gutterBackground: transparent;
|
||||||
|
--gutterForeground: #8a919966;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: #123;
|
background: #123;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,23 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// TODO: find out if leaving out tutorial path works now
|
// TODO: find out if leaving out tutorial path works now
|
||||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
content: ['./src/**/*.{js,jsx,ts,tsx}', '../../src/**/*.{html,js,jsx,md,mdx,ts,tsx}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
colors: {
|
||||||
|
// codemirror-theme settings
|
||||||
|
background: 'var(--background)',
|
||||||
|
lineBackground: 'var(--lineBackground)',
|
||||||
|
foreground: 'var(--foreground)',
|
||||||
|
caret: 'var(--caret)',
|
||||||
|
selection: 'var(--selection)',
|
||||||
|
selectionMatch: 'var(--selectionMatch)',
|
||||||
|
gutterBackground: 'var(--gutterBackground)',
|
||||||
|
gutterForeground: 'var(--gutterForeground)',
|
||||||
|
gutterBorder: 'var(--gutterBorder)',
|
||||||
|
lineHighlight: 'var(--lineHighlight)',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
};
|
};
|
||||||
|
|||||||
+20
-17
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/react",
|
"name": "@strudel.cycles/react",
|
||||||
"version": "0.6.0",
|
"version": "0.8.0",
|
||||||
"description": "React components for strudel",
|
"description": "React components for strudel",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -32,30 +32,33 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-javascript": "^6.1.1",
|
"@codemirror/autocomplete": "^6.6.0",
|
||||||
|
"@codemirror/lang-javascript": "^6.1.7",
|
||||||
"@codemirror/state": "^6.2.0",
|
"@codemirror/state": "^6.2.0",
|
||||||
"@codemirror/view": "^6.7.3",
|
"@codemirror/view": "^6.10.0",
|
||||||
"@lezer/highlight": "^1.1.3",
|
"@lezer/highlight": "^1.1.4",
|
||||||
|
"@replit/codemirror-emacs": "^6.0.1",
|
||||||
|
"@replit/codemirror-vim": "^6.0.14",
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
"@strudel.cycles/transpiler": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
"@uiw/codemirror-themes": "^4.12.4",
|
"@uiw/codemirror-themes": "^4.19.16",
|
||||||
"@uiw/react-codemirror": "^4.12.4",
|
"@uiw/react-codemirror": "^4.19.16",
|
||||||
"react-hook-inview": "^4.5.0"
|
"react-hook-inview": "^4.5.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^17.0.2",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^17.0.2"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^17.0.2",
|
"@types/react": "^18.2.0",
|
||||||
"@types/react-dom": "^17.0.2",
|
"@types/react-dom": "^18.2.1",
|
||||||
"@vitejs/plugin-react": "^2.2.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.14",
|
||||||
"postcss": "^8.4.18",
|
"postcss": "^8.4.23",
|
||||||
"react": "^17.0.2",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^18.2.0",
|
||||||
"tailwindcss": "^3.0.24",
|
"tailwindcss": "^3.3.2",
|
||||||
"vite": "^3.2.2"
|
"vite": "^4.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ evalScope(
|
|||||||
import('@strudel.cycles/core'),
|
import('@strudel.cycles/core'),
|
||||||
import('@strudel.cycles/tonal'),
|
import('@strudel.cycles/tonal'),
|
||||||
import('@strudel.cycles/mini'),
|
import('@strudel.cycles/mini'),
|
||||||
import('@strudel.cycles/midi'),
|
|
||||||
import('@strudel.cycles/xen'),
|
|
||||||
import('@strudel.cycles/webaudio'),
|
import('@strudel.cycles/webaudio'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import jsdoc from '../../../../doc.json';
|
||||||
|
|
||||||
|
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||||
|
const getInnerText = (html) => {
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.innerHTML = html;
|
||||||
|
return div.textContent || div.innerText || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Autocomplete({ doc }) {
|
||||||
|
return (
|
||||||
|
<div className="prose dark:prose-invert max-h-[400px] overflow-auto">
|
||||||
|
<h3 className="pt-0 mt-0">{getDocLabel(doc)}</h3>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: doc.description }} />
|
||||||
|
<ul>
|
||||||
|
{doc.params?.map(({ name, type, description }, i) => (
|
||||||
|
<li key={i}>
|
||||||
|
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
{doc.examples?.map((example, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
<pre
|
||||||
|
className="cursor-pointer"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
console.log('ola!');
|
||||||
|
navigator.clipboard.writeText(example);
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{example}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsdocCompletions = jsdoc.docs
|
||||||
|
.filter(
|
||||||
|
(doc) =>
|
||||||
|
getDocLabel(doc) &&
|
||||||
|
!getDocLabel(doc).startsWith('_') &&
|
||||||
|
!['package'].includes(doc.kind) &&
|
||||||
|
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
|
||||||
|
)
|
||||||
|
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
||||||
|
.map((doc) /*: Completion */ => ({
|
||||||
|
label: getDocLabel(doc),
|
||||||
|
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
||||||
|
info: () => {
|
||||||
|
const node = document.createElement('div');
|
||||||
|
// if Autocomplete is non-interactive, it could also be rendered at build time..
|
||||||
|
// .. using renderToStaticMarkup
|
||||||
|
createRoot(node).render(<Autocomplete doc={doc} />);
|
||||||
|
return node;
|
||||||
|
},
|
||||||
|
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const strudelAutocomplete = (context /* : CompletionContext */) => {
|
||||||
|
let word = context.matchBefore(/\w*/);
|
||||||
|
if (word.from == word.to && !context.explicit) return null;
|
||||||
|
return {
|
||||||
|
from: word.from,
|
||||||
|
options: jsdocCompletions,
|
||||||
|
/* options: [
|
||||||
|
{ label: 'match', type: 'keyword' },
|
||||||
|
{ label: 'hello', type: 'variable', info: '(World)' },
|
||||||
|
{ label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
|
||||||
|
], */
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import _CodeMirror from '@uiw/react-codemirror';
|
import _CodeMirror from '@uiw/react-codemirror';
|
||||||
import { EditorView, Decoration } from '@codemirror/view';
|
import { EditorView, Decoration } from '@codemirror/view';
|
||||||
import { StateField, StateEffect } from '@codemirror/state';
|
import { StateField, StateEffect } from '@codemirror/state';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
||||||
import strudelTheme from '../themes/strudel-theme';
|
import strudelTheme from '../themes/strudel-theme';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { autocompletion } from '@codemirror/autocomplete';
|
||||||
|
import { strudelAutocomplete } from './Autocomplete';
|
||||||
|
import { vim } from '@replit/codemirror-vim';
|
||||||
|
import { emacs } from '@replit/codemirror-emacs';
|
||||||
|
|
||||||
export const setFlash = StateEffect.define();
|
export const setFlash = StateEffect.define();
|
||||||
const flashField = StateField.define({
|
const flashField = StateField.define({
|
||||||
@@ -49,19 +53,24 @@ const highlightField = StateField.define({
|
|||||||
try {
|
try {
|
||||||
for (let e of tr.effects) {
|
for (let e of tr.effects) {
|
||||||
if (e.is(setHighlights)) {
|
if (e.is(setHighlights)) {
|
||||||
|
const { haps } = e.value;
|
||||||
const marks =
|
const marks =
|
||||||
e.value
|
haps
|
||||||
.map((hap) =>
|
.map((hap) =>
|
||||||
(hap.context.locations || []).map(({ start, end }) => {
|
(hap.context.locations || []).map(({ start, end }) => {
|
||||||
const color = hap.context.color || '#FFCA28';
|
const color = hap.context.color || e.value.color;
|
||||||
let from = tr.newDoc.line(start.line).from + start.column;
|
let from = tr.newDoc.line(start.line).from + start.column;
|
||||||
let to = tr.newDoc.line(end.line).from + end.column;
|
let to = tr.newDoc.line(end.line).from + end.column;
|
||||||
const l = tr.newDoc.length;
|
const l = tr.newDoc.length;
|
||||||
if (from > l || to > l) {
|
if (from > l || to > l) {
|
||||||
return; // dont mark outside of range, as it will throw an error
|
return; // dont mark outside of range, as it will throw an error
|
||||||
}
|
}
|
||||||
// const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
|
let mark;
|
||||||
const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } });
|
if (color) {
|
||||||
|
mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
|
||||||
|
} else {
|
||||||
|
mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
|
||||||
|
}
|
||||||
return mark.range(from, to);
|
return mark.range(from, to);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -79,21 +88,37 @@ const highlightField = StateField.define({
|
|||||||
provide: (f) => EditorView.decorations.from(f),
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
});
|
});
|
||||||
|
|
||||||
const extensions = [javascript(), strudelTheme, highlightField, flashField];
|
const staticExtensions = [javascript(), highlightField, flashField];
|
||||||
|
|
||||||
export default function CodeMirror({ value, onChange, onViewChanged, onSelectionChange, options, editorDidMount }) {
|
export default function CodeMirror({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onViewChanged,
|
||||||
|
onSelectionChange,
|
||||||
|
theme,
|
||||||
|
keybindings,
|
||||||
|
isLineNumbersDisplayed,
|
||||||
|
isAutoCompletionEnabled,
|
||||||
|
isLineWrappingEnabled,
|
||||||
|
fontSize = 18,
|
||||||
|
fontFamily = 'monospace',
|
||||||
|
options,
|
||||||
|
editorDidMount,
|
||||||
|
}) {
|
||||||
const handleOnChange = useCallback(
|
const handleOnChange = useCallback(
|
||||||
(value) => {
|
(value) => {
|
||||||
onChange?.(value);
|
onChange?.(value);
|
||||||
},
|
},
|
||||||
[onChange],
|
[onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnCreateEditor = useCallback(
|
const handleOnCreateEditor = useCallback(
|
||||||
(view) => {
|
(view) => {
|
||||||
onViewChanged?.(view);
|
onViewChanged?.(view);
|
||||||
},
|
},
|
||||||
[onViewChanged],
|
[onViewChanged],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnUpdate = useCallback(
|
const handleOnUpdate = useCallback(
|
||||||
(viewUpdate) => {
|
(viewUpdate) => {
|
||||||
if (viewUpdate.selectionSet && onSelectionChange) {
|
if (viewUpdate.selectionSet && onSelectionChange) {
|
||||||
@@ -102,16 +127,43 @@ export default function CodeMirror({ value, onChange, onViewChanged, onSelection
|
|||||||
},
|
},
|
||||||
[onSelectionChange],
|
[onSelectionChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const extensions = useMemo(() => {
|
||||||
|
let _extensions = [...staticExtensions];
|
||||||
|
let bindings = {
|
||||||
|
vim,
|
||||||
|
emacs,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bindings[keybindings]) {
|
||||||
|
_extensions.push(bindings[keybindings]());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAutoCompletionEnabled) {
|
||||||
|
_extensions.push(javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }));
|
||||||
|
} else {
|
||||||
|
_extensions.push(autocompletion({ override: [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLineWrappingEnabled) {
|
||||||
|
_extensions.push(EditorView.lineWrapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _extensions;
|
||||||
|
}, [keybindings, isAutoCompletionEnabled, isLineWrappingEnabled]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div style={{ fontSize, fontFamily }} className="w-full">
|
||||||
<_CodeMirror
|
<_CodeMirror
|
||||||
value={value}
|
value={value}
|
||||||
|
theme={theme || strudelTheme}
|
||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
onCreateEditor={handleOnCreateEditor}
|
onCreateEditor={handleOnCreateEditor}
|
||||||
onUpdate={handleOnUpdate}
|
onUpdate={handleOnUpdate}
|
||||||
extensions={extensions}
|
extensions={extensions}
|
||||||
|
basicSetup={{ lineNumbers: isLineNumbersDisplayed }}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,18 +7,35 @@ import useHighlighting from '../hooks/useHighlighting.mjs';
|
|||||||
import useStrudel from '../hooks/useStrudel.mjs';
|
import useStrudel from '../hooks/useStrudel.mjs';
|
||||||
import CodeMirror6, { flash } from './CodeMirror6';
|
import CodeMirror6, { flash } from './CodeMirror6';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import styles from './MiniRepl.module.css';
|
|
||||||
import './style.css';
|
import './style.css';
|
||||||
import { logger } from '@strudel.cycles/core';
|
import { logger } from '@strudel.cycles/core';
|
||||||
|
import useEvent from '../hooks/useEvent.mjs';
|
||||||
|
import useKeydown from '../hooks/useKeydown.mjs';
|
||||||
|
|
||||||
const getTime = () => getAudioContext().currentTime;
|
const getTime = () => getAudioContext().currentTime;
|
||||||
|
|
||||||
export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTime, punchcard, canvasHeight = 200 }) {
|
export function MiniRepl({
|
||||||
|
tune,
|
||||||
|
hideOutsideView = false,
|
||||||
|
enableKeyboard,
|
||||||
|
onTrigger,
|
||||||
|
drawTime,
|
||||||
|
punchcard,
|
||||||
|
punchcardLabels,
|
||||||
|
onPaint,
|
||||||
|
canvasHeight = 200,
|
||||||
|
fontSize = 18,
|
||||||
|
fontFamily,
|
||||||
|
hideHeader = false,
|
||||||
|
theme,
|
||||||
|
keybindings,
|
||||||
|
isLineNumbersDisplayed,
|
||||||
|
}) {
|
||||||
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
|
drawTime = drawTime || (punchcard ? [0, 4] : undefined);
|
||||||
const evalOnMount = !!drawTime;
|
const evalOnMount = !!drawTime;
|
||||||
const drawContext = useCallback(
|
const drawContext = useCallback(
|
||||||
!!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
|
punchcard ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null,
|
||||||
[drawTime],
|
[punchcard],
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
code,
|
code,
|
||||||
@@ -38,7 +55,18 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTi
|
|||||||
} = useStrudel({
|
} = useStrudel({
|
||||||
initialCode: tune,
|
initialCode: tune,
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: webaudioOutput,
|
||||||
editPattern: (pat) => (punchcard ? pat.punchcard() : pat),
|
editPattern: (pat, id) => {
|
||||||
|
//pat = pat.withContext((ctx) => ({ ...ctx, id }));
|
||||||
|
if (onTrigger) {
|
||||||
|
pat = pat.onTrigger(onTrigger, false);
|
||||||
|
}
|
||||||
|
if (onPaint) {
|
||||||
|
pat = pat.onPaint(onPaint);
|
||||||
|
} else if (punchcard) {
|
||||||
|
pat = pat.punchcard({ labels: punchcardLabels });
|
||||||
|
}
|
||||||
|
return pat;
|
||||||
|
},
|
||||||
getTime,
|
getTime,
|
||||||
evalOnMount,
|
evalOnMount,
|
||||||
drawContext,
|
drawContext,
|
||||||
@@ -63,25 +91,26 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTi
|
|||||||
getTime: () => scheduler.now(),
|
getTime: () => scheduler.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// set active pattern on ctrl+enter
|
// keyboard shortcuts
|
||||||
useLayoutEffect(() => {
|
useKeydown(
|
||||||
if (enableKeyboard) {
|
useCallback(
|
||||||
const handleKeyPress = async (e) => {
|
async (e) => {
|
||||||
|
if (view?.hasFocus) {
|
||||||
if (e.ctrlKey || e.altKey) {
|
if (e.ctrlKey || e.altKey) {
|
||||||
if (e.code === 'Enter') {
|
if (e.code === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
flash(view);
|
flash(view);
|
||||||
await activateCode();
|
await activateCode();
|
||||||
} else if (e.code === 'Period') {
|
} else if (e.key === '.' || e.code === 'Period') {
|
||||||
stop();
|
stop();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
window.addEventListener('keydown', handleKeyPress, true);
|
|
||||||
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
|
||||||
}
|
}
|
||||||
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
|
},
|
||||||
|
[activateCode, stop, view],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const [log, setLog] = useState([]);
|
const [log, setLog] = useState([]);
|
||||||
useLogger(
|
useLogger(
|
||||||
@@ -91,29 +120,54 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTi
|
|||||||
// const logId = data?.pattern?.meta?.id;
|
// const logId = data?.pattern?.meta?.id;
|
||||||
if (logId === replId) {
|
if (logId === replId) {
|
||||||
setLog((l) => {
|
setLog((l) => {
|
||||||
return l.concat([e.detail]).slice(-10);
|
return l.concat([e.detail]).slice(-8);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []),
|
}, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container} ref={ref}>
|
<div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight" ref={ref}>
|
||||||
<div className={styles.header}>
|
{!hideHeader && (
|
||||||
<div className={styles.buttons}>
|
<div className="flex justify-between bg-lineHighlight">
|
||||||
<button className={cx(styles.button, started ? 'animate-pulse' : '')} onClick={() => togglePlay()}>
|
<div className="flex">
|
||||||
|
<button
|
||||||
|
className={cx(
|
||||||
|
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
|
||||||
|
started ? 'animate-pulse' : '',
|
||||||
|
)}
|
||||||
|
onClick={() => togglePlay()}
|
||||||
|
>
|
||||||
<Icon type={started ? 'stop' : 'play'} />
|
<Icon type={started ? 'stop' : 'play'} />
|
||||||
</button>
|
</button>
|
||||||
<button className={cx(isDirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
<button
|
||||||
|
className={cx(
|
||||||
|
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
|
||||||
|
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
|
||||||
|
)}
|
||||||
|
onClick={() => activateCode()}
|
||||||
|
>
|
||||||
<Icon type="refresh" />
|
<Icon type="refresh" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{error && <div className={styles.error}>{error.message}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.body}>
|
)}
|
||||||
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
|
<div className="overflow-auto relative">
|
||||||
|
{show && (
|
||||||
|
<CodeMirror6
|
||||||
|
value={code}
|
||||||
|
onChange={setCode}
|
||||||
|
onViewChanged={setView}
|
||||||
|
theme={theme}
|
||||||
|
fontFamily={fontFamily}
|
||||||
|
fontSize={fontSize}
|
||||||
|
keybindings={keybindings}
|
||||||
|
isLineNumbersDisplayed={isLineNumbersDisplayed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
|
||||||
</div>
|
</div>
|
||||||
{drawTime && (
|
{punchcard && (
|
||||||
<canvas
|
<canvas
|
||||||
id={canvasId}
|
id={canvasId}
|
||||||
className="w-full pointer-events-none"
|
className="w-full pointer-events-none"
|
||||||
@@ -140,13 +194,3 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTi
|
|||||||
function useLogger(onTrigger) {
|
function useLogger(onTrigger) {
|
||||||
useEvent(logger.key, onTrigger);
|
useEvent(logger.key, onTrigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: dedupe
|
|
||||||
function useEvent(name, onTrigger, useCapture = false) {
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener(name, onTrigger, useCapture);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener(name, onTrigger, useCapture);
|
|
||||||
};
|
|
||||||
}, [onTrigger]);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
.container {
|
|
||||||
@apply rounded-md overflow-hidden bg-[#222222];
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
@apply flex justify-between bg-slate-700 border-t border-slate-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
@apply flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
@apply cursor-pointer w-16 flex items-center justify-center p-1 bg-slate-700 border-r border-slate-500 text-white hover:bg-slate-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonDisabled {
|
|
||||||
@apply w-16 flex items-center justify-center p-1 bg-slate-600 text-slate-400 cursor-not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
@apply text-right p-1 text-sm text-red-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
@apply overflow-auto relative;
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,30 @@
|
|||||||
|
:root {
|
||||||
|
--background: #222;
|
||||||
|
--lineBackground: #22222299;
|
||||||
|
--foreground: #fff;
|
||||||
|
--caret: #ffcc00;
|
||||||
|
--selection: rgba(128, 203, 196, 0.5);
|
||||||
|
--selectionMatch: #036dd626;
|
||||||
|
--lineHighlight: #00000050;
|
||||||
|
--gutterBackground: transparent;
|
||||||
|
--gutterForeground: #8a919966;
|
||||||
|
}
|
||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
font-size: 18px;
|
}
|
||||||
|
|
||||||
|
.cm-theme {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-theme-light {
|
.cm-theme-light {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-line > * {
|
footer {
|
||||||
background: #00000095;
|
z-index: 0 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
function useEvent(name, onTrigger, useCapture = false) {
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener(name, onTrigger, useCapture);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener(name, onTrigger, useCapture);
|
||||||
|
};
|
||||||
|
}, [onTrigger]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useEvent;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { setHighlights } from '../components/CodeMirror6';
|
import { setHighlights } from '../components/CodeMirror6';
|
||||||
|
const round = (x) => Math.round(x * 1000) / 1000;
|
||||||
|
|
||||||
function useHighlighting({ view, pattern, active, getTime }) {
|
function useHighlighting({ view, pattern, active, getTime }) {
|
||||||
const highlights = useRef([]);
|
const highlights = useRef([]);
|
||||||
@@ -14,14 +15,14 @@ function useHighlighting({ view, pattern, active, getTime }) {
|
|||||||
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
|
// 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
|
// see https://github.com/tidalcycles/strudel/issues/108
|
||||||
const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy
|
const begin = Math.max(lastEnd.current ?? audioTime, audioTime - 1 / 10, -0.01); // negative time seems buggy
|
||||||
const span = [begin, audioTime + 1 / 60];
|
const span = [round(begin), round(audioTime + 1 / 60)];
|
||||||
lastEnd.current = span[1];
|
lastEnd.current = span[1];
|
||||||
highlights.current = highlights.current.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
|
highlights.current = highlights.current.filter((hap) => hap.endClipped > audioTime); // keep only highlights that are still active
|
||||||
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
||||||
highlights.current = highlights.current.concat(haps); // add potential new onsets
|
highlights.current = highlights.current.concat(haps); // add potential new onsets
|
||||||
view.dispatch({ effects: setHighlights.of(highlights.current) }); // highlight all still active + new active haps
|
view.dispatch({ effects: setHighlights.of({ haps: highlights.current }) }); // highlight all still active + new active haps
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
view.dispatch({ effects: setHighlights.of([]) });
|
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
|
||||||
}
|
}
|
||||||
frame = requestAnimationFrame(updateHighlights);
|
frame = requestAnimationFrame(updateHighlights);
|
||||||
});
|
});
|
||||||
@@ -30,7 +31,7 @@ function useHighlighting({ view, pattern, active, getTime }) {
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
highlights.current = [];
|
highlights.current = [];
|
||||||
view.dispatch({ effects: setHighlights.of([]) });
|
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [pattern, active, view]);
|
}, [pattern, active, view]);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ function useStrudel({
|
|||||||
getTime,
|
getTime,
|
||||||
evalOnMount = false,
|
evalOnMount = false,
|
||||||
initialCode = '',
|
initialCode = '',
|
||||||
autolink = false,
|
|
||||||
beforeEval,
|
beforeEval,
|
||||||
afterEval,
|
afterEval,
|
||||||
editPattern,
|
editPattern,
|
||||||
@@ -30,10 +29,11 @@ function useStrudel({
|
|||||||
const [pattern, setPattern] = useState();
|
const [pattern, setPattern] = useState();
|
||||||
const [started, setStarted] = useState(false);
|
const [started, setStarted] = useState(false);
|
||||||
const isDirty = code !== activeCode;
|
const isDirty = code !== activeCode;
|
||||||
const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
|
//const shouldPaint = useCallback((pat) => !!(pat?.context?.onPaint && drawContext), [drawContext]);
|
||||||
|
const shouldPaint = useCallback((pat) => !!pat?.context?.onPaint, []);
|
||||||
|
|
||||||
// TODO: make sure this hook reruns when scheduler.started changes
|
// TODO: make sure this hook reruns when scheduler.started changes
|
||||||
const { scheduler, evaluate, start, stop, pause } = useMemo(
|
const { scheduler, evaluate, start, stop, pause, setCps } = useMemo(
|
||||||
() =>
|
() =>
|
||||||
repl({
|
repl({
|
||||||
interval,
|
interval,
|
||||||
@@ -47,19 +47,17 @@ function useStrudel({
|
|||||||
drawContext,
|
drawContext,
|
||||||
transpiler,
|
transpiler,
|
||||||
editPattern,
|
editPattern,
|
||||||
beforeEval: ({ code }) => {
|
beforeEval: async ({ code }) => {
|
||||||
setCode(code);
|
setCode(code);
|
||||||
beforeEval?.();
|
await beforeEval?.();
|
||||||
},
|
},
|
||||||
afterEval: ({ pattern: _pattern, code }) => {
|
afterEval: (res) => {
|
||||||
|
const { pattern: _pattern, code } = res;
|
||||||
setActiveCode(code);
|
setActiveCode(code);
|
||||||
setPattern(_pattern);
|
setPattern(_pattern);
|
||||||
setEvalError();
|
setEvalError();
|
||||||
setSchedulerError();
|
setSchedulerError();
|
||||||
if (autolink) {
|
afterEval?.(res);
|
||||||
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
|
||||||
}
|
|
||||||
afterEval?.();
|
|
||||||
},
|
},
|
||||||
onToggle: (v) => {
|
onToggle: (v) => {
|
||||||
setStarted(v);
|
setStarted(v);
|
||||||
@@ -156,6 +154,7 @@ function useStrudel({
|
|||||||
stop,
|
stop,
|
||||||
pause,
|
pause,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
|
setCps,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
// import 'tailwindcss/tailwind.css';
|
// import 'tailwindcss/tailwind.css';
|
||||||
|
|
||||||
export { default as CodeMirror, flash } from './components/CodeMirror6';
|
export { default as CodeMirror, flash } from './components/CodeMirror6'; // !SSR
|
||||||
export * from './components/MiniRepl';
|
export * from './components/MiniRepl'; // !SSR
|
||||||
export { default as useHighlighting } from './hooks/useHighlighting';
|
export { default as useHighlighting } from './hooks/useHighlighting'; // !SSR
|
||||||
|
export { default as useStrudel } from './hooks/useStrudel'; // !SSR
|
||||||
export { default as usePostMessage } from './hooks/usePostMessage';
|
export { default as usePostMessage } from './hooks/usePostMessage';
|
||||||
export { default as useStrudel } from './hooks/useStrudel';
|
|
||||||
export { default as useKeydown } from './hooks/useKeydown';
|
export { default as useKeydown } from './hooks/useKeydown';
|
||||||
|
export { default as useEvent } from './hooks/useEvent';
|
||||||
|
export { default as strudelTheme } from './themes/strudel-theme';
|
||||||
export { default as cx } from './cx';
|
export { default as cx } from './cx';
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
ReactDOM.render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById('root'),
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
import { createTheme } from '@uiw/codemirror-themes';
|
||||||
|
export const settings = {
|
||||||
|
background: '#9bbc0f',
|
||||||
|
foreground: '#0f380f', // whats that?
|
||||||
|
caret: '#0f380f',
|
||||||
|
selection: '#306230',
|
||||||
|
selectionMatch: '#ffffff26',
|
||||||
|
lineHighlight: '#8bac0f',
|
||||||
|
lineBackground: '#9bbc0f50',
|
||||||
|
//lineBackground: 'transparent',
|
||||||
|
gutterBackground: 'transparent',
|
||||||
|
gutterForeground: '#0f380f',
|
||||||
|
light: true,
|
||||||
|
};
|
||||||
|
export default createTheme({
|
||||||
|
theme: 'light',
|
||||||
|
settings,
|
||||||
|
styles: [
|
||||||
|
{ tag: t.keyword, color: '#0f380f' },
|
||||||
|
{ tag: t.operator, color: '#0f380f' },
|
||||||
|
{ tag: t.special(t.variableName), color: '#0f380f' },
|
||||||
|
{ tag: t.typeName, color: '#0f380f' },
|
||||||
|
{ tag: t.atom, color: '#0f380f' },
|
||||||
|
{ tag: t.number, color: '#0f380f' },
|
||||||
|
{ tag: t.definition(t.variableName), color: '#0f380f' },
|
||||||
|
{ tag: t.string, color: '#0f380f' },
|
||||||
|
{ tag: t.special(t.string), color: '#0f380f' },
|
||||||
|
{ tag: t.comment, color: '#0f380f' },
|
||||||
|
{ tag: t.variableName, color: '#0f380f' },
|
||||||
|
{ tag: t.tagName, color: '#0f380f' },
|
||||||
|
{ tag: t.bracket, color: '#0f380f' },
|
||||||
|
{ tag: t.meta, color: '#0f380f' },
|
||||||
|
{ tag: t.attributeName, color: '#0f380f' },
|
||||||
|
{ tag: t.propertyName, color: '#0f380f' },
|
||||||
|
{ tag: t.className, color: '#0f380f' },
|
||||||
|
{ tag: t.invalid, color: '#0f380f' },
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
import { createTheme } from '@uiw/codemirror-themes';
|
||||||
|
export const settings = {
|
||||||
|
background: 'black',
|
||||||
|
foreground: 'white', // whats that?
|
||||||
|
caret: 'white',
|
||||||
|
selection: '#ffffff20',
|
||||||
|
selectionMatch: '#036dd626',
|
||||||
|
lineHighlight: '#ffffff10',
|
||||||
|
lineBackground: '#00000050',
|
||||||
|
gutterBackground: 'transparent',
|
||||||
|
gutterForeground: '#8a919966',
|
||||||
|
};
|
||||||
|
export default createTheme({
|
||||||
|
theme: 'dark',
|
||||||
|
settings,
|
||||||
|
styles: [
|
||||||
|
{ tag: t.keyword, color: 'white' },
|
||||||
|
{ tag: t.operator, color: 'white' },
|
||||||
|
{ tag: t.special(t.variableName), color: 'white' },
|
||||||
|
{ tag: t.typeName, color: 'white' },
|
||||||
|
{ tag: t.atom, color: 'white' },
|
||||||
|
{ tag: t.number, color: 'white' },
|
||||||
|
{ tag: t.definition(t.variableName), color: 'white' },
|
||||||
|
{ tag: t.string, color: 'white' },
|
||||||
|
{ tag: t.special(t.string), color: 'white' },
|
||||||
|
{ tag: t.comment, color: 'white' },
|
||||||
|
{ tag: t.variableName, color: 'white' },
|
||||||
|
{ tag: t.tagName, color: 'white' },
|
||||||
|
{ tag: t.bracket, color: 'white' },
|
||||||
|
{ tag: t.meta, color: 'white' },
|
||||||
|
{ tag: t.attributeName, color: 'white' },
|
||||||
|
{ tag: t.propertyName, color: 'white' },
|
||||||
|
{ tag: t.className, color: 'white' },
|
||||||
|
{ tag: t.invalid, color: 'white' },
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
import { createTheme } from '@uiw/codemirror-themes';
|
||||||
|
export const settings = {
|
||||||
|
background: '#051DB5',
|
||||||
|
lineBackground: '#051DB550',
|
||||||
|
foreground: 'white', // whats that?
|
||||||
|
caret: 'white',
|
||||||
|
selection: 'rgba(128, 203, 196, 0.5)',
|
||||||
|
selectionMatch: '#036dd626',
|
||||||
|
// lineHighlight: '#8a91991a', // original
|
||||||
|
lineHighlight: '#00000050',
|
||||||
|
gutterBackground: 'transparent',
|
||||||
|
// gutterForeground: '#8a919966',
|
||||||
|
gutterForeground: '#8a919966',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createTheme({
|
||||||
|
theme: 'dark',
|
||||||
|
settings,
|
||||||
|
styles: [
|
||||||
|
{ tag: t.keyword, color: 'white' },
|
||||||
|
{ tag: t.operator, color: 'white' },
|
||||||
|
{ tag: t.special(t.variableName), color: 'white' },
|
||||||
|
{ tag: t.typeName, color: 'white' },
|
||||||
|
{ tag: t.atom, color: 'white' },
|
||||||
|
{ tag: t.number, color: 'white' },
|
||||||
|
{ tag: t.definition(t.variableName), color: 'white' },
|
||||||
|
{ tag: t.string, color: 'white' },
|
||||||
|
{ tag: t.special(t.string), color: 'white' },
|
||||||
|
{ tag: t.comment, color: 'white' },
|
||||||
|
{ tag: t.variableName, color: 'white' },
|
||||||
|
{ tag: t.tagName, color: 'white' },
|
||||||
|
{ tag: t.bracket, color: 'white' },
|
||||||
|
{ tag: t.meta, color: 'white' },
|
||||||
|
{ tag: t.attributeName, color: 'white' },
|
||||||
|
{ tag: t.propertyName, color: 'white' },
|
||||||
|
{ tag: t.className, color: 'white' },
|
||||||
|
{ tag: t.invalid, color: 'white' },
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
import { createTheme } from '@uiw/codemirror-themes';
|
||||||
|
export const settings = {
|
||||||
|
background: 'black',
|
||||||
|
foreground: '#41FF00', // whats that?
|
||||||
|
caret: '#41FF00',
|
||||||
|
selection: '#ffffff20',
|
||||||
|
selectionMatch: '#036dd626',
|
||||||
|
lineHighlight: '#ffffff10',
|
||||||
|
gutterBackground: 'transparent',
|
||||||
|
gutterForeground: '#8a919966',
|
||||||
|
};
|
||||||
|
export default createTheme({
|
||||||
|
theme: 'dark',
|
||||||
|
settings,
|
||||||
|
styles: [
|
||||||
|
{ tag: t.keyword, color: '#41FF00' },
|
||||||
|
{ tag: t.operator, color: '#41FF00' },
|
||||||
|
{ tag: t.special(t.variableName), color: '#41FF00' },
|
||||||
|
{ tag: t.typeName, color: '#41FF00' },
|
||||||
|
{ tag: t.atom, color: '#41FF00' },
|
||||||
|
{ tag: t.number, color: '#41FF00' },
|
||||||
|
{ tag: t.definition(t.variableName), color: '#41FF00' },
|
||||||
|
{ tag: t.string, color: '#41FF00' },
|
||||||
|
{ tag: t.special(t.string), color: '#41FF00' },
|
||||||
|
{ tag: t.comment, color: '#41FF00' },
|
||||||
|
{ tag: t.variableName, color: '#41FF00' },
|
||||||
|
{ tag: t.tagName, color: '#41FF00' },
|
||||||
|
{ tag: t.bracket, color: '#41FF00' },
|
||||||
|
{ tag: t.meta, color: '#41FF00' },
|
||||||
|
{ tag: t.attributeName, color: '#41FF00' },
|
||||||
|
{ tag: t.propertyName, color: '#41FF00' },
|
||||||
|
{ tag: t.className, color: '#41FF00' },
|
||||||
|
{ tag: t.invalid, color: '#41FF00' },
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
import { createTheme } from '@uiw/codemirror-themes';
|
||||||
|
export const settings = {
|
||||||
|
background: 'white',
|
||||||
|
foreground: 'black', // whats that?
|
||||||
|
caret: 'black',
|
||||||
|
selection: 'rgba(128, 203, 196, 0.5)',
|
||||||
|
selectionMatch: '#ffffff26',
|
||||||
|
lineHighlight: '#cccccc50',
|
||||||
|
lineBackground: '#ffffff50',
|
||||||
|
gutterBackground: 'transparent',
|
||||||
|
gutterForeground: 'black',
|
||||||
|
light: true,
|
||||||
|
};
|
||||||
|
export default createTheme({
|
||||||
|
theme: 'light',
|
||||||
|
settings,
|
||||||
|
styles: [
|
||||||
|
{ tag: t.keyword, color: 'black' },
|
||||||
|
{ tag: t.operator, color: 'black' },
|
||||||
|
{ tag: t.special(t.variableName), color: 'black' },
|
||||||
|
{ tag: t.typeName, color: 'black' },
|
||||||
|
{ tag: t.atom, color: 'black' },
|
||||||
|
{ tag: t.number, color: 'black' },
|
||||||
|
{ tag: t.definition(t.variableName), color: 'black' },
|
||||||
|
{ tag: t.string, color: 'black' },
|
||||||
|
{ tag: t.special(t.string), color: 'black' },
|
||||||
|
{ tag: t.comment, color: 'black' },
|
||||||
|
{ tag: t.variableName, color: 'black' },
|
||||||
|
{ tag: t.tagName, color: 'black' },
|
||||||
|
{ tag: t.bracket, color: 'black' },
|
||||||
|
{ tag: t.meta, color: 'black' },
|
||||||
|
{ tag: t.attributeName, color: 'black' },
|
||||||
|
{ tag: t.propertyName, color: 'black' },
|
||||||
|
{ tag: t.className, color: 'black' },
|
||||||
|
{ tag: t.invalid, color: 'black' },
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -1,7 +1,21 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
colors: {
|
||||||
|
// codemirror-theme settings
|
||||||
|
background: 'var(--background)',
|
||||||
|
lineBackground: 'var(--lineBackground)',
|
||||||
|
foreground: 'var(--foreground)',
|
||||||
|
caret: 'var(--caret)',
|
||||||
|
selection: 'var(--selection)',
|
||||||
|
selectionMatch: 'var(--selectionMatch)',
|
||||||
|
gutterBackground: 'var(--gutterBackground)',
|
||||||
|
gutterForeground: 'var(--gutterForeground)',
|
||||||
|
gutterBorder: 'var(--gutterBorder)',
|
||||||
|
lineHighlight: 'var(--lineHighlight)',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
corePlugins: {
|
corePlugins: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/serial",
|
"name": "@strudel.cycles/serial",
|
||||||
"version": "0.6.0",
|
"version": "0.8.0",
|
||||||
"description": "Webserial API for strudel",
|
"description": "Webserial API for strudel",
|
||||||
"main": "serial.mjs",
|
"main": "serial.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -32,6 +32,6 @@
|
|||||||
"@strudel.cycles/core": "workspace:*"
|
"@strudel.cycles/core": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^3.2.2"
|
"vite": "^4.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { toMidi } from '@strudel.cycles/core';
|
import { noteToMidi, freqToMidi } from '@strudel.cycles/core';
|
||||||
|
import { getAudioContext, registerSound, getEnvelope } from '@strudel.cycles/webaudio';
|
||||||
|
import gm from './gm.mjs';
|
||||||
|
|
||||||
let loadCache = {};
|
let loadCache = {};
|
||||||
async function loadFont(name) {
|
async function loadFont(name) {
|
||||||
@@ -8,7 +10,6 @@ async function loadFont(name) {
|
|||||||
const load = async () => {
|
const load = async () => {
|
||||||
// TODO: make soundfont source configurable
|
// TODO: make soundfont source configurable
|
||||||
const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`;
|
const url = `https://felixroos.github.io/webaudiofontdata/sound/${name}.js`;
|
||||||
console.log('load font', name, url);
|
|
||||||
const preset = await fetch(url).then((res) => res.text());
|
const preset = await fetch(url).then((res) => res.text());
|
||||||
let [_, data] = preset.split('={');
|
let [_, data] = preset.split('={');
|
||||||
return eval('{' + data);
|
return eval('{' + data);
|
||||||
@@ -17,15 +18,24 @@ async function loadFont(name) {
|
|||||||
return loadCache[name];
|
return loadCache[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFontBufferSource(name, pitch, ac) {
|
export async function getFontBufferSource(name, value, ac) {
|
||||||
if (typeof pitch === 'string') {
|
let { note = 'c3', freq } = value;
|
||||||
pitch = toMidi(pitch);
|
let midi;
|
||||||
|
if (freq) {
|
||||||
|
midi = freqToMidi(freq);
|
||||||
|
} else if (typeof note === 'string') {
|
||||||
|
midi = noteToMidi(note);
|
||||||
|
} else if (typeof note === 'number') {
|
||||||
|
midi = note;
|
||||||
|
} else {
|
||||||
|
throw new Error(`unexpected "note" type "${typeof note}"`);
|
||||||
}
|
}
|
||||||
const { buffer, zone } = await getFontPitch(name, pitch, ac);
|
|
||||||
|
const { buffer, zone } = await getFontPitch(name, midi, ac);
|
||||||
const src = ac.createBufferSource();
|
const src = ac.createBufferSource();
|
||||||
src.buffer = buffer;
|
src.buffer = buffer;
|
||||||
const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune;
|
const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune;
|
||||||
const playbackRate = 1.0 * Math.pow(2, (100.0 * pitch - baseDetune) / 1200.0);
|
const playbackRate = 1.0 * Math.pow(2, (100.0 * midi - baseDetune) / 1200.0);
|
||||||
// src detune?
|
// src detune?
|
||||||
src.playbackRate.value = playbackRate;
|
src.playbackRate.value = playbackRate;
|
||||||
const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd;
|
const loop = zone.loopStart > 1 && zone.loopStart < zone.loopEnd;
|
||||||
@@ -114,3 +124,32 @@ async function getBuffer(zone, audioContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function registerSoundfonts() {
|
||||||
|
Object.entries(gm).forEach(([name, fonts]) => {
|
||||||
|
registerSound(
|
||||||
|
name,
|
||||||
|
async (time, value, onended) => {
|
||||||
|
const { n = 0 } = value;
|
||||||
|
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = value;
|
||||||
|
const font = fonts[n % fonts.length];
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
const bufferSource = await getFontBufferSource(font, value, ctx);
|
||||||
|
bufferSource.start(time);
|
||||||
|
const { node: envelope, stop: releaseEnvelope } = getEnvelope(attack, decay, sustain, release, 0.3, time);
|
||||||
|
bufferSource.connect(envelope);
|
||||||
|
const stop = (releaseTime) => {
|
||||||
|
bufferSource.stop(releaseTime + release);
|
||||||
|
releaseEnvelope(releaseTime);
|
||||||
|
};
|
||||||
|
bufferSource.onended = () => {
|
||||||
|
bufferSource.disconnect();
|
||||||
|
envelope.disconnect();
|
||||||
|
onended();
|
||||||
|
};
|
||||||
|
return { node: envelope, stop };
|
||||||
|
},
|
||||||
|
{ type: 'soundfont', prebake: true, fonts },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
import { getFontBufferSource } from './fontloader.mjs';
|
import { getFontBufferSource, registerSoundfonts } from './fontloader.mjs';
|
||||||
import * as soundfontList from './list.mjs';
|
import * as soundfontList from './list.mjs';
|
||||||
import { startPresetNote } from 'sfumato';
|
import { startPresetNote } from 'sfumato';
|
||||||
import { loadSoundfont } from './sfumato.mjs';
|
import { loadSoundfont } from './sfumato.mjs';
|
||||||
|
|
||||||
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList };
|
export { loadSoundfont, startPresetNote, getFontBufferSource, soundfontList, registerSoundfonts };
|
||||||
|
|||||||
+926
-665
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/soundfonts",
|
"name": "@strudel.cycles/soundfonts",
|
||||||
"version": "0.6.0",
|
"version": "0.8.2",
|
||||||
"description": "Soundsfont support for strudel",
|
"description": "Soundsfont support for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"soundfont2": "^0.4.0"
|
"soundfont2": "^0.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"node-fetch": "^3.2.6",
|
"node-fetch": "^3.3.1",
|
||||||
"vite": "^3.2.2"
|
"vite": "^4.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Pattern, getPlayableNoteValue, toMidi } from '@strudel.cycles/core';
|
import { Pattern, getPlayableNoteValue, noteToMidi } from '@strudel.cycles/core';
|
||||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
import { getAudioContext, registerSound } from '@strudel.cycles/webaudio';
|
||||||
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
||||||
|
|
||||||
Pattern.prototype.soundfont = function (sf, n = 0) {
|
Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||||
@@ -8,7 +8,7 @@ Pattern.prototype.soundfont = function (sf, n = 0) {
|
|||||||
const note = getPlayableNoteValue(h);
|
const note = getPlayableNoteValue(h);
|
||||||
const preset = sf.presets[n % sf.presets.length];
|
const preset = sf.presets[n % sf.presets.length];
|
||||||
const deadline = ctx.currentTime + t - ct;
|
const deadline = ctx.currentTime + t - ct;
|
||||||
const args = [ctx, preset, toMidi(note), deadline];
|
const args = [ctx, preset, noteToMidi(note), deadline];
|
||||||
const stop = startPresetNote(...args);
|
const stop = startPresetNote(...args);
|
||||||
stop(deadline + h.duration);
|
stop(deadline + h.duration);
|
||||||
});
|
});
|
||||||
@@ -21,5 +21,29 @@ export function loadSoundfont(url) {
|
|||||||
}
|
}
|
||||||
const sf = _loadSoundfont(url);
|
const sf = _loadSoundfont(url);
|
||||||
soundfontCache.set(url, sf);
|
soundfontCache.set(url, sf);
|
||||||
|
/*sf.then((font) => {
|
||||||
|
font.presets.forEach((preset) => {
|
||||||
|
console.log('preset', preset.header.name);
|
||||||
|
registerSound(
|
||||||
|
preset.header.name.replaceAll(' ', '_'),
|
||||||
|
(time, value, onended) => {
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
let { note } = value; // freq ?
|
||||||
|
|
||||||
|
const p = font.presets.find((p) => p.header.name === preset.header.name);
|
||||||
|
|
||||||
|
if (!p) {
|
||||||
|
throw new Error('preset not found');
|
||||||
|
}
|
||||||
|
const deadline = time; // - ctx.currentTime;
|
||||||
|
const args = [ctx, p, noteToMidi(note), deadline];
|
||||||
|
const stop = startPresetNote(...args);
|
||||||
|
return { node: undefined, stop };
|
||||||
|
},
|
||||||
|
{ type: 'soundfont' },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
//console.log('f', f);
|
||||||
|
});*/
|
||||||
return sf;
|
return sf;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/tonal",
|
"name": "@strudel.cycles/tonal",
|
||||||
"version": "0.6.0",
|
"version": "0.8.2",
|
||||||
"description": "Tonal functions for strudel",
|
"description": "Tonal functions for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -34,10 +34,10 @@
|
|||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"@tonaljs/tonal": "^4.7.2",
|
"@tonaljs/tonal": "^4.7.2",
|
||||||
"chord-voicings": "^0.0.1",
|
"chord-voicings": "^0.0.1",
|
||||||
"webmidi": "^3.0.21"
|
"webmidi": "^3.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^3.2.2",
|
"vite": "^4.3.3",
|
||||||
"vitest": "^0.25.7"
|
"vitest": "^0.28.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import '../tonal.mjs'; // need to import this to add prototypes
|
import '../tonal.mjs'; // need to import this to add prototypes
|
||||||
import { pure, controls, seq } from '@strudel.cycles/core';
|
import { pure, controls, seq } from '@strudel.cycles/core';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { mini } from '../../mini/mini.mjs';
|
||||||
const { n } = controls;
|
const { n } = controls;
|
||||||
|
|
||||||
describe('tonal', () => {
|
describe('tonal', () => {
|
||||||
@@ -30,4 +31,18 @@ describe('tonal', () => {
|
|||||||
.firstCycleValues.map((h) => h.note),
|
.firstCycleValues.map((h) => h.note),
|
||||||
).toEqual(['C3', 'D3', 'E3']);
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
});
|
});
|
||||||
|
it('scale with colon', () => {
|
||||||
|
expect(
|
||||||
|
n(0, 1, 2)
|
||||||
|
.scale('C:major')
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
|
});
|
||||||
|
it('scale with mininotation colon', () => {
|
||||||
|
expect(
|
||||||
|
n(0, 1, 2)
|
||||||
|
.scale(mini('C:major'))
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -123,28 +123,37 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
|
|||||||
/**
|
/**
|
||||||
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
||||||
*
|
*
|
||||||
* The scale name has the form "TO? N" wher
|
* A scale consists of a root note (e.g. `c4`, `c`, `f#`, `bb4`) followed by semicolon (':') and then a [scale type](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
|
||||||
*
|
*
|
||||||
* - T = Tonic
|
* The root note defaults to octave 3, if no octave number is given.
|
||||||
* - O = Octave (optional, defaults to 3)
|
|
||||||
* - N = Name of scale, available names can be found [here](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
|
|
||||||
*
|
*
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @name scale
|
* @name scale
|
||||||
* @param {string} scale Name of scale
|
* @param {string} scale Name of scale
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
|
* "0 2 4 6 4 2".scale("C2:major").note()
|
||||||
|
* @example
|
||||||
* "0 2 4 6 4 2"
|
* "0 2 4 6 4 2"
|
||||||
* .scale(seq('C2 major', 'C2 minor').slow(2))
|
* .scale("C2:<major minor>")
|
||||||
* .note()
|
* .note()
|
||||||
|
* @example
|
||||||
|
* "0 1 2 3 4 5 6 7".rev().scale("C2:<major minor>").note()
|
||||||
|
* .s("folkharp")
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const scale = register('scale', function (scale /* : string */, pat) {
|
export const scale = register('scale', function (scale, pat) {
|
||||||
|
// Supports ':' list syntax in mininotation
|
||||||
|
if (Array.isArray(scale)) {
|
||||||
|
scale = scale.flat().join(' ');
|
||||||
|
}
|
||||||
return pat.withHap((hap) => {
|
return pat.withHap((hap) => {
|
||||||
const isObject = typeof hap.value === 'object';
|
const isObject = typeof hap.value === 'object';
|
||||||
let note = isObject ? hap.value.n : hap.value;
|
let note = isObject ? hap.value.n : hap.value;
|
||||||
const asNumber = Number(note);
|
const asNumber = Number(note);
|
||||||
if (!isNaN(asNumber)) {
|
if (!isNaN(asNumber)) {
|
||||||
|
// TODO: worth keeping for supporting ':' in (non-mininotation) strings?
|
||||||
|
scale = scale.replaceAll(':', ' ');
|
||||||
let [tonic, scaleName] = Scale.tokenize(scale);
|
let [tonic, scaleName] = Scale.tokenize(scale);
|
||||||
const { pc, oct = 3 } = Note.get(tonic);
|
const { pc, oct = 3 } = Note.get(tonic);
|
||||||
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
|
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
|
||||||
|
|||||||
@@ -6,10 +6,53 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { stack, register } from '@strudel.cycles/core';
|
import { stack, register } from '@strudel.cycles/core';
|
||||||
import _voicings from 'chord-voicings';
|
import _voicings from 'chord-voicings';
|
||||||
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
|
const { dictionaryVoicing, minTopNoteDiff } = _voicings.default || _voicings; // parcel module resolution fuckup
|
||||||
|
|
||||||
|
const lefthand = {
|
||||||
|
m7: ['3m 5P 7m 9M', '7m 9M 10m 12P'],
|
||||||
|
7: ['3M 6M 7m 9M', '7m 9M 10M 13M'],
|
||||||
|
'^7': ['3M 5P 7M 9M', '7M 9M 10M 12P'],
|
||||||
|
69: ['3M 5P 6A 9M'],
|
||||||
|
m7b5: ['3m 5d 7m 8P', '7m 8P 10m 12d'],
|
||||||
|
'7b9': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
|
||||||
|
'7b13': ['3M 6m 7m 9m', '7m 9m 10M 13m'],
|
||||||
|
o7: ['1P 3m 5d 6M', '5d 6M 8P 10m'],
|
||||||
|
'7#11': ['7m 9M 11A 13A'],
|
||||||
|
'7#9': ['3M 7m 9A'],
|
||||||
|
mM7: ['3m 5P 7M 9M', '7M 9M 10m 12P'],
|
||||||
|
m6: ['3m 5P 6M 9M', '6M 9M 10m 12P'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const guidetones = {
|
||||||
|
m7: ['3m 7m', '7m 10m'],
|
||||||
|
m9: ['3m 7m', '7m 10m'],
|
||||||
|
7: ['3M 7m', '7m 10M'],
|
||||||
|
'^7': ['3M 7M', '7M 10M'],
|
||||||
|
'^9': ['3M 7M', '7M 10M'],
|
||||||
|
69: ['3M 6M'],
|
||||||
|
6: ['3M 6M', '6M 10M'],
|
||||||
|
m7b5: ['3m 7m', '7m 10m'],
|
||||||
|
'7b9': ['3M 7m', '7m 10M'],
|
||||||
|
'7b13': ['3M 7m', '7m 10M'],
|
||||||
|
o7: ['3m 6M', '6M 10m'],
|
||||||
|
'7#11': ['3M 7m', '7m 10M'],
|
||||||
|
'7#9': ['3M 7m', '7m 10M'],
|
||||||
|
mM7: ['3m 7M', '7M 10m'],
|
||||||
|
m6: ['3m 6M', '6M 10m'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const triads = {
|
||||||
|
'': ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
|
||||||
|
M: ['1P 3M 5P', '3M 5P 8P', '5P 8P 10M'],
|
||||||
|
m: ['1P 3m 5P', '3m 5P 8P', '5P 8P 10m'],
|
||||||
|
o: ['1P 3m 5d', '3m 5d 8P', '5d 8P 10m'],
|
||||||
|
aug: ['1P 3m 5A', '3m 5A 8P', '5A 8P 10m'],
|
||||||
|
};
|
||||||
|
|
||||||
export const voicingRegistry = {
|
export const voicingRegistry = {
|
||||||
lefthand: { dictionary: lefthand, range: ['F3', 'A4'] },
|
lefthand: { dictionary: lefthand, range: ['F3', 'A4'] },
|
||||||
|
triads: { dictionary: triads },
|
||||||
|
guidetones: { dictionary: guidetones },
|
||||||
};
|
};
|
||||||
export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
|
export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/tone",
|
"name": "@strudel.cycles/tone",
|
||||||
"version": "0.6.0",
|
"version": "0.8.0",
|
||||||
"description": "Tone.js API for strudel",
|
"description": "Tone.js API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"tone": "^14.7.77"
|
"tone": "^14.7.77"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^3.2.2",
|
"vite": "^4.3.3",
|
||||||
"vitest": "^0.25.7"
|
"vitest": "^0.28.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/transpiler",
|
"name": "@strudel.cycles/transpiler",
|
||||||
"version": "0.6.0",
|
"version": "0.8.2",
|
||||||
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"estree-walker": "^3.0.1"
|
"estree-walker": "^3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^3.2.2",
|
"vite": "^4.3.3",
|
||||||
"vitest": "^0.25.8"
|
"vitest": "^0.28.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# @strudel/web
|
||||||
|
|
||||||
|
This package provides an easy to use bundle of multiple strudel packages for the web.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Save this code as a `.html` file and double click it:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<button id="play">play</button>
|
||||||
|
<button id="stop">stop</button>
|
||||||
|
<script type="module">
|
||||||
|
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
|
||||||
|
|
||||||
|
initStrudel();
|
||||||
|
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').play());
|
||||||
|
document.getElementById('stop').addEventListener('click', () => hush());
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
With the help of [skypack](https://www.skypack.dev/), you don't need a bundler nor a server.
|
||||||
|
|
||||||
|
As soon as you call `initStrudel()`, all strudel functions are made available.
|
||||||
|
In this case, we are using the `note` function to create a pattern.
|
||||||
|
To actually play the pattern, you have to append `.play()` to the end.
|
||||||
|
|
||||||
|
Note: Due to the [Autoplay policy](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Best_practices#autoplay_policy), you can only play audio in a browser after a click event.
|
||||||
|
|
||||||
|
### Via npm
|
||||||
|
|
||||||
|
If you're using a bundler, you can install the package via `npm i @strudel/web`, then just import it like:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { initStrudel } from '@strudel/web';
|
||||||
|
```
|
||||||
|
|
||||||
|
The rest of the code should be the same. Check out [vite](https://vitejs.dev/) for a good bundler / dev server.
|
||||||
|
|
||||||
|
### Loading samples
|
||||||
|
|
||||||
|
By default, no external samples are loaded, but you can add them like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
initStrudel({
|
||||||
|
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('play').addEventListener('click',
|
||||||
|
() => s("bd sd").play()
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can learn [more about the `samples` function here](https://strudel.tidalcycles.org/learn/samples#loading-custom-samples).
|
||||||
|
|
||||||
|
### Evaluating Code
|
||||||
|
|
||||||
|
Instead of creating patterns directly in JS, you might also want to take in user input and turn that into a pattern.
|
||||||
|
This is called evaluation: Taking a piece of code and executing it on the fly.
|
||||||
|
|
||||||
|
To do that, you can use the `evaluate` function:
|
||||||
|
|
||||||
|
```js
|
||||||
|
initStrudel();
|
||||||
|
document.getElementById('play').addEventListener('click',
|
||||||
|
() => evaluate('note("c a f e").jux(rev)')
|
||||||
|
);
|
||||||
|
document.getElementById('play').addEventListener('stop',
|
||||||
|
() => hush()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Double vs Single Quotes
|
||||||
|
|
||||||
|
There is a tiny difference between the [Strudel REPL](https://strudel.tidalcycles.org/) and `@strudel/web`.
|
||||||
|
In the REPL you can use 'single quotes' for regular JS strings and "double quotes" for mini notation patterns.
|
||||||
|
In `@strudel/web`, it does not matter which types of quotes you're using.
|
||||||
|
There will probably be an escapte hatch for that in the future.
|
||||||
|
|
||||||
|
## More Examples
|
||||||
|
|
||||||
|
Check out the examples folder for more examples, both using plain html and vite!
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<button id="play">play</button>
|
||||||
|
<button id="stop">stop</button>
|
||||||
|
<script type="module">
|
||||||
|
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
|
||||||
|
|
||||||
|
initStrudel();
|
||||||
|
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
|
||||||
|
document.getElementById('play').addEventListener('stop', () => hush());
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<button id="a">A</button>
|
||||||
|
<button id="b">B</button>
|
||||||
|
<button id="c">C</button>
|
||||||
|
<button id="stop">stop</button>
|
||||||
|
<script type="module">
|
||||||
|
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
|
||||||
|
initStrudel({
|
||||||
|
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
|
||||||
|
});
|
||||||
|
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
|
||||||
|
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
|
||||||
|
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
|
||||||
|
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
|
||||||
|
click('stop', () => hush());
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<button id="play">play</button>
|
||||||
|
<button id="stop">stop</button>
|
||||||
|
<script type="module">
|
||||||
|
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
|
||||||
|
|
||||||
|
initStrudel();
|
||||||
|
document.getElementById('play').addEventListener('click', () => note('<c a f e>(3,8)').play());
|
||||||
|
document.getElementById('stop').addEventListener('click', () => hush());
|
||||||
|
</script>
|
||||||
@@ -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?
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="https://strudel.tidalcycles.org/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>@strudel/web REPL Example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<button id="a">A</button>
|
||||||
|
<button id="b">B</button>
|
||||||
|
<button id="c">C</button>
|
||||||
|
<button id="stop">stop</button>
|
||||||
|
<script type="module">
|
||||||
|
import { initStrudel } from '@strudel/web';
|
||||||
|
initStrudel({
|
||||||
|
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
|
||||||
|
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
|
||||||
|
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
|
||||||
|
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
|
||||||
|
click('stop', () => hush());
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "repl-example",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^4.3.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@strudel/web": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<button id="play">play</button>
|
||||||
|
<button id="stop">stop</button>
|
||||||
|
<script type="module">
|
||||||
|
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
|
||||||
|
|
||||||
|
initStrudel({
|
||||||
|
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
|
||||||
|
});
|
||||||
|
document.getElementById('play').addEventListener('click', () => s('[bd sd](3,8)').play());
|
||||||
|
document.getElementById('stop').addEventListener('click', () => hush());
|
||||||
|
</script>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user