mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 22:35:15 -04:00
Compare commits
511 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57c3c21334 | |||
| 957427fd33 | |||
| 4cc453c640 | |||
| 74e27ca94f | |||
| 7dfd7dbcdc | |||
| 2cba1dcb31 | |||
| 48a6a12648 | |||
| 6bbf7cbe32 | |||
| 9f36b73081 | |||
| a7e485992e | |||
| ab74ccfe7c | |||
| 92b2013cf6 | |||
| 2e97fb7be2 | |||
| df1934f87b | |||
| d3472feb5c | |||
| addb6db1c7 | |||
| 566d590453 | |||
| e57bd98da0 | |||
| f5c4373f99 | |||
| e762291bb7 | |||
| 9da027d377 | |||
| 96dccdc38b | |||
| d6254294b0 | |||
| 339c1bb573 | |||
| 9782795761 | |||
| 2d2b238da9 | |||
| f0839e02eb | |||
| a9f7d825c8 | |||
| 709b654ed3 | |||
| ed00686804 | |||
| e10c469749 | |||
| d81a214c74 | |||
| e57f240e70 | |||
| e18296e760 | |||
| c145355ccc | |||
| 352a3c39d7 | |||
| a46617f3f0 | |||
| d164b5e7f5 | |||
| c0bf86d11e | |||
| 6f06683d70 | |||
| 637d714ab2 | |||
| c42d00f048 | |||
| 702e558d1a | |||
| bcc2e8f759 | |||
| 27339715b7 | |||
| 5e2e6411e5 | |||
| 0fac16e769 | |||
| d0ce82e3cd | |||
| e82629de2e | |||
| 38ce593c60 | |||
| 8a91549a7b | |||
| cc27d482cd | |||
| 37e640470b | |||
| 5e5e7730c1 | |||
| 89e7d52308 | |||
| 99bb227cf4 | |||
| 70e776e799 | |||
| 66aad0004a | |||
| a968545458 | |||
| 5414bbe85d | |||
| e7e80bfd83 | |||
| c57f5bb429 | |||
| f2608e712b | |||
| 34b41fc26f | |||
| f6e171fed8 | |||
| 2511dcc09e | |||
| 798eb22d9a | |||
| 977420e74d | |||
| 9c108862ef | |||
| 2fbdc9fdd7 | |||
| 0a96fa5896 | |||
| 3e0e903d38 | |||
| 591c3fe08f | |||
| 56b1ea3fa5 | |||
| c199b51645 | |||
| 8d2a368da9 | |||
| 052d09e892 | |||
| 439a7dc5e6 | |||
| 06ebcbff8c | |||
| 4e2e790864 | |||
| 4f3b40bd7b | |||
| 142160d79a | |||
| ff2c59a2a6 | |||
| 9b9176325b | |||
| b8e1b8d7ac | |||
| e84c39837f | |||
| 6527c68cda | |||
| 6a09f54b25 | |||
| dc6b766ab7 | |||
| 9eb3c9410d | |||
| 189e650a73 | |||
| a5886bb9d4 | |||
| 877bc95a58 | |||
| 18b1739b89 | |||
| 022ac95bbd | |||
| b3e25cfc52 | |||
| 3d25aa1b91 | |||
| b54139376e | |||
| d64a0ef0eb | |||
| 659071a4ee | |||
| 64f7bc4442 | |||
| 8c97f2e2d0 | |||
| b8c98736c5 | |||
| b8c46d6b26 | |||
| 79453ac2c3 | |||
| b08890bd9e | |||
| 938c92fda2 | |||
| a0fc52b1ec | |||
| 9a9fe83f9c | |||
| 0693a32736 | |||
| 42a903ecc0 | |||
| da283eb55a | |||
| b12707316a | |||
| 66aa3ac1da | |||
| bf5d9917ab | |||
| 4f15d681b0 | |||
| d81b1ab65c | |||
| 8465517c76 | |||
| 215ab87809 | |||
| e5fe998327 | |||
| 6f75a0a62b | |||
| 0464845262 | |||
| e490774294 | |||
| 34e8a57472 | |||
| 59c8d70714 | |||
| ce7cff2c3b | |||
| 32fe73aba2 | |||
| d6ee10e05c | |||
| 36441e7b73 | |||
| 189daa3942 | |||
| d009b9592e | |||
| f7f1bd63e8 | |||
| 7d24e45692 | |||
| cf55d4c8d1 | |||
| f17a4d045f | |||
| e7e636886d | |||
| 8e1e83f3cf | |||
| 2d64b538e7 | |||
| b717615558 | |||
| e9aef9e4d4 | |||
| 8c4f10464b | |||
| 018649c4f3 | |||
| 5ff59a878d | |||
| a20468b85f | |||
| 9648e35de2 | |||
| cd344cefe0 | |||
| 1f6a313f02 | |||
| 5cbeb227d8 | |||
| c8c6a2ce08 | |||
| 2ab2cc5fc8 | |||
| 3dd667e825 | |||
| 482ebb695a | |||
| 218952b8b6 | |||
| 2e8a8b50df | |||
| 612efad3eb | |||
| e7839a09a1 | |||
| 3292f88810 | |||
| a4c040e101 | |||
| 7046c1f546 | |||
| a4ab8e1b06 | |||
| f22f89347e | |||
| de15d79edf | |||
| 23e5358ae4 | |||
| 3b6777e259 | |||
| ed5bf3b3fc | |||
| b089328da3 | |||
| 784c711870 | |||
| 34b10b6c84 | |||
| 450f9685f3 | |||
| 03090e399c | |||
| ed58d26da6 | |||
| e3605590e5 | |||
| d55ef8e843 | |||
| efc2333360 | |||
| 92626e7c20 | |||
| ad113b3888 | |||
| 39c45bc2b2 | |||
| 899af88bd9 | |||
| c2b8cc4b82 | |||
| 40ed7b5382 | |||
| 5d829419ba | |||
| e26da854c0 | |||
| a28b7a2d5a | |||
| 83c776d70f | |||
| 051efdc13b | |||
| ef3cd56b0d | |||
| 9017085fa3 | |||
| cdbbe079db | |||
| d6045c943b | |||
| 6ee9dea2ff | |||
| 03cbe4b6d4 | |||
| 784ee576da | |||
| ee3e1217b4 | |||
| 30c80119e9 | |||
| 973ae4c7da | |||
| 4d13b981f1 | |||
| 7691403fd0 | |||
| 3b6e3624be | |||
| 51f0bab1f8 | |||
| dae1560c40 | |||
| 82f0dbb750 | |||
| fb7d76a2ab | |||
| 26225e4ea7 | |||
| ab12c6ae0a | |||
| 4a894b0be2 | |||
| 4ad305550e | |||
| cf5c502a05 | |||
| 2415210be6 | |||
| fe18aa770d | |||
| 2eda047108 | |||
| c8cf1dc712 | |||
| e136e95375 | |||
| a26559fd3d | |||
| 004c0baa59 | |||
| 5f44ae9c4b | |||
| 66828e17d3 | |||
| b2f90ecb3b | |||
| ccf2a6de52 | |||
| 9416f317d4 | |||
| 75b6f59212 | |||
| 84238c8999 | |||
| 2d855cf5e8 | |||
| dd04d49c75 | |||
| 34874862ab | |||
| 5448fc6077 | |||
| 06fafbd8eb | |||
| 10156f0067 | |||
| 5632d83afb | |||
| a32473d570 | |||
| 0361bc629f | |||
| 1fdd87a4f3 | |||
| 16e9aca222 | |||
| 4a3d5ca44f | |||
| 6c0e8a0833 | |||
| 7336ff0ded | |||
| 735f57e276 | |||
| eb4f1f69f6 | |||
| e7e321fe03 | |||
| bf3de368f0 | |||
| d645cc6fe5 | |||
| 6fe11be218 | |||
| cf7fc23981 | |||
| bfdfab0a00 | |||
| edb347e85a | |||
| 63fcce79e0 | |||
| 8defdc424c | |||
| 16b2fc0632 | |||
| d0a544d7a7 | |||
| 9b3c6b9f94 | |||
| 6327e17feb | |||
| 15fe2734e9 | |||
| 1dd1ce254f | |||
| 3ea91adfd6 | |||
| ce5f425142 | |||
| 671a22fc22 | |||
| 976955dae3 | |||
| a6ad58a375 | |||
| 681eed3d0e | |||
| 81a9811803 | |||
| 72b582c605 | |||
| 2f0891d4e5 | |||
| a7a283a99e | |||
| d9e44dcda6 | |||
| e87bb4376f | |||
| 0722cf7ded | |||
| 37fde8f3e8 | |||
| acd5661106 | |||
| b0b2b6a120 | |||
| 664d76cf1b | |||
| a901e2e387 | |||
| 82b14b5292 | |||
| 05bcd5e32f | |||
| 077556e7cb | |||
| 4cc2de9b7e | |||
| 8655c5b73c | |||
| 31c1450edc | |||
| cbb4207bab | |||
| b0c0a393a1 | |||
| 168269a839 | |||
| 34771f03d4 | |||
| 504fe9ed40 | |||
| caf6686d01 | |||
| c063fb6b1c | |||
| e3680b96de | |||
| 688e275e0c | |||
| 2e33569786 | |||
| 46fe6d3c4d | |||
| 9220b95677 | |||
| 5b27b19972 | |||
| 5a72ea94b1 | |||
| eac6f27ca4 | |||
| f2330a2307 | |||
| db413881db | |||
| 77bab433e0 | |||
| f8cc1c2022 | |||
| e0bf9ea4d6 | |||
| 374661eb23 | |||
| df06248c54 | |||
| 27073d6c17 | |||
| f44caf9096 | |||
| ff5b11f5ed | |||
| 719c70a598 | |||
| 7a53f6c021 | |||
| 09d05abd70 | |||
| 3acc364a03 | |||
| 2dd528134a | |||
| fef50e654c | |||
| b1aba294ad | |||
| 63ac80eb79 | |||
| 82e3f9b837 | |||
| e19fd24447 | |||
| fe9a91d1e4 | |||
| 860bb58d61 | |||
| d58b3e490b | |||
| 0003ce174e | |||
| 8e0f23e081 | |||
| 71e60ed8de | |||
| e2a29914c7 | |||
| a9a3d491a2 | |||
| a750764f88 | |||
| 5b397009ab | |||
| e68df9e789 | |||
| 4d3bbd5057 | |||
| 15f3321b04 | |||
| 67bfedb317 | |||
| c0184dd827 | |||
| 09fdc45ff2 | |||
| c01b4c651c | |||
| 634eed1d82 | |||
| a1d181d609 | |||
| 440f1cb081 | |||
| 95193b0bfd | |||
| 1cc15c7e8d | |||
| 10ac7c353c | |||
| 9412bf60bf | |||
| c751bb7e5f | |||
| 28cd7c1681 | |||
| 5efcd14f40 | |||
| 8051e56d13 | |||
| 239987af0b | |||
| 8363ba5a41 | |||
| 30f7127a92 | |||
| 14193abe03 | |||
| 3caf08e199 | |||
| 5cbd38a3e8 | |||
| bbd7ed0f27 | |||
| 43504d16b6 | |||
| 1b7fbecf50 | |||
| 7e8206dcbc | |||
| 3963b807eb | |||
| e99adffc56 | |||
| c9f494d865 | |||
| 117cf7e18a | |||
| e8254735bb | |||
| 3d30007756 | |||
| aeaed50446 | |||
| ae15bb7274 | |||
| 3ac0fdf619 | |||
| d1b6b6be85 | |||
| 1081fc69e9 | |||
| 73ef43dd2c | |||
| 181e51ef0e | |||
| 1167fdb910 | |||
| ec9109dd89 | |||
| 55aef18851 | |||
| 356d4360ec | |||
| 0f2aa9569b | |||
| ac7ee376e5 | |||
| ab052ce1ac | |||
| 5988e3d3b4 | |||
| 2f2bf5bf29 | |||
| b000f2297d | |||
| 77463a199a | |||
| e54449f5f7 | |||
| d743570546 | |||
| 1dac69635d | |||
| 49f38aeddb | |||
| 43c05cb504 | |||
| ccc90d547c | |||
| 998a010247 | |||
| e4cd5dc552 | |||
| 6cf0b6f651 | |||
| 794bf86904 | |||
| 9ed690ce45 | |||
| 623bf93368 | |||
| 0cc18a6468 | |||
| 87598e10d1 | |||
| ccec7e725a | |||
| f0e6c5483c | |||
| 2cbd35f6d2 | |||
| 930dabe4d6 | |||
| ff4b7ca171 | |||
| 5129fa6677 | |||
| 5336569c25 | |||
| 483843bd0a | |||
| 1b3b07842e | |||
| 98efd72ab4 | |||
| 1eb5f6a995 | |||
| 9b10dc8535 | |||
| 2951a60fac | |||
| 94257e81ee | |||
| 006ce9d1da | |||
| 607a6121bc | |||
| 4208c79c09 | |||
| 7565354274 | |||
| ffe831d24b | |||
| 60dd8bd763 | |||
| 29e5833c8c | |||
| e3a6444f41 | |||
| 30fd4c5dec | |||
| 2da03ab46f | |||
| 243adda3dd | |||
| 7fc21d55d7 | |||
| d5ef686fe0 | |||
| d4733a30ab | |||
| 989bd0990e | |||
| 60d09adb44 | |||
| b2588e8c93 | |||
| 39d2784441 | |||
| aa70246afc | |||
| f6ffdd6ae6 | |||
| 077aac1888 | |||
| 94a3a60e49 | |||
| f2372e7a41 | |||
| a0fb8fb37f | |||
| b4027fd92a | |||
| c9cafa37fd | |||
| 2e017e46f9 | |||
| c9f58220a3 | |||
| e9de45993e | |||
| 6b2b8b5124 | |||
| 64a6dacc1e | |||
| 1738e4d538 | |||
| abc7a1e48d | |||
| 7716fdb98e | |||
| 2938bfd88f | |||
| 222e479e30 | |||
| 92b5b6538f | |||
| 3a19e23473 | |||
| dd5743ab8c | |||
| 51e3aa257c | |||
| 5eabcf0618 | |||
| 6d21456423 | |||
| bca16cdf99 | |||
| 492271d786 | |||
| f71db4aeed | |||
| 5d4ef46ac7 | |||
| bd93e44154 | |||
| 6fd89ddee7 | |||
| cf1f4ec35c | |||
| 12bb82d295 | |||
| c33cfc78c5 | |||
| ac6472a43a | |||
| 989fdfa20b | |||
| 05ae31838e | |||
| c691916cbf | |||
| e16295623d | |||
| 9626f3cc9a | |||
| 432e8dcccc | |||
| d2e93a99f1 | |||
| 3505732afa | |||
| 55a5f1d62a | |||
| 3657e2fd65 | |||
| 65a7b3030e | |||
| 208706fb52 | |||
| e04f250adb | |||
| 642ddcdb59 | |||
| 88ca54461e | |||
| 129fd7d6b0 | |||
| 7db52b3dc2 | |||
| 40f6489111 | |||
| 118b619b74 | |||
| 2dd445c102 | |||
| 3c3832dbce | |||
| 9b8761bc45 | |||
| 77ade0758e | |||
| 902012759a | |||
| ea61627963 | |||
| 95526ac99c | |||
| b0da353115 | |||
| fae59a6bcd | |||
| 11196fb1e6 | |||
| e782dc09dd | |||
| e3df504423 | |||
| b71b1354c3 | |||
| dbe3915368 | |||
| 62705da3de | |||
| 81ca199a96 | |||
| bee8c81254 | |||
| 8ad48e1633 | |||
| 4a181d5b33 | |||
| d5ab6b7211 | |||
| 4ba9d47bff | |||
| c6c5a3035c | |||
| e024ae2142 | |||
| 19df19375d | |||
| cba96049a3 | |||
| 1509ff1793 | |||
| 6a33032da7 | |||
| 6c05a4eb7b | |||
| 8d8b8ab99e | |||
| a4f10d5339 | |||
| 16472b59dd | |||
| 33a73c37bc | |||
| 4e6d39666a | |||
| a5b6022506 | |||
| 7df663e87d | |||
| 3159f66560 | |||
| 0d900c0134 | |||
| 66a27a609c | |||
| 496d60dc1c |
@@ -1,6 +1,6 @@
|
||||
name: Strudel tests
|
||||
|
||||
on: [push]
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache: "pnpm"
|
||||
- run: pnpm install
|
||||
- run: pnpm run format-check
|
||||
- run: pnpm run lint
|
||||
|
||||
+77
-1
@@ -17,7 +17,7 @@ git remote set-url origin git@codeberg.org:uzu/strudel.git
|
||||
|
||||
To get in touch with the contributors, either
|
||||
|
||||
- [join the Tidal Discord Channel](https://discord.gg/remJ6gQA) and go to the #strudel channel
|
||||
- [join the Tidal Discord Channel](https://discord.com/invite/HGEdXmRkzT) and go to the #strudel channel
|
||||
- Find related discussions on the [tidal club forum](https://club.tidalcycles.org/)
|
||||
|
||||
## Ask a Question
|
||||
@@ -150,6 +150,7 @@ Important: Always publish with `pnpm`, as `npm` does not support overriding main
|
||||
|
||||
|
||||
## useful commands
|
||||
|
||||
```sh
|
||||
#regenerate the test snapshots (ex: when updating or creating new pattern functions)
|
||||
pnpm snapshot
|
||||
@@ -160,6 +161,81 @@ pnpm run osc
|
||||
#build the standalone version
|
||||
pnpm tauri build
|
||||
```
|
||||
|
||||
## version tag patching
|
||||
|
||||
here's a little guide on how to patch patterns in the database to prevent breaking old patterns due to breaking changes in newer versions.
|
||||
|
||||
the general tactic is to use `// @version x.y` to tag a pattern with a specific strudel version. when a pattern is evaluated, this metadata will de-activate any breaking changes that came after the specified version.
|
||||
for example, in version 1.1, the default value for `fanchor` was changed from `0.5` to `0`.
|
||||
if play a pattern that was made before that change, sounds that use filter evenlopes can sound very different, so by adding `// @version 1.0` will make it sound like it used to.
|
||||
before releasing a new version with breaking changes, we can edit all patterns in the database, inserting the version tag they were created under:
|
||||
|
||||
as an example, to release version 1.2, do the following:
|
||||
|
||||
1. get date range
|
||||
|
||||
```sh
|
||||
# get date of last version:
|
||||
git log -1 --format=%aI @strudel/core@1.1.0
|
||||
# 2024-05-31T23:07:26+02:00
|
||||
|
||||
# get date of current version:
|
||||
git log -1 --format=%aI @strudel/core@1.2.0
|
||||
# 2025-05-01T12:39:24+02:00
|
||||
# might also use todays timestamp if version is not yet released
|
||||
```
|
||||
|
||||
now we know, all patterns between these 2 dates have to receive a version tag (unless they already have one).
|
||||
|
||||
2. get patterns in question
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM code_v1
|
||||
WHERE code NOT LIKE '%@version%'
|
||||
AND created_at > '2024-05-31T23:07:26+02:00'
|
||||
AND created_at < '2025-05-01T12:39:24+02:00'
|
||||
ORDER BY created_at ASC;
|
||||
```
|
||||
|
||||
this gives us all unversioned patterns that were saved between 1.1.0 and 1.2.0. in this case, it's 9373 patterns!
|
||||
|
||||
3. insert version tags
|
||||
|
||||
we are now ready to insert the version tag to these patterns.
|
||||
before updating thousands of patterns, it's probably a good idea to test if a single one gets udpated:
|
||||
|
||||
```sql
|
||||
UPDATE code_v1
|
||||
SET code = code || E'\n// @version 1.1'
|
||||
WHERE hash = 'Ns2sMB40yIw4';
|
||||
```
|
||||
|
||||
after [verifying](https://strudel.cc/?Ns2sMB40yIw4) that the version tag has been added, let's insert it everywhere:
|
||||
|
||||
```sql
|
||||
UPDATE code_v1
|
||||
SET code = code || E'\n// @version 1.1'
|
||||
WHERE code NOT LIKE '%@version%'
|
||||
AND created_at > '2024-05-31T23:07:26+02:00'
|
||||
AND created_at < '2025-05-01T12:39:24+02:00'
|
||||
```
|
||||
|
||||
4. verify
|
||||
|
||||
we can verify that the edits worked by querying all patterns that contain the new version tag:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM code_v1
|
||||
WHERE code LIKE '%@version 1.1%'
|
||||
AND created_at > '2024-05-31T23:07:26+02:00'
|
||||
AND created_at < '2025-05-01T12:39:24+02:00'
|
||||
ORDER BY created_at ASC;
|
||||
```
|
||||
|
||||
|
||||
## Have Fun
|
||||
|
||||
Remember to have fun, and that this project is driven by the passion of volunteers!
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
FROM node:24
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm install pnpm --global
|
||||
|
||||
COPY pnpm-workspace.yaml ./
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY packages/ ./packages/
|
||||
COPY examples/ ./examples/
|
||||
RUN mkdir -p website/public
|
||||
COPY website/package.json ./website/
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 4321
|
||||
|
||||
CMD ["pnpm", "dev"]
|
||||
@@ -3,8 +3,6 @@
|
||||
Live coding patterns on the web
|
||||
https://strudel.cc/
|
||||
|
||||
Development is moving to https://codeberg.org/uzu/strudel
|
||||
|
||||
- Try it here: <https://strudel.cc>
|
||||
- Docs: <https://strudel.cc/learn>
|
||||
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
||||
@@ -32,19 +30,13 @@ This project is organized into many [packages](./packages), which are also avail
|
||||
|
||||
Read more about how to use these in your own project [here](https://strudel.cc/technical-manual/project-start).
|
||||
|
||||
You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE.md). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details.
|
||||
You will need to abide by the terms of the [GNU Affero Public Licence v3](LICENSE). As such, Strudel code can only be shared within free/open source projects under the same license -- see the license for details.
|
||||
|
||||
Licensing info for the default sound banks can be found over on the [dough-samples](https://github.com/felixroos/dough-samples/blob/main/README.md) repository.
|
||||
|
||||
## Contributing
|
||||
|
||||
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
|
||||
|
||||
<a href="https://codeberg.org/uzu/strudel/activity/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=tidalcycles/strudel" />
|
||||
</a>
|
||||
|
||||
Made with [contrib.rocks](https://contrib.rocks).
|
||||
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md). You can find the full list of contributors [here](https://codeberg.org/uzu/strudel/activity/contributors).
|
||||
|
||||
## Community
|
||||
|
||||
@@ -53,3 +45,5 @@ There is a #strudel channel on the TidalCycles discord: <https://discord.com/inv
|
||||
You can also ask questions and find related discussions on the tidal club forum: <https://club.tidalcycles.org/>
|
||||
|
||||
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
|
||||
|
||||
We also have a mastodon account: <a rel="me" href="https://social.toplap.org/@strudel">social.toplap.org/@strudel</a>
|
||||
|
||||
@@ -42,6 +42,7 @@ export default [
|
||||
'**/hydra.mjs',
|
||||
'**/jsdoc-synonyms.js',
|
||||
'packages/hs2js/src/hs2js.mjs',
|
||||
'packages/supradough/dough-export.mjs',
|
||||
'**/samples',
|
||||
],
|
||||
},
|
||||
@@ -83,4 +84,14 @@ export default [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Properties provided by AudioWorkletGlobalScope
|
||||
files: ['packages/superdough/worklets.mjs'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
currentTime: 'readonly',
|
||||
sampleRate: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
jsdoc-synonyms.js - Add support for @synonym tag
|
||||
jsdoc-synonyms.js - Add support for @synonyms tag
|
||||
Copyright (C) 2023 Strudel contributors - see <https://codeberg.org/uzu/strudel/activity/contributors>
|
||||
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/>.
|
||||
*/
|
||||
|
||||
@@ -1,68 +1,122 @@
|
||||
import jsdoc from '../../doc.json';
|
||||
// import { javascriptLanguage } from '@codemirror/lang-javascript';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { h } from './html';
|
||||
|
||||
function plaintext(str) {
|
||||
const escapeHtml = (str) => {
|
||||
const div = document.createElement('div');
|
||||
div.innerText = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
};
|
||||
|
||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||
const getInnerText = (html) => {
|
||||
var div = document.createElement('div');
|
||||
const stripHtml = (html) => {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
return div.textContent || div.innerText || '';
|
||||
};
|
||||
|
||||
export function Autocomplete({ doc, label }) {
|
||||
return h`<div class="prose dark:prose-invert max-h-[400px] overflow-auto p-2">
|
||||
<h1 class="pt-0 mt-0">${label || getDocLabel(doc)}</h1>
|
||||
${doc.description}
|
||||
<ul>
|
||||
${doc.params?.map(
|
||||
({ name, type, description }) =>
|
||||
`<li>${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}</li>`,
|
||||
)}
|
||||
</ul>
|
||||
<div>
|
||||
${doc.examples?.map((example) => `<div><pre>${plaintext(example)}</pre></div>`)}
|
||||
</div>
|
||||
</div>`[0];
|
||||
/*
|
||||
<pre
|
||||
className="cursor-pointer"
|
||||
onMouseDown={(e) => {
|
||||
console.log('ola!');
|
||||
navigator.clipboard.writeText(example);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{example}
|
||||
</pre>
|
||||
*/
|
||||
}
|
||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||
|
||||
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: () => Autocomplete({ doc }),
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
}));
|
||||
const buildParamsList = (params) =>
|
||||
params?.length
|
||||
? `
|
||||
<div class="autocomplete-info-params-section">
|
||||
<h4 class="autocomplete-info-section-title">Parameters</h4>
|
||||
<ul class="autocomplete-info-params-list">
|
||||
${params
|
||||
.map(
|
||||
({ name, type, description }) => `
|
||||
<li class="autocomplete-info-param-item">
|
||||
<span class="autocomplete-info-param-name">${name}</span>
|
||||
<span class="autocomplete-info-param-type">${type.names?.join(' | ')}</span>
|
||||
${description ? `<div class="autocomplete-info-param-desc">${stripHtml(description)}</div>` : ''}
|
||||
</li>
|
||||
`,
|
||||
)
|
||||
.join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`
|
||||
: '';
|
||||
|
||||
const buildExamples = (examples) =>
|
||||
examples?.length
|
||||
? `
|
||||
<div class="autocomplete-info-examples-section">
|
||||
<h4 class="autocomplete-info-section-title">Examples</h4>
|
||||
${examples
|
||||
.map(
|
||||
(example) => `
|
||||
<pre class="autocomplete-info-example-code">${escapeHtml(example)}</pre>
|
||||
`,
|
||||
)
|
||||
.join('')}
|
||||
</div>
|
||||
`
|
||||
: '';
|
||||
|
||||
export const Autocomplete = (doc) =>
|
||||
h`
|
||||
<div class="autocomplete-info-container">
|
||||
<div class="autocomplete-info-tooltip">
|
||||
<h3 class="autocomplete-info-function-name">${getDocLabel(doc)}</h3>
|
||||
${doc.synonyms_text ? `<div class="autocomplete-info-function-synonyms">Synonyms: ${doc.synonyms_text}</div>` : ''}
|
||||
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
|
||||
${buildParamsList(doc.params)}
|
||||
${buildExamples(doc.examples)}
|
||||
</div>
|
||||
</div>
|
||||
`[0];
|
||||
|
||||
const isValidDoc = (doc) => {
|
||||
const label = getDocLabel(doc);
|
||||
return label && !label.startsWith('_') && !['package'].includes(doc.kind);
|
||||
};
|
||||
|
||||
const hasExcludedTags = (doc) =>
|
||||
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
||||
|
||||
export const getSynonymDoc = (doc, synonym) => {
|
||||
const synonyms = doc.synonyms || [];
|
||||
const docLabel = getDocLabel(doc);
|
||||
// Swap `doc.name` in for `s` in the list of synonyms
|
||||
const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
|
||||
return {
|
||||
...doc,
|
||||
name: synonym,
|
||||
longname: synonym,
|
||||
synonyms: synonymsWithDoc,
|
||||
synonyms_text: synonymsWithDoc.join(', '),
|
||||
};
|
||||
};
|
||||
|
||||
const jsdocCompletions = (() => {
|
||||
const seen = new Set(); // avoid repetition
|
||||
const completions = [];
|
||||
for (const doc of jsdoc.docs) {
|
||||
if (!isValidDoc(doc) || hasExcludedTags(doc)) continue;
|
||||
const docLabel = getDocLabel(doc);
|
||||
// Remove duplicates
|
||||
const synonyms = doc.synonyms || [];
|
||||
let labels = [docLabel, ...synonyms];
|
||||
for (const label of labels) {
|
||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
||||
if (label && !seen.has(label)) {
|
||||
seen.add(label);
|
||||
completions.push({
|
||||
label,
|
||||
info: () => Autocomplete(getSynonymDoc(doc, label)),
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return completions;
|
||||
})();
|
||||
|
||||
export const strudelAutocomplete = (context) => {
|
||||
const word = context.matchBefore(/\w*/);
|
||||
if (word.from === word.to && !context.explicit) return null;
|
||||
|
||||
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,
|
||||
@@ -74,11 +128,5 @@ export const strudelAutocomplete = (context /* : CompletionContext */) => {
|
||||
};
|
||||
};
|
||||
|
||||
export function isAutoCompletionEnabled(on) {
|
||||
return on
|
||||
? [
|
||||
autocompletion({ override: [strudelAutocomplete] }),
|
||||
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
|
||||
]
|
||||
: []; // autocompletion({ override: [] })
|
||||
}
|
||||
export const isAutoCompletionEnabled = (enabled) =>
|
||||
enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : [];
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
keymap,
|
||||
highlightSpecialChars,
|
||||
drawSelection,
|
||||
highlightActiveLine,
|
||||
dropCursor,
|
||||
rectangularSelection,
|
||||
crosshairCursor,
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter,
|
||||
} from '@codemirror/view';
|
||||
import {
|
||||
defaultHighlightStyle,
|
||||
syntaxHighlighting,
|
||||
bracketMatching,
|
||||
foldGutter,
|
||||
foldKeymap,
|
||||
} from '@codemirror/language';
|
||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete';
|
||||
|
||||
// Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
|
||||
|
||||
export const basicSetup = (() => [
|
||||
// lineNumbers(),
|
||||
// highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
// foldGutter(),
|
||||
// drawSelection(),
|
||||
dropCursor(),
|
||||
// EditorState.allowMultipleSelections.of(true),
|
||||
// indentOnInput(),
|
||||
// syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
// autocompletion(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
// highlightActiveLine(),
|
||||
// highlightSelectionMatches(),
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
// ...searchKeymap,
|
||||
...historyKeymap,
|
||||
// ...foldKeymap,
|
||||
// ...completionKeymap,
|
||||
]),
|
||||
])();
|
||||
|
||||
/// A minimal set of extensions to create a functional editor. Only
|
||||
/// includes [the default keymap](#commands.defaultKeymap), [undo
|
||||
/// history](#commands.history), [special character
|
||||
/// highlighting](#view.highlightSpecialChars), [custom selection
|
||||
/// drawing](#view.drawSelection), and [default highlight
|
||||
/// style](#language.defaultHighlightStyle).
|
||||
export const minimalSetup = (() => [
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
drawSelection(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
keymap.of([...defaultKeymap, ...historyKeymap]),
|
||||
])();
|
||||
@@ -1,8 +1,8 @@
|
||||
import { closeBrackets } from '@codemirror/autocomplete';
|
||||
export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
|
||||
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { history } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { indentWithTab } from '@codemirror/commands';
|
||||
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
||||
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
|
||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||
import {
|
||||
@@ -24,6 +24,7 @@ import { initTheme, activateTheme, theme } from './themes.mjs';
|
||||
import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||
import { widgetPlugin, updateWidgets } from './widget.mjs';
|
||||
import { persistentAtom } from '@nanostores/persistent';
|
||||
import { basicSetup } from './basicSetup.mjs';
|
||||
|
||||
const extensions = {
|
||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||
@@ -37,6 +38,14 @@ const extensions = {
|
||||
isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
|
||||
isFlashEnabled,
|
||||
keybindings,
|
||||
isTabIndentationEnabled: (on) => (on ? keymap.of([indentWithTab]) : []),
|
||||
isMultiCursorEnabled: (on) =>
|
||||
on
|
||||
? [
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
|
||||
]
|
||||
: [],
|
||||
};
|
||||
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||
|
||||
@@ -51,6 +60,8 @@ export const defaultSettings = {
|
||||
isFlashEnabled: true,
|
||||
isTooltipEnabled: false,
|
||||
isLineWrappingEnabled: false,
|
||||
isTabIndentationEnabled: false,
|
||||
isMultiCursorEnabled: false,
|
||||
theme: 'strudelTheme',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 18,
|
||||
@@ -62,7 +73,7 @@ export const codemirrorSettings = persistentAtom('codemirror-settings', defaultS
|
||||
});
|
||||
|
||||
// https://codemirror.net/docs/guide/
|
||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root }) {
|
||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) {
|
||||
const settings = codemirrorSettings.get();
|
||||
const initialSettings = Object.keys(compartments).map((key) =>
|
||||
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
||||
@@ -75,13 +86,17 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
/* search(),
|
||||
highlightSelectionMatches(), */
|
||||
...initialSettings,
|
||||
javascript(),
|
||||
basicSetup,
|
||||
mondo ? [] : javascript(),
|
||||
javascriptLanguage.data.of({
|
||||
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||
}),
|
||||
sliderPlugin,
|
||||
widgetPlugin,
|
||||
// indentOnInput(), // works without. already brought with javascript extension?
|
||||
// bracketMatching(), // does not do anything
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
history(),
|
||||
EditorView.updateListener.of((v) => onChange(v)),
|
||||
drawSelection({ cursorBlinkRate: 0 }),
|
||||
Prec.highest(
|
||||
@@ -209,6 +224,7 @@ export class StrudelMirror {
|
||||
},
|
||||
onEvaluate: () => this.evaluate(),
|
||||
onStop: () => this.stop(),
|
||||
mondo: replOptions.mondo,
|
||||
});
|
||||
const cmEditor = this.root.querySelector('.cm-editor');
|
||||
if (cmEditor) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
||||
import { RangeSetBuilder, StateEffect, StateField, Prec } from '@codemirror/state';
|
||||
import { Decoration, EditorView } from '@codemirror/view';
|
||||
|
||||
export const setMiniLocations = StateEffect.define();
|
||||
@@ -134,5 +134,5 @@ export const isPatternHighlightingEnabled = (on, config) => {
|
||||
setTimeout(() => {
|
||||
updateMiniLocations(config.editor, config.miniLocations);
|
||||
}, 100);
|
||||
return on ? highlightExtension : [];
|
||||
return on ? Prec.highest(highlightExtension) : [];
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
|
||||
export let html = (string) => {
|
||||
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = string.trim();
|
||||
return template.content.childNodes;
|
||||
};
|
||||
let parseChunk = (chunk) => {
|
||||
if (Array.isArray(chunk)) return chunk.flat().join('');
|
||||
|
||||
@@ -3,8 +3,9 @@ import { keymap, ViewPlugin } from '@codemirror/view';
|
||||
// import { searchKeymap } from '@codemirror/search';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
// import { vim } from './vim_test.mjs';
|
||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
|
||||
const vscodePlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
@@ -21,11 +22,11 @@ const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
||||
const keymaps = {
|
||||
vim,
|
||||
emacs,
|
||||
codemirror: () => keymap.of(defaultKeymap),
|
||||
vscode: vscodeExtension,
|
||||
};
|
||||
|
||||
export function keybindings(name) {
|
||||
const active = keymaps[name];
|
||||
return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []];
|
||||
// keymap.of(searchKeymap),
|
||||
return [active ? Prec.high(active()) : []];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/codemirror",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.5",
|
||||
"description": "Codemirror Extensions for Strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
@@ -42,7 +42,7 @@
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@nanostores/persistent": "^0.10.2",
|
||||
"@replit/codemirror-emacs": "^6.1.0",
|
||||
"@replit/codemirror-vim": "^6.2.1",
|
||||
"@replit/codemirror-vim": "^6.3.0",
|
||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/draw": "workspace:*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { hoverTooltip } from '@codemirror/view';
|
||||
import jsdoc from '../../doc.json';
|
||||
import { Autocomplete } from './autocomplete.mjs';
|
||||
import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
|
||||
|
||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||
|
||||
@@ -52,10 +52,11 @@ export const strudelTooltip = hoverTooltip(
|
||||
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
||||
if (!entry) {
|
||||
// Try for synonyms
|
||||
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||
if (!entry) {
|
||||
const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
entry = getSynonymDoc(doc, word);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip(
|
||||
create(view) {
|
||||
let dom = document.createElement('div');
|
||||
dom.className = 'strudel-tooltip';
|
||||
const ac = Autocomplete({ doc: entry, label: word });
|
||||
const ac = Autocomplete(entry);
|
||||
dom.appendChild(ac);
|
||||
return { dom };
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { describe, bench } from 'vitest';
|
||||
|
||||
import { calculateTactus, sequence, stack } from '../index.mjs';
|
||||
import { calculateSteps, sequence, stack } from '../index.mjs';
|
||||
|
||||
const pat64 = sequence(...Array(64).keys());
|
||||
|
||||
describe('steps', () => {
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
bench(
|
||||
'+tactus',
|
||||
() => {
|
||||
@@ -14,7 +14,7 @@ describe('steps', () => {
|
||||
{ time: 1000 },
|
||||
);
|
||||
|
||||
calculateTactus(false);
|
||||
calculateSteps(false);
|
||||
bench(
|
||||
'-tactus',
|
||||
() => {
|
||||
@@ -25,7 +25,7 @@ describe('steps', () => {
|
||||
});
|
||||
|
||||
describe('stack', () => {
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
bench(
|
||||
'+tactus',
|
||||
() => {
|
||||
@@ -34,7 +34,7 @@ describe('stack', () => {
|
||||
{ time: 1000 },
|
||||
);
|
||||
|
||||
calculateTactus(false);
|
||||
calculateSteps(false);
|
||||
bench(
|
||||
'-tactus',
|
||||
() => {
|
||||
@@ -43,4 +43,4 @@ describe('stack', () => {
|
||||
{ time: 1000 },
|
||||
);
|
||||
});
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
|
||||
+301
-37
@@ -4,13 +4,14 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
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, register, sequence } from './pattern.mjs';
|
||||
import { Pattern, register, reify } from './pattern.mjs';
|
||||
|
||||
export function createParam(names) {
|
||||
let isMulti = Array.isArray(names);
|
||||
names = !isMulti ? [names] : names;
|
||||
const name = names[0];
|
||||
|
||||
// todo: make this less confusing
|
||||
const withVal = (xs) => {
|
||||
let bag;
|
||||
// check if we have an object with an unnamed control (.value)
|
||||
@@ -35,25 +36,34 @@ export function createParam(names) {
|
||||
}
|
||||
};
|
||||
|
||||
const func = (...pats) => sequence(...pats).withValue(withVal);
|
||||
|
||||
const setter = function (...pats) {
|
||||
if (!pats.length) {
|
||||
return this.fmap(withVal);
|
||||
// todo: make this less confusing
|
||||
const func = function (value, pat) {
|
||||
if (!pat) {
|
||||
return reify(value).withValue(withVal);
|
||||
}
|
||||
return this.set(func(...pats));
|
||||
if (typeof value === 'undefined') {
|
||||
return pat.fmap(withVal);
|
||||
}
|
||||
return pat.set(reify(value).withValue(withVal));
|
||||
};
|
||||
Pattern.prototype[name] = function (value) {
|
||||
return func(value, this);
|
||||
};
|
||||
Pattern.prototype[name] = setter;
|
||||
return func;
|
||||
}
|
||||
|
||||
// maps control alias names to the "main" control name
|
||||
const controlAlias = new Map();
|
||||
|
||||
export function isControlName(name) {
|
||||
return controlAlias.has(name);
|
||||
}
|
||||
|
||||
export function registerControl(names, ...aliases) {
|
||||
const name = Array.isArray(names) ? names[0] : names;
|
||||
let bag = {};
|
||||
bag[name] = createParam(names);
|
||||
controlAlias.set(name, name);
|
||||
aliases.forEach((alias) => {
|
||||
bag[alias] = bag[name];
|
||||
controlAlias.set(alias, name);
|
||||
@@ -81,6 +91,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
|
||||
* Define a custom webaudio node to use as a sound source.
|
||||
*
|
||||
* @name source
|
||||
* @synonyms src
|
||||
* @param {function} getSource
|
||||
* @synonyms src
|
||||
*
|
||||
@@ -103,7 +114,7 @@ export const { n } = registerControl('n');
|
||||
*
|
||||
* - a letter (a-g or A-G)
|
||||
* - optional accidentals (b or #)
|
||||
* - optional octave number (0-9). Defaults to 3
|
||||
* - optional (possibly negative) octave number (0-9). Defaults to 3
|
||||
*
|
||||
* Examples of valid note names: `c`, `bb`, `Bb`, `f#`, `c3`, `A4`, `Eb2`, `c#5`
|
||||
*
|
||||
@@ -116,6 +127,8 @@ export const { n } = registerControl('n');
|
||||
* note("c4 a4 f4 e4")
|
||||
* @example
|
||||
* note("60 69 65 64")
|
||||
* @example
|
||||
* note("fbb1 a#0 cbbb-1 e##-2").sound("saw")
|
||||
*/
|
||||
export const { note } = registerControl(['note', 'n']);
|
||||
|
||||
@@ -131,8 +144,8 @@ export const { note } = registerControl(['note', 'n']);
|
||||
*/
|
||||
export const { accelerate } = registerControl('accelerate');
|
||||
/**
|
||||
*
|
||||
* Sets the velocity from 0 to 1. Is multiplied together with gain.
|
||||
*
|
||||
* @name velocity
|
||||
* @example
|
||||
* s("hh*8")
|
||||
@@ -242,6 +255,20 @@ export const { fmenv } = registerControl('fmenv');
|
||||
*
|
||||
*/
|
||||
export const { fmattack } = registerControl('fmattack');
|
||||
|
||||
/**
|
||||
* Waveform of the fm modulator
|
||||
*
|
||||
* @name fmwave
|
||||
* @param {number | Pattern} wave waveform
|
||||
* @example
|
||||
* n("0 1 2 3".fast(4)).scale("d:minor").s("sine").fmwave("<sine square sawtooth crackle>").fm(4).fmh(2.01)
|
||||
* @example
|
||||
* n("0 1 2 3".fast(4)).chord("<Dm Am F G>").voicing().s("sawtooth").fmwave("brown").fm(.6)
|
||||
*
|
||||
*/
|
||||
export const { fmwave } = registerControl('fmwave');
|
||||
|
||||
/**
|
||||
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
|
||||
*
|
||||
@@ -285,6 +312,17 @@ export const { fmvelocity } = registerControl('fmvelocity');
|
||||
*/
|
||||
export const { bank } = registerControl('bank');
|
||||
|
||||
/**
|
||||
* mix control for the chorus effect
|
||||
*
|
||||
* @name chorus
|
||||
* @param {string | Pattern} chorus mix amount between 0 and 1
|
||||
* @example
|
||||
* note("d d a# a").s("sawtooth").chorus(.5)
|
||||
*
|
||||
*/
|
||||
export const { chorus } = registerControl('chorus');
|
||||
|
||||
// analyser node send amount 0 - 1 (used by scope)
|
||||
export const { analyze } = registerControl('analyze');
|
||||
// fftSize of analyser
|
||||
@@ -296,6 +334,7 @@ export const { fft } = registerControl('fft');
|
||||
*
|
||||
* @name decay
|
||||
* @param {number | Pattern} time decay time in seconds
|
||||
* @synonyms dec
|
||||
* @example
|
||||
* note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0)
|
||||
*
|
||||
@@ -352,7 +391,7 @@ export const { bandf, bpf, bp } = registerControl(['bandf', 'bandq', 'bpenv'], '
|
||||
// ['bpq'],
|
||||
export const { bandq, bpq } = registerControl('bandq', 'bpq');
|
||||
/**
|
||||
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
||||
* A pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name begin
|
||||
@@ -374,6 +413,32 @@ export const { begin } = registerControl('begin');
|
||||
*
|
||||
*/
|
||||
export const { end } = registerControl('end');
|
||||
/**
|
||||
* the offset of the sample buffer in seconds
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name beginSeconds
|
||||
* @param {number | Pattern} seconds
|
||||
* @example
|
||||
* samples('github:switchangel/pad')
|
||||
* s("swpad!4").clip(1).beginSeconds("2 5 .2 3").fast(2)
|
||||
* @example
|
||||
* samples('github:switchangel/pad')
|
||||
* $: s("swpad!16").beginseconds(time).dec(.1)
|
||||
*
|
||||
*/
|
||||
export const { beginSeconds, beginseconds } = registerControl('beginSeconds', 'beginseconds');
|
||||
/**
|
||||
* The end of the sample buffer in seconds
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name endSeconds
|
||||
* @param {number | Pattern} seconds
|
||||
* @example
|
||||
* s("bd*2,oh*4").endSeconds("<.1 .05 .2 1>").fast(2)
|
||||
*
|
||||
*/
|
||||
export const { endSeconds, endseconds } = registerControl('endSeconds', 'endseconds');
|
||||
/**
|
||||
* Loops the sample.
|
||||
* Note that the tempo of the loop is not synced with the cycle tempo.
|
||||
@@ -413,7 +478,7 @@ export const { loopBegin, loopb } = registerControl('loopBegin', 'loopb');
|
||||
*/
|
||||
export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
|
||||
/**
|
||||
* bit crusher effect.
|
||||
* Bit crusher effect.
|
||||
*
|
||||
* @name crush
|
||||
* @param {number | Pattern} depth between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
|
||||
@@ -424,7 +489,7 @@ export const { loopEnd, loope } = registerControl('loopEnd', 'loope');
|
||||
// ['clhatdecay'],
|
||||
export const { crush } = registerControl('crush');
|
||||
/**
|
||||
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
|
||||
* Fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
|
||||
*
|
||||
* @name coarse
|
||||
* @param {number | Pattern} factor 1 for original 2 for half, 3 for a third and so on.
|
||||
@@ -435,7 +500,80 @@ export const { crush } = registerControl('crush');
|
||||
export const { coarse } = registerControl('coarse');
|
||||
|
||||
/**
|
||||
* filter overdrive for supported filter types
|
||||
* Modulate the amplitude of a sound with a continuous waveform
|
||||
*
|
||||
* @name tremolo
|
||||
* @synonyms trem
|
||||
* @param {number | Pattern} speed modulation speed in HZ
|
||||
* @example
|
||||
* note("d d d# d".fast(4)).s("supersaw").tremolo("<3 2 100> ").tremoloskew("<.5>")
|
||||
*
|
||||
*/
|
||||
export const { tremolo } = registerControl(['tremolo', 'tremolodepth', 'tremoloskew', 'tremolophase'], 'trem');
|
||||
|
||||
/**
|
||||
* Modulate the amplitude of a sound with a continuous waveform
|
||||
*
|
||||
* @name tremolosync
|
||||
* @synonyms tremsync
|
||||
* @param {number | Pattern} cycles modulation speed in cycles
|
||||
* @example
|
||||
* note("d d d# d".fast(4)).s("supersaw").tremolosync("4").tremoloskew("<1 .5 0>")
|
||||
*
|
||||
*/
|
||||
export const { tremolosync } = registerControl(
|
||||
['tremolosync', 'tremolodepth', 'tremoloskew', 'tremolophase'],
|
||||
'tremsync',
|
||||
);
|
||||
|
||||
/**
|
||||
* Depth of amplitude modulation
|
||||
*
|
||||
* @name tremolodepth
|
||||
* @synonyms tremdepth
|
||||
* @param {number | Pattern} depth
|
||||
* @example
|
||||
* note("a1 a1 a#1 a1".fast(4)).s("pulse").tremsync(4).tremolodepth("<1 2 .7>")
|
||||
*
|
||||
*/
|
||||
export const { tremolodepth } = registerControl('tremolodepth', 'tremdepth');
|
||||
/**
|
||||
* Alter the shape of the modulation waveform
|
||||
*
|
||||
* @name tremoloskew
|
||||
* @synonyms tremskew
|
||||
* @param {number | Pattern} amount between 0 & 1, the shape of the waveform
|
||||
* @example
|
||||
* note("{f a c e}%16").s("sawtooth").tremsync(4).tremoloskew("<.5 0 1>")
|
||||
*
|
||||
*/
|
||||
export const { tremoloskew } = registerControl('tremoloskew', 'tremskew');
|
||||
|
||||
/**
|
||||
* Alter the phase of the modulation waveform
|
||||
*
|
||||
* @name tremolophase
|
||||
* @synonyms tremphase
|
||||
* @param {number | Pattern} offset the offset in cycles of the modulation
|
||||
* @example
|
||||
* note("{f a c e}%16").s("sawtooth").tremsync(4).tremolophase("<0 .25 .66>")
|
||||
*
|
||||
*/
|
||||
export const { tremolophase } = registerControl('tremolophase', 'tremphase');
|
||||
|
||||
/**
|
||||
* Shape of amplitude modulation
|
||||
*
|
||||
* @name tremoloshape
|
||||
* @synonyms tremshape
|
||||
* @param {number | Pattern} shape tri | square | sine | saw | ramp
|
||||
* @example
|
||||
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
|
||||
*
|
||||
*/
|
||||
export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
|
||||
/**
|
||||
* Filter overdrive for supported filter types
|
||||
*
|
||||
* @name drive
|
||||
* @param {number | Pattern} amount
|
||||
@@ -445,6 +583,92 @@ export const { coarse } = registerControl('coarse');
|
||||
*/
|
||||
export const { drive } = registerControl('drive');
|
||||
|
||||
/**
|
||||
* Modulate the amplitude of an orbit to create a "sidechain" like effect.
|
||||
*
|
||||
* Can be applied to multiple orbits with the ':' mininotation, e.g. `duckorbit("2:3")`
|
||||
*
|
||||
* @name duckorbit
|
||||
* @synonyms duck
|
||||
* @param {number | Pattern} orbit target orbit
|
||||
* @example
|
||||
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth(1)
|
||||
* @example
|
||||
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||
* $: s("hh*16").orbit(3)
|
||||
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth(1)
|
||||
*
|
||||
*/
|
||||
export const { duck } = registerControl('duckorbit', 'duck');
|
||||
|
||||
/**
|
||||
* The amount of ducking applied to target orbit
|
||||
*
|
||||
* Can vary across orbits with the ':' mininotation, e.g. `duckdepth("0.3:0.1")`.
|
||||
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
|
||||
*
|
||||
* @name duckdepth
|
||||
* @param {number | Pattern} depth depth of modulation from 0 to 1
|
||||
* @example
|
||||
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack(0.2).duckdepth("<1 .9 .6 0>"))
|
||||
* @example
|
||||
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
|
||||
* $: s("hh*16").orbit(3)
|
||||
* $: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:3").duckattack(0.2).duckdepth("1:0.5")
|
||||
*
|
||||
*/
|
||||
export const { duckdepth } = registerControl('duckdepth');
|
||||
|
||||
/**
|
||||
* The time required for the ducked signal(s) to reach their lowest volume.
|
||||
* Can be used to prevent clicking or for creative rhythmic effects.
|
||||
*
|
||||
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
|
||||
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
|
||||
*
|
||||
* @name duckonset
|
||||
* @synonyms duckons
|
||||
*
|
||||
* @param {number | Pattern} time The onset time in seconds
|
||||
* @example
|
||||
* // Clicks
|
||||
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
|
||||
* duckerWithClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0).postgain(0)
|
||||
* @example
|
||||
* // No clicks
|
||||
* sound: freq("63.2388").s("sine").orbit(2).gain(4)
|
||||
* duckerWithoutClick: s("bd*4").duckorbit(2).duckattack(0.3).duckonset(0.01).postgain(0)
|
||||
* @example
|
||||
* // Rhythmic
|
||||
* noise: s("pink").distort("2:1").orbit(4) // used rhythmically with 0.3 onset below
|
||||
* hhat: s("hh*16").orbit(7)
|
||||
* ducker: s("bd*4").bank("tr909").duckorbit("4:7").duckonset("0.3:0.003").duckattack(0.25)
|
||||
*
|
||||
*/
|
||||
export const { duckonset } = registerControl('duckonset', 'duckons');
|
||||
|
||||
/**
|
||||
* The time required for the ducked signal(s) to return to their normal volume.
|
||||
*
|
||||
* Can vary across orbits with the ':' mininotation, e.g. `duckonset("0:0.003")`.
|
||||
* Note: this requires first applying the effect to multiple orbits with e.g. `duckorbit("2:3")`.
|
||||
*
|
||||
* @name duckattack
|
||||
* @synonyms duckatt
|
||||
*
|
||||
* @param {number | Pattern} time The attack time in seconds
|
||||
* @example
|
||||
* sound: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
|
||||
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1)
|
||||
* @example
|
||||
* moreduck: n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2)
|
||||
* lessduck: s("hh*16").orbit(5)
|
||||
* ducker: s("bd:4!4").beat("0,4,8,11,14",16).duckorbit("2:5").duckattack("0.4:0.1")
|
||||
*
|
||||
*/
|
||||
export const { duckattack } = registerControl('duckattack', 'duckatt');
|
||||
|
||||
/**
|
||||
* Create byte beats with custom expressions
|
||||
*
|
||||
@@ -485,7 +709,7 @@ export const { byteBeatStartTime, bbst } = registerControl('byteBeatStartTime',
|
||||
export const { channels, ch } = registerControl('channels', 'ch');
|
||||
|
||||
/**
|
||||
* controls the pulsewidth of the pulse oscillator
|
||||
* Controls the pulsewidth of the pulse oscillator
|
||||
*
|
||||
* @name pw
|
||||
* @param {number | Pattern} pulsewidth
|
||||
@@ -497,7 +721,7 @@ export const { channels, ch } = registerControl('channels', 'ch');
|
||||
export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
|
||||
|
||||
/**
|
||||
* controls the lfo rate for the pulsewidth of the pulse oscillator
|
||||
* Controls the lfo rate for the pulsewidth of the pulse oscillator
|
||||
*
|
||||
* @name pwrate
|
||||
* @param {number | Pattern} rate
|
||||
@@ -509,7 +733,7 @@ export const { pw } = registerControl(['pw', 'pwrate', 'pwsweep']);
|
||||
export const { pwrate } = registerControl('pwrate');
|
||||
|
||||
/**
|
||||
* controls the lfo sweep for the pulsewidth of the pulse oscillator
|
||||
* Controls the lfo sweep for the pulsewidth of the pulse oscillator
|
||||
*
|
||||
* @name pwsweep
|
||||
* @param {number | Pattern} sweep
|
||||
@@ -550,7 +774,7 @@ export const { phaserrate, ph, phaser } = registerControl(
|
||||
export const { phasersweep, phs } = registerControl('phasersweep', 'phs');
|
||||
|
||||
/**
|
||||
* The center frequency of the phaser in HZ. Defaults to 1000
|
||||
* The center frequency of the phaser in HZ. Defaults to 1000
|
||||
*
|
||||
* @name phasercenter
|
||||
* @synonyms phc
|
||||
@@ -567,7 +791,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
|
||||
* The amount the signal is affected by the phaser effect. Defaults to 0.75
|
||||
*
|
||||
* @name phaserdepth
|
||||
* @synonyms phd
|
||||
* @synonyms phd, phasdp
|
||||
* @param {number | Pattern} depth number between 0 and 1
|
||||
* @example
|
||||
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
|
||||
@@ -578,7 +802,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
|
||||
export const { phaserdepth, phd, phasdp } = registerControl('phaserdepth', 'phd', 'phasdp');
|
||||
|
||||
/**
|
||||
* choose the channel the pattern is sent to in superdirt
|
||||
* Choose the channel the pattern is sent to in superdirt
|
||||
*
|
||||
* @name channel
|
||||
* @param {number | Pattern} channel channel number
|
||||
@@ -964,26 +1188,55 @@ export const { delay } = registerControl(['delay', 'delaytime', 'delayfeedback']
|
||||
*
|
||||
*/
|
||||
export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback', 'delayfb', 'dfb');
|
||||
|
||||
/**
|
||||
* Sets the level of the signal that is fed back into the delay.
|
||||
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
|
||||
*
|
||||
* @name delayfeedback
|
||||
* @param {number | Pattern} feedback between 0 and 1
|
||||
* @synonyms delayfb, dfb
|
||||
* @example
|
||||
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>")
|
||||
*
|
||||
*/
|
||||
export const { delayspeed } = registerControl('delayspeed');
|
||||
/**
|
||||
* Sets the time of the delay effect.
|
||||
*
|
||||
* @name delaytime
|
||||
* @param {number | Pattern} seconds between 0 and Infinity
|
||||
* @name delayspeed
|
||||
* @param {number | Pattern} delayspeed controls the pitch of the delay feedback
|
||||
* @synonyms delayt, dt
|
||||
* @example
|
||||
* s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")
|
||||
* note("d d a# a".fast(2)).s("sawtooth").delay(.8).delaytime(1/2).delayspeed("<2 .5 -1 -2>")
|
||||
*
|
||||
*/
|
||||
export const { delaytime, delayt, dt } = registerControl('delaytime', 'delayt', 'dt');
|
||||
/* // TODO: test
|
||||
|
||||
/**
|
||||
* Sets the time of the delay effect in cycles.
|
||||
*
|
||||
* @name delaysync
|
||||
* @param {number | Pattern} cycles delay length in cycles
|
||||
* @synonyms delayt, dt
|
||||
* @example
|
||||
* s("bd bd").delay(.25).delaysync("<1 2 3 5>".div(8))
|
||||
*
|
||||
*/
|
||||
export const { delaysync } = registerControl('delaysync');
|
||||
|
||||
/**
|
||||
* Specifies whether delaytime is calculated relative to cps.
|
||||
*
|
||||
* @name lock
|
||||
* @param {number | Pattern} enable When set to 1, delaytime is a direct multiple of a cycle.
|
||||
* @superdirtOnly
|
||||
* @example
|
||||
* s("sd").delay().lock(1).osc()
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
export const { lock } = registerControl('lock');
|
||||
/**
|
||||
* Set detune for stacked voices of supported oscillators
|
||||
@@ -1033,6 +1286,7 @@ export const { dry } = registerControl('dry');
|
||||
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
|
||||
*
|
||||
* @name fadeTime
|
||||
* @synonyms fadeOutTime
|
||||
* @param {number | Pattern} time between 0 and 1
|
||||
* @example
|
||||
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()
|
||||
@@ -1367,6 +1621,29 @@ export const { roomfade, rfade } = registerControl('roomfade', 'rfade');
|
||||
*
|
||||
*/
|
||||
export const { ir, iresponse } = registerControl(['ir', 'i'], 'iresponse');
|
||||
|
||||
/**
|
||||
* Sets speed of the sample for the impulse response.
|
||||
* @name irspeed
|
||||
* @param {string | Pattern} speed
|
||||
* @example
|
||||
* samples('github:switchangel/pad')
|
||||
* $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.2).irspeed("<2 1 .5>/2").irbegin(.5).roomsize(.5)
|
||||
*
|
||||
*/
|
||||
export const { irspeed } = registerControl('irspeed');
|
||||
|
||||
/**
|
||||
* Sets the beginning of the IR response sample
|
||||
* @name irbegin
|
||||
* @param {string | Pattern} begin between 0 and 1
|
||||
* @synonyms ir
|
||||
* @example
|
||||
* samples('github:switchangel/pad')
|
||||
* $: s("brk/2").fit().scrub(irand(16).div(16).seg(8)).ir("swpad:4").room(.65).irspeed("-2").irbegin("<0 .5 .75>/2").roomsize(.6)
|
||||
*
|
||||
*/
|
||||
export const { irbegin } = registerControl('irbegin');
|
||||
/**
|
||||
* Sets the room size of the reverb, see `room`.
|
||||
* When this property is changed, the reverb will be recaculated, so only change this sparsely..
|
||||
@@ -1535,18 +1812,6 @@ export const { density } = registerControl('density');
|
||||
// ['modwheel'],
|
||||
export const { expression } = registerControl('expression');
|
||||
export const { sustainpedal } = registerControl('sustainpedal');
|
||||
/* // TODO: doesn't seem to do anything
|
||||
*
|
||||
* Tremolo Audio DSP effect
|
||||
*
|
||||
* @name tremolodepth
|
||||
* @param {number | Pattern} depth between 0 and 1
|
||||
* @example
|
||||
* n("0,4,7").tremolodepth("<0 .3 .6 .9>").osc()
|
||||
*
|
||||
*/
|
||||
export const { tremolodepth, tremdp } = registerControl('tremolodepth', 'tremdp');
|
||||
export const { tremolorate, tremr } = registerControl('tremolorate', 'tremr');
|
||||
|
||||
export const { fshift } = registerControl('fshift');
|
||||
export const { fshiftnote } = registerControl('fshiftnote');
|
||||
@@ -1623,7 +1888,6 @@ export const { zmod } = registerControl('zmod');
|
||||
// like crush but scaled differently
|
||||
export const { zcrush } = registerControl('zcrush');
|
||||
export const { zdelay } = registerControl('zdelay');
|
||||
export const { tremolo } = registerControl('tremolo');
|
||||
export const { zzfx } = registerControl('zzfx');
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import createClock from './zyklus.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { errorLogger, logger } from './logger.mjs';
|
||||
|
||||
export class Cyclist {
|
||||
constructor({
|
||||
@@ -67,6 +67,7 @@ export class Cyclist {
|
||||
// the following line is dumb and only here for backwards compatibility
|
||||
// see https://codeberg.org/uzu/strudel/pulls/1004
|
||||
const deadline = targetTime - phase;
|
||||
// this onTrigger has another signature
|
||||
onTrigger?.(hap, deadline, duration, this.cps, targetTime);
|
||||
if (hap.value.cps !== undefined && this.cps != hap.value.cps) {
|
||||
this.cps = hap.value.cps;
|
||||
@@ -75,7 +76,7 @@ export class Cyclist {
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger(`[cyclist] error: ${e.message}`);
|
||||
errorLogger(e);
|
||||
onError?.(e);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ https://rohandrape.net/?t=hmt
|
||||
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 { timeCat, register, silence } from './pattern.mjs';
|
||||
import { timeCat, register, silence, stack, pure, _morph } from './pattern.mjs';
|
||||
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
|
||||
import Fraction, { lcm } from './fraction.mjs';
|
||||
|
||||
@@ -139,6 +139,14 @@ export const euclid = register('euclid', function (pulses, steps, pat) {
|
||||
return pat.struct(_euclidRot(pulses, steps, 0));
|
||||
});
|
||||
|
||||
export const e = register('e', function (euc, pat) {
|
||||
if (!Array.isArray(euc)) {
|
||||
euc = [euc];
|
||||
}
|
||||
const [pulses, steps = pulses, rot = 0] = euc;
|
||||
return pat.struct(_euclidRot(pulses, steps, rot));
|
||||
});
|
||||
|
||||
export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) {
|
||||
return pat.struct(_euclidRot(pulses, steps, rotation));
|
||||
});
|
||||
@@ -188,3 +196,26 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
|
||||
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
||||
return _euclidLegato(pulses, steps, rotation, pat);
|
||||
});
|
||||
|
||||
/**
|
||||
* A 'euclid' variant with an additional parameter that morphs the resulting
|
||||
* rhythm from 0 (no morphing) to 1 (completely 'even'). For example
|
||||
* `sound("bd").euclidish(3,8,0)` would be the same as
|
||||
* `sound("bd").euclid(3,8)`, and `sound("bd").euclidish(3,8,1)` would be the
|
||||
* same as `sound("bd bd bd")`. `sound("bd").euclidish(3,8,0.5)` would have a
|
||||
* groove somewhere between.
|
||||
* Inspired by the work of Malcom Braff.
|
||||
* @name euclidish
|
||||
* @synonyms eish
|
||||
* @memberof Pattern
|
||||
* @param {number} pulses the number of onsets
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param {number} groove exists between the extremes of 0 (straight euclidian) and 1 (straight pulse)
|
||||
* @example
|
||||
* sound("hh").euclidish(7,12,sine.slow(8))
|
||||
* .pan(sine.slow(8))
|
||||
*/
|
||||
export const { euclidish, eish } = register(['euclidish', 'eish'], function (pulses, steps, perc, pat) {
|
||||
const morphed = _morph(bjork(pulses, steps), new Array(pulses).fill(1), perc);
|
||||
return pat.struct(morphed).setSteps(steps);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const strudelScope = {};
|
||||
|
||||
export const evalScope = async (...args) => {
|
||||
const results = await Promise.allSettled(args);
|
||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||
@@ -18,6 +20,7 @@ export const evalScope = async (...args) => {
|
||||
modules.forEach((module) => {
|
||||
Object.entries(module).forEach(([name, value]) => {
|
||||
globalThis[name] = value;
|
||||
strudelScope[name] = value;
|
||||
});
|
||||
});
|
||||
return modules;
|
||||
|
||||
@@ -4,6 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
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 Fraction from './fraction.mjs';
|
||||
import { stringifyValues } from './util.mjs';
|
||||
|
||||
export class Hap {
|
||||
/*
|
||||
@@ -148,13 +149,7 @@ export class Hap {
|
||||
}
|
||||
|
||||
showWhole(compact = false) {
|
||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
|
||||
typeof this.value === 'object'
|
||||
? compact
|
||||
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||
: JSON.stringify(this.value)
|
||||
: this.value
|
||||
}`;
|
||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`;
|
||||
}
|
||||
|
||||
combineContext(b) {
|
||||
|
||||
@@ -4,6 +4,12 @@ let debounce = 1000,
|
||||
lastMessage,
|
||||
lastTime;
|
||||
|
||||
export function errorLogger(e, origin = 'cyclist') {
|
||||
//TODO: add some kind of debug flag that enables this while in dev mode
|
||||
// console.error(e);
|
||||
logger(`[${origin}] error: ${e.message}`);
|
||||
}
|
||||
|
||||
export function logger(message, type, data = {}) {
|
||||
let t = performance.now();
|
||||
if (lastMessage === message && t - lastTime < debounce) {
|
||||
|
||||
@@ -11,7 +11,6 @@ export class NeoCyclist {
|
||||
constructor({ onTrigger, onToggle, getTime }) {
|
||||
this.started = false;
|
||||
this.cps = 0.5;
|
||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||
this.getTime = getTime; // get absolute time
|
||||
this.time_at_last_tick_message = 0;
|
||||
// the clock of the worker and the audio context clock can drift apart over time
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/core",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
+215
-20
@@ -21,6 +21,8 @@ import {
|
||||
numeralArgs,
|
||||
parseNumeral,
|
||||
pairs,
|
||||
zipWith,
|
||||
stringifyValues,
|
||||
} from './util.mjs';
|
||||
import drawLine from './drawLine.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
@@ -852,14 +854,29 @@ export class Pattern {
|
||||
);
|
||||
}
|
||||
|
||||
log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) {
|
||||
/**
|
||||
* Writes the content of the current event to the console (visible in the side menu).
|
||||
* @name log
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("bd sd").log()
|
||||
*/
|
||||
log(func = (hap) => `[hap] ${hap.showWhole(true)}`, getData = (hap) => ({ hap })) {
|
||||
return this.onTrigger((...args) => {
|
||||
logger(func(...args), undefined, getData(...args));
|
||||
}, false);
|
||||
}
|
||||
|
||||
logValues(func = id) {
|
||||
return this.log((_, hap) => func(hap.value));
|
||||
/**
|
||||
* A simplified version of `log` which writes all "values" (various configurable parameters)
|
||||
* within the event to the console (visible in the side menu).
|
||||
* @name logValues
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("bd sd").gain("0.25 0.5 1").n("2 1 0").logValues()
|
||||
*/
|
||||
logValues(func = (value) => `[hap] ${stringifyValues(value, true)}`) {
|
||||
return this.log((hap) => func(hap.value));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
@@ -869,6 +886,31 @@ export class Pattern {
|
||||
console.log(drawLine(this));
|
||||
return this;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// methods relating to breaking patterns into subcycles
|
||||
|
||||
// Breaks a pattern into a pattern of patterns, according to the structure of the given binary pattern.
|
||||
unjoin(pieces, func = id) {
|
||||
return pieces.withHap((hap) =>
|
||||
hap.withValue((v) => (v ? func(this.ribbon(hap.whole.begin, hap.whole.duration)) : this)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaks a pattern into pieces according to the structure of a given pattern.
|
||||
* True values in the given pattern cause the corresponding subcycle of the
|
||||
* source pattern to be looped, and for an (optional) given function to be
|
||||
* applied. False values result in the corresponding part of the source pattern
|
||||
* to be played unchanged.
|
||||
* @name into
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* sound("bd sd ht lt").into("1 0", hurry(2))
|
||||
*/
|
||||
into(pieces, func) {
|
||||
return this.unjoin(pieces, func).innerJoin();
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
@@ -904,12 +946,13 @@ Pattern.prototype.collect = function () {
|
||||
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
|
||||
* .arpWith(haps => haps[2])
|
||||
* */
|
||||
Pattern.prototype.arpWith = function (func) {
|
||||
return this.collect()
|
||||
export const arpWith = register('arpWith', (func, pat) => {
|
||||
return pat
|
||||
.collect()
|
||||
.fmap((v) => reify(func(v)))
|
||||
.innerJoin()
|
||||
.withHap((h) => new Hap(h.whole, h.part, h.value.value, h.combineContext(h.value)));
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Selects indices in in stacked notes.
|
||||
@@ -917,9 +960,11 @@ Pattern.prototype.arpWith = function (func) {
|
||||
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
|
||||
* .arp("0 [0,2] 1 [0,2]")
|
||||
* */
|
||||
Pattern.prototype.arp = function (pat) {
|
||||
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
|
||||
};
|
||||
export const arp = register(
|
||||
'arp',
|
||||
(indices, pat) => pat.arpWith((haps) => reify(indices).fmap((i) => haps[i % haps.length])),
|
||||
false,
|
||||
);
|
||||
|
||||
/*
|
||||
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
|
||||
@@ -1201,7 +1246,8 @@ export const silence = gap(1);
|
||||
/* Like silence, but with a 'steps' (relative duration) of 0 */
|
||||
export const nothing = gap(0);
|
||||
|
||||
/** A discrete value that repeats once per cycle.
|
||||
/**
|
||||
* A discrete value that repeats once per cycle.
|
||||
*
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
@@ -1254,13 +1300,14 @@ export function sequenceP(pats) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The given items are played at the same time at the same length.
|
||||
/**
|
||||
* The given items are played at the same time at the same length.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @synonyms polyrhythm, pr
|
||||
* @example
|
||||
* stack("g3", "b3", ["e4", "d4"]).note()
|
||||
* // "g3,b3,[e4,d4]".note()
|
||||
* // "g3,b3,[e4 d4]".note()
|
||||
*
|
||||
* @example
|
||||
* // As a chained function:
|
||||
@@ -1337,11 +1384,11 @@ export function stackBy(by, ...pats) {
|
||||
.setSteps(steps);
|
||||
}
|
||||
|
||||
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
|
||||
*
|
||||
* synonyms: `cat`
|
||||
/**
|
||||
* Concatenation: combines a list of patterns, switching between them successively, one per cycle.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @synonyms cat
|
||||
* @example
|
||||
* slowcat("e5", "b4", ["d5", "c5"])
|
||||
*
|
||||
@@ -1541,7 +1588,7 @@ export const func = curry((a, b) => reify(b).func(a));
|
||||
/**
|
||||
* Registers a new pattern method. The method is added to the Pattern class + the standalone function is returned from register.
|
||||
*
|
||||
* @param {string} name name of the function
|
||||
* @param {string | string[]} name name of the function, or an array of names to be used as synonyms
|
||||
* @param {function} func function with 1 or more params, where last is the current pattern
|
||||
* @noAutocomplete
|
||||
*
|
||||
@@ -2352,6 +2399,57 @@ export const stut = register('stut', function (times, feedback, time, pat) {
|
||||
return pat._echoWith(times, time, (pat, i) => pat.gain(Math.pow(feedback, i)));
|
||||
});
|
||||
|
||||
export const applyN = register('applyN', function (n, func, p) {
|
||||
let result = p;
|
||||
for (let i = 0; i < n; i++) {
|
||||
result = func(result);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* The plyWith function repeats each event the given number of times, applying the given function to each event.\n
|
||||
* @name plyWith
|
||||
* @synonyms plywith
|
||||
* @param {number} factor how many times to repeat
|
||||
* @param {function} func function to apply, given the pattern
|
||||
* @example
|
||||
* "<0 [2 4]>"
|
||||
* .plyWith(4, (p) => p.add(2))
|
||||
* .scale("C:minor").note()
|
||||
*/
|
||||
export const plyWith = register(['plyWith', 'plywith'], function (factor, func, pat) {
|
||||
const result = pat
|
||||
.fmap((x) => cat(...listRange(0, factor - 1).map((i) => applyN(i, func, x)))._fast(factor))
|
||||
.squeezeJoin();
|
||||
if (__steps) {
|
||||
result._steps = Fraction(factor).mulmaybe(pat._steps);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* The plyForEach function repeats each event the given number of times, applying the given function to each event.
|
||||
* This version of ply uses the iteration index as an argument to the function, similar to echoWith.
|
||||
* @name plyForEach
|
||||
* @synonyms plyforeach
|
||||
* @param {number} factor how many times to repeat
|
||||
* @param {function} func function to apply, given the pattern and the iteration index
|
||||
* @example
|
||||
* "<0 [2 4]>"
|
||||
* .plyForEach(4, (p,n) => p.add(n*2))
|
||||
* .scale("C:minor").note()
|
||||
*/
|
||||
export const plyForEach = register(['plyForEach', 'plyforeach'], function (factor, func, pat) {
|
||||
const result = pat
|
||||
.fmap((x) => cat(cat(pure(x), ...listRange(1, factor - 1).map((i) => func(pure(x), i))))._fast(factor))
|
||||
.squeezeJoin();
|
||||
if (__steps) {
|
||||
result._steps = Fraction(factor).mulmaybe(pat._steps);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Divides a pattern into a given number of subdivisions, plays the subdivisions in order, but increments the starting subdivision each cycle. The pattern wraps to the first subdivision after the last subdivision is played.
|
||||
* @name iter
|
||||
@@ -2491,6 +2589,37 @@ export const { fastchunk, fastChunk } = register(
|
||||
true,
|
||||
);
|
||||
|
||||
/**
|
||||
* Like `chunk`, but the function is applied to a looped subcycle of the source pattern.
|
||||
* @name chunkInto
|
||||
* @synonyms chunkinto
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
|
||||
* .bank("tr909")
|
||||
*/
|
||||
export const { chunkinto, chunkInto } = register(['chunkinto', 'chunkInto'], function (n, func, pat) {
|
||||
return pat.into(fastcat(true, ...Array(n - 1).fill(false))._iterback(n), func);
|
||||
});
|
||||
|
||||
/**
|
||||
* Like `chunkInto`, but moves backwards through the chunks.
|
||||
* @name chunkBackInto
|
||||
* @synonyms chunkbackinto
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* sound("bd sd ht lt bd - cp lt").chunkInto(4, hurry(2))
|
||||
* .bank("tr909")
|
||||
*/
|
||||
export const { chunkbackinto, chunkBackInto } = register(['chunkbackinto', 'chunkBackInto'], function (n, func, pat) {
|
||||
return pat.into(
|
||||
fastcat(true, ...Array(n - 1).fill(false))
|
||||
._iter(n)
|
||||
._early(1),
|
||||
func,
|
||||
);
|
||||
});
|
||||
|
||||
// TODO - redefine elsewhere in terms of mask
|
||||
export const bypass = register(
|
||||
'bypass',
|
||||
@@ -2506,7 +2635,7 @@ export const bypass = register(
|
||||
* Loops the pattern inside an `offset` for `cycles`.
|
||||
* If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it.
|
||||
* @name ribbon
|
||||
* @synonym rib
|
||||
* @synonyms rib
|
||||
* @param {number} offset start point of loop in cycles
|
||||
* @param {number} cycles loop length in cycles
|
||||
* @example
|
||||
@@ -2720,7 +2849,7 @@ export function stepcat(...timepats) {
|
||||
if (timepats.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
const findsteps = (x) => (Array.isArray(x) ? x : [x._steps, x]);
|
||||
const findsteps = (x) => (Array.isArray(x) ? x : [x._steps ?? 1, x]);
|
||||
timepats = timepats.map(findsteps);
|
||||
if (timepats.find((x) => x[0] === undefined)) {
|
||||
const times = timepats.map((a) => a[0]).filter((x) => x !== undefined);
|
||||
@@ -3201,10 +3330,10 @@ export const slice = register(
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("bd!8").onTriggerTime((hap) => {console.info(hap)})
|
||||
* s("bd!8").onTriggerTime((hap) => {console.log(hap)})
|
||||
*/
|
||||
Pattern.prototype.onTriggerTime = function (func) {
|
||||
return this.onTrigger((t_deprecate, hap, currentTime, cps = 1, targetTime) => {
|
||||
return this.onTrigger((hap, currentTime, _cps, targetTime) => {
|
||||
const diff = targetTime - currentTime;
|
||||
window.setTimeout(() => {
|
||||
func(hap);
|
||||
@@ -3341,3 +3470,69 @@ export const { beat } = register(
|
||||
['beat'],
|
||||
__beat((x) => x.innerJoin()),
|
||||
);
|
||||
|
||||
export const _morph = (from, to, by) => {
|
||||
by = Fraction(by);
|
||||
const dur = Fraction(1).div(from.length);
|
||||
const positions = (list) => {
|
||||
const result = [];
|
||||
for (const [pos, value] of list.entries()) {
|
||||
if (value) {
|
||||
result.push([Fraction(pos).div(list.length), value]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const arcs = zipWith(
|
||||
([posa, valuea], [posb, valueb]) => {
|
||||
const b = by.mul(posb - posa).add(posa);
|
||||
const e = b.add(dur);
|
||||
return new TimeSpan(b, e);
|
||||
},
|
||||
positions(from),
|
||||
positions(to),
|
||||
);
|
||||
function query(state) {
|
||||
const cycle = state.span.begin.sam();
|
||||
const cycleArc = state.span.cycleArc();
|
||||
const result = [];
|
||||
for (const whole of arcs) {
|
||||
const part = whole.intersection(cycleArc);
|
||||
if (part !== undefined) {
|
||||
result.push(
|
||||
new Hap(
|
||||
whole.withTime((x) => x.add(cycle)),
|
||||
part.withTime((x) => x.add(cycle)),
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return new Pattern(query).splitQueries();
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes two binary rhythms represented as lists of 1s and 0s, and a number
|
||||
* between 0 and 1 that morphs between them. The two lists should contain the same
|
||||
* number of true values.
|
||||
* @example
|
||||
* sound("hh").struct(morph([1,0,1,0,1,0,1,0], // straight rhythm
|
||||
* [1,1,0,1,0,1,0], // wonky rhythm
|
||||
* 0.25 // creates a slightly wonky rhythm
|
||||
* )
|
||||
* )
|
||||
* @example
|
||||
* sound("hh").struct(morph("1:0:1:0:1:0:1:0", // straight rhythm
|
||||
* "1:1:0:1:0:1:0", // wonky rhythm
|
||||
* sine.slow(8) // slowly morph between the rhythms
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
export const morph = (frompat, topat, bypat) => {
|
||||
frompat = reify(frompat);
|
||||
topat = reify(topat);
|
||||
bypat = reify(bypat);
|
||||
return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by))));
|
||||
};
|
||||
|
||||
+16
-6
@@ -1,7 +1,7 @@
|
||||
import { NeoCyclist } from './neocyclist.mjs';
|
||||
import { Cyclist } from './cyclist.mjs';
|
||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { errorLogger, logger } from './logger.mjs';
|
||||
import { setTime } from './time.mjs';
|
||||
import { evalScope } from './evaluate.mjs';
|
||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||
@@ -21,6 +21,7 @@ export function repl({
|
||||
setInterval,
|
||||
clearInterval,
|
||||
id,
|
||||
mondo = false,
|
||||
}) {
|
||||
const state = {
|
||||
schedulerError: undefined,
|
||||
@@ -113,8 +114,9 @@ export function repl({
|
||||
* all(x => x.pianoroll())
|
||||
* ```
|
||||
*/
|
||||
let allTransforms = [];
|
||||
const all = function (transform) {
|
||||
allTransform = transform;
|
||||
allTransforms.push(transform);
|
||||
return silence;
|
||||
};
|
||||
/** Applies a function to each of the running patterns separately. This is intended for future use with upcoming 'stepwise' features. See `all` for a version that applies the function to all the patterns stacked together into a single pattern.
|
||||
@@ -190,7 +192,12 @@ export function repl({
|
||||
await injectPatternMethods();
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
await beforeEval?.({ code });
|
||||
allTransforms = []; // reset all transforms
|
||||
shouldHush && hush();
|
||||
|
||||
if (mondo) {
|
||||
code = `mondolang\`${code}\``;
|
||||
}
|
||||
let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions);
|
||||
if (Object.keys(pPatterns).length) {
|
||||
let patterns = Object.values(pPatterns);
|
||||
@@ -202,8 +209,10 @@ export function repl({
|
||||
} else if (eachTransform) {
|
||||
pattern = eachTransform(pattern);
|
||||
}
|
||||
if (allTransform) {
|
||||
pattern = allTransform(pattern);
|
||||
if (allTransforms.length) {
|
||||
for (let i in allTransforms) {
|
||||
pattern = allTransforms[i](pattern);
|
||||
}
|
||||
}
|
||||
if (!isPattern(pattern)) {
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
@@ -236,6 +245,7 @@ export function repl({
|
||||
export const getTrigger =
|
||||
({ getTime, defaultOutput }) =>
|
||||
async (hap, deadline, duration, cps, t) => {
|
||||
// ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger)
|
||||
// TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004
|
||||
try {
|
||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||
@@ -243,9 +253,9 @@ export const getTrigger =
|
||||
}
|
||||
if (hap.context.onTrigger) {
|
||||
// call signature of output / onTrigger is different...
|
||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps, t);
|
||||
await hap.context.onTrigger(hap, getTime(), cps, t);
|
||||
}
|
||||
} catch (err) {
|
||||
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||
errorLogger(err, 'getTrigger');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,7 +71,6 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||
|
||||
/**
|
||||
* A sine signal between 0 and 1.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* n(sine.segment(16).range(0,15))
|
||||
@@ -100,7 +99,6 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||
|
||||
/**
|
||||
* A square signal between 0 and 1.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* n(square.segment(4).range(0,7)).scale("C:minor")
|
||||
@@ -154,7 +152,9 @@ export const itri2 = fastcat(isaw2, saw2);
|
||||
*
|
||||
* @return {Pattern}
|
||||
*/
|
||||
export const time = signal(id);
|
||||
export const time = signal((x) => {
|
||||
return x.valueOf();
|
||||
});
|
||||
|
||||
/**
|
||||
* The mouse's x position value ranges from 0 to 1.
|
||||
@@ -266,7 +266,7 @@ export const randrun = (n) => {
|
||||
const rands = timeToRands(t.floor().add(0.5), n);
|
||||
const nums = rands
|
||||
.map((n, i) => [n, i])
|
||||
.sort((a, b) => a[0] > b[0] - a[0] < b[0])
|
||||
.sort((a, b) => (a[0] > b[0]) - (a[0] < b[0]))
|
||||
.map((x) => x[1]);
|
||||
const i = t.cyclePos().mul(n).floor() % n;
|
||||
return nums[i];
|
||||
@@ -285,7 +285,7 @@ const _rearrangeWith = (ipat, n, pat) => {
|
||||
* @example
|
||||
* note("c d e f").sound("piano").shuffle(4)
|
||||
* @example
|
||||
* note("c d e f".shuffle(4), "g").sound("piano")
|
||||
* seq("c d e f".shuffle(4), "g").note().sound("piano")
|
||||
*/
|
||||
export const shuffle = register('shuffle', (n, pat) => {
|
||||
return _rearrangeWith(randrun(n), n, pat);
|
||||
@@ -298,7 +298,7 @@ export const shuffle = register('shuffle', (n, pat) => {
|
||||
* @example
|
||||
* note("c d e f").sound("piano").scramble(4)
|
||||
* @example
|
||||
* note("c d e f".scramble(4), "g").sound("piano")
|
||||
* seq("c d e f".scramble(4), "g").note().sound("piano")
|
||||
*/
|
||||
export const scramble = register('scramble', (n, pat) => {
|
||||
return _rearrangeWith(_irand(n)._segment(n), n, pat);
|
||||
@@ -398,6 +398,10 @@ export const chooseInWith = (pat, xs) => {
|
||||
*/
|
||||
export const choose = (...xs) => chooseWith(rand, xs);
|
||||
|
||||
// todo: doc
|
||||
export const chooseIn = (...xs) => chooseInWith(rand, xs);
|
||||
export const chooseOut = choose;
|
||||
|
||||
/**
|
||||
* Chooses from the given list of values (or patterns of values), according
|
||||
* to the pattern that the method is called on. The pattern should be in
|
||||
|
||||
@@ -32,7 +32,7 @@ function triggerSpeech(words, lang, voice) {
|
||||
}
|
||||
|
||||
export const speak = register('speak', function (lang, voice, pat) {
|
||||
return pat.onTrigger((_, hap) => {
|
||||
return pat.onTrigger((hap) => {
|
||||
triggerSpeech(hap.value, lang, voice);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import Fraction from 'fraction.js';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
TimeSpan,
|
||||
@@ -52,8 +52,11 @@ import {
|
||||
stackCentre,
|
||||
stepcat,
|
||||
sometimes,
|
||||
expand,
|
||||
} from '../index.mjs';
|
||||
|
||||
import { log, logValues } from '../pattern.mjs';
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
|
||||
import { n, s } from '../controls.mjs';
|
||||
@@ -874,7 +877,7 @@ describe('Pattern', () => {
|
||||
.squeezeJoin()
|
||||
.queryArc(3, 4)
|
||||
.map((x) => x.value),
|
||||
).toStrictEqual([Fraction(3)]);
|
||||
).toStrictEqual([3]);
|
||||
});
|
||||
});
|
||||
describe('ply', () => {
|
||||
@@ -1001,7 +1004,7 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('hurry', () => {
|
||||
it('Can speed up patterns and sounds', () => {
|
||||
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
|
||||
sameFirst(s(sequence('a', 'b')).hurry(2), s(sequence('a', 'b')).fast(2).speed(2));
|
||||
});
|
||||
});
|
||||
/*describe('composable functions', () => {
|
||||
@@ -1179,6 +1182,9 @@ describe('Pattern', () => {
|
||||
it('calculates undefined steps as the average', () => {
|
||||
expect(sameFirst(stepcat(pure(1), pure(2), pure(3).setSteps(undefined)), fastcat(1, 2, 3)));
|
||||
});
|
||||
it('works with auto-reified values', () => {
|
||||
expect(sameFirst(stepcat(expand(3, 'bd'), 'rim'), stepcat(expand(3, 'bd'), pure('rim'))));
|
||||
});
|
||||
});
|
||||
describe('shrink', () => {
|
||||
it('can shrink', () => {
|
||||
@@ -1267,4 +1273,75 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('unjoin', () => {
|
||||
it('destructures a pattern into subcycles', () => {
|
||||
sameFirst(
|
||||
fastcat('a', 'b', 'c', 'd')
|
||||
.unjoin(fastcat(true, fastcat(true, true)))
|
||||
.fmap(fast(2))
|
||||
.join(),
|
||||
fastcat('a', 'b', 'a', 'b', 'c', 'c', 'd', 'd'),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('into', () => {
|
||||
it('applies a function to subcycles of a pattern', () => {
|
||||
sameFirst(
|
||||
fastcat('a', 'b', 'c', 'd').into(fastcat(fastcat('true', 'true'), 'true'), fast(2)),
|
||||
fastcat('a', 'a', 'b', 'b', 'c', 'd', 'c', 'd'),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('chunkinto', () => {
|
||||
it('chunks into subcycles', () => {
|
||||
sameFirst(
|
||||
fastcat('a', 'b', 'c').chunkInto(3, fast(2)).fast(3),
|
||||
fastcat(fastcat('a', 'a'), 'b', 'c', 'a', fastcat('b', 'b'), 'c', 'a', 'b', fastcat('c', 'c')),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('chunkbackinto', () => {
|
||||
it('chunks into subcycles backwards', () => {
|
||||
sameFirst(
|
||||
fastcat('a', 'b', 'c').chunkBackInto(3, fast(2)).fast(3),
|
||||
fastcat('a', 'b', fastcat('c', 'c'), 'a', fastcat('b', 'b'), 'c', fastcat('a', 'a'), 'b', 'c'),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('log', () => {
|
||||
it('logs to console', () => {
|
||||
const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const pattern = pure('a').log();
|
||||
const haps = pattern.queryArc(0, 1);
|
||||
|
||||
// Force a trigger
|
||||
haps.forEach((hap) => {
|
||||
hap.context?.onTrigger?.(hap);
|
||||
});
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||
'%c[hap] 0/1 → 1/1: a',
|
||||
'background-color: black;color:white;border-radius:15px',
|
||||
);
|
||||
mockConsoleLog.mockRestore();
|
||||
});
|
||||
});
|
||||
describe('logValues', () => {
|
||||
it('logs values to console', () => {
|
||||
const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const pattern = pure('a').note('c#').logValues();
|
||||
const haps = pattern.queryArc(0, 1);
|
||||
|
||||
// Force a trigger
|
||||
haps.forEach((hap) => {
|
||||
hap.context?.onTrigger?.(hap);
|
||||
});
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledWith(
|
||||
'%c[hap] value:a note:c#',
|
||||
'background-color: black;color:white;border-radius:15px',
|
||||
);
|
||||
mockConsoleLog.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ export class TimeSpan {
|
||||
}
|
||||
|
||||
intersection(other) {
|
||||
// Intersection of two timespans, returns None if they don't intersect.
|
||||
// Intersection of two timespans, returns undefined if they don't intersect.
|
||||
const intersect_begin = this.begin.max(other.begin);
|
||||
const intersect_end = this.end.min(other.end);
|
||||
|
||||
|
||||
+12
-2
@@ -8,12 +8,12 @@ import { logger } from './logger.mjs';
|
||||
|
||||
// returns true if the given string is a note
|
||||
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
||||
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
|
||||
export const isNote = (name) => /^[a-gA-G][#bsf]*-?[0-9]?$/.test(name);
|
||||
export const tokenizeNote = (note) => {
|
||||
if (typeof note !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
|
||||
if (!pc) {
|
||||
return [];
|
||||
}
|
||||
@@ -487,3 +487,13 @@ export function getCurrentKeyboardState() {
|
||||
// }
|
||||
// return lcm((x * y) / gcd(x, y), ...z);
|
||||
// };
|
||||
|
||||
// Takes values -- typically derived from events, i.e. `hap`s -- and renders them
|
||||
// into a readable format
|
||||
export function stringifyValues(value, compact = false) {
|
||||
return typeof value === 'object'
|
||||
? compact
|
||||
? JSON.stringify(value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||
: JSON.stringify(value)
|
||||
: value;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const csound = register('csound', (instrument, pat) => {
|
||||
instrument = instrument || 'triangle';
|
||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
||||
return pat.onTrigger((time_deprecate, hap, currentTime, _cps, targetTime) => {
|
||||
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
|
||||
if (!_csound) {
|
||||
logger('[csound] not loaded yet', 'warning');
|
||||
return;
|
||||
@@ -142,7 +142,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
||||
p1 = `"${instrument}"`;
|
||||
}
|
||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||
return pat.onTrigger((tidal_time, hap) => {
|
||||
return pat.onTrigger((hap, currentTime, _cps, targetTime) => {
|
||||
if (!_csound) {
|
||||
logger('[csound] not loaded yet', 'warning');
|
||||
return;
|
||||
@@ -151,7 +151,7 @@ export const csoundm = register('csoundm', (instrument, pat) => {
|
||||
throw new Error('csound only support objects as hap values');
|
||||
}
|
||||
// Time in seconds counting from now.
|
||||
const p2 = tidal_time - getAudioContext().currentTime;
|
||||
const p2 = targetTime - currentTime;
|
||||
const p3 = hap.duration.valueOf() + 0;
|
||||
const frequency = getFrequency(hap);
|
||||
let { gain = 1, velocity = 0.9 } = hap.value;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/csound",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.5",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -6,7 +6,7 @@ const OFF_MESSAGE = 0x80;
|
||||
const CC_MESSAGE = 0xb0;
|
||||
|
||||
Pattern.prototype.midi = function (output) {
|
||||
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
return this.onTrigger((hap, currentTime, cps, targetTime) => {
|
||||
let { note, nrpnn, nrpv, ccn, ccv, velocity = 0.9, gain = 1 } = hap.value;
|
||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
||||
const latencyMs = 34;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Invoke } from './utils.mjs';
|
||||
|
||||
const collator = new ClockCollator({});
|
||||
|
||||
export async function oscTriggerTauri(t_deprecate, hap, currentTime, cps = 1, targetTime) {
|
||||
export async function oscTriggerTauri(hap, currentTime, cps = 1, targetTime) {
|
||||
const controls = parseControlsFromHap(hap, cps);
|
||||
const params = [];
|
||||
const timestamp = collator.calculateTimestamp(currentTime, targetTime);
|
||||
|
||||
@@ -150,7 +150,7 @@ export class Drawer {
|
||||
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.endClipped >= phase - lookbehind - lookahead)
|
||||
.filter((h) => h.whole && h.endClipped >= phase - lookbehind - lookahead)
|
||||
// add new haps with onset (think right edge bars scrolling in)
|
||||
.concat(haps.filter((h) => h.hasOnset()));
|
||||
const time = phase - lookahead;
|
||||
@@ -175,7 +175,7 @@ export class Drawer {
|
||||
// +0.1 = workaround for weird holes in query..
|
||||
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);
|
||||
this.visibleHaps = this.visibleHaps.filter((h) => h.whole?.begin < t);
|
||||
this.painters = []; // will get populated by .onPaint calls attached to the pattern
|
||||
// query future haps
|
||||
const futureHaps = scheduler.pattern.queryArc(begin, end, { painters: this.painters });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/draw",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"description": "Helpers for drawing with Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/embed",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
||||
"main": "embed.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/gamepad",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"description": "Gamepad Inputs for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/hydra",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"description": "Hydra integration for strudel",
|
||||
"main": "hydra.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -333,7 +333,7 @@ Pattern.prototype.midi = function (midiport, options = {}) {
|
||||
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
||||
});
|
||||
|
||||
return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
|
||||
return this.onTrigger((hap, currentTime, cps, targetTime) => {
|
||||
if (!WebMidi.enabled) {
|
||||
logger('Midi not enabled');
|
||||
return;
|
||||
@@ -493,6 +493,9 @@ export async function midin(input) {
|
||||
otherInputs?.length ? `Also available: ${getMidiDeviceNamesString(otherInputs)}` : ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
// ensure refs for this input are initialized
|
||||
if (!refs[input]) {
|
||||
refs[input] = {};
|
||||
}
|
||||
const cc = (cc) => ref(() => refs[input][cc] || 0);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/midi",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.5",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, bench } from 'vitest';
|
||||
|
||||
import { calculateTactus } from '../../core/index.mjs';
|
||||
import { calculateSteps } from '../../core/index.mjs';
|
||||
import { mini } from '../index.mjs';
|
||||
|
||||
describe('mini', () => {
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
bench(
|
||||
'+tactus',
|
||||
() => {
|
||||
@@ -13,7 +13,7 @@ describe('mini', () => {
|
||||
{ time: 1000 },
|
||||
);
|
||||
|
||||
calculateTactus(false);
|
||||
calculateSteps(false);
|
||||
bench(
|
||||
'-tactus',
|
||||
() => {
|
||||
@@ -21,5 +21,5 @@ describe('mini', () => {
|
||||
},
|
||||
{ time: 1000 },
|
||||
);
|
||||
calculateTactus(true);
|
||||
calculateSteps(true);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mini",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# mondo
|
||||
|
||||
a lisp-based language intended to be used as a custom dsl for patterns that can stand on its own feet.
|
||||
see the `test` folder for usage examples
|
||||
|
||||
more info:
|
||||
|
||||
- [uzulang I](https://garten.salat.dev/uzu/uzulang1.html)
|
||||
- [uzulang II](https://garten.salat.dev/uzu/uzulang2.html)
|
||||
|
||||
## Example Usage
|
||||
|
||||
```js
|
||||
import { MondoRunner } from 'mondolang'
|
||||
// define our library of functions and variables
|
||||
let lib = {
|
||||
add: (a, b) => a + b,
|
||||
mul: (a, b) => a * b,
|
||||
PI: Math.PI,
|
||||
};
|
||||
// this function will evaluate nodes in the syntax tree
|
||||
function evaluator(node) {
|
||||
// check if node is a leaf node (!= list)
|
||||
if (node.type !== 'list') {
|
||||
// check lib if we find a match in the lib, otherwise return value
|
||||
return lib[node.value] ?? node.value;
|
||||
}
|
||||
// now it can only be a list..
|
||||
const [fn, ...args] = node.children;
|
||||
// children in a list will already be evaluated
|
||||
// the first child is expected to be a function
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`"${fn}" is not a function`);
|
||||
}
|
||||
return fn(...args);
|
||||
}
|
||||
const runner = new MondoRunner({ evaluator });
|
||||
const pat = runner.run('add 1 (mul 2 PI)') // 7.283185307179586
|
||||
```
|
||||
@@ -0,0 +1,484 @@
|
||||
/*
|
||||
mondo.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/test/mini.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/>.
|
||||
*/
|
||||
|
||||
// evolved from https://garten.salat.dev/lisp/parser.html
|
||||
export class MondoParser {
|
||||
// these are the tokens we expect
|
||||
token_types = {
|
||||
comment: /^\/\/(.*?)(?=\n|$)/,
|
||||
quotes_double: /^"(.*?)"/,
|
||||
quotes_single: /^'(.*?)'/,
|
||||
open_list: /^\(/,
|
||||
close_list: /^\)/,
|
||||
open_angle: /^</,
|
||||
close_angle: /^>/,
|
||||
open_square: /^\[/,
|
||||
close_square: /^\]/,
|
||||
open_curly: /^\{/,
|
||||
close_curly: /^\}/,
|
||||
number: /^-?[0-9]*\.?[0-9]+/, // before pipe!
|
||||
// TODO: better error handling when "-" is used as rest, e.g "s [- bd]"
|
||||
op: /^[*/:!@%?+-]|^\.{2}/, // * / : ! @ % ? ..
|
||||
// dollar: /^\$/,
|
||||
pipe: /^#/,
|
||||
stack: /^[,$]/,
|
||||
or: /^[|]/,
|
||||
plain: /^[a-zA-Z0-9-~_^#]+/,
|
||||
};
|
||||
// matches next token
|
||||
next_token(code, offset = 0) {
|
||||
for (let type in this.token_types) {
|
||||
const match = code.match(this.token_types[type]);
|
||||
if (match) {
|
||||
let token = { type, value: match[0] };
|
||||
if (offset !== -1) {
|
||||
// add location
|
||||
token.loc = [offset, offset + match[0].length];
|
||||
}
|
||||
return token;
|
||||
}
|
||||
}
|
||||
throw new Error(`mondo: could not match '${code}'`);
|
||||
}
|
||||
// takes code string, returns list of matched tokens (if valid)
|
||||
tokenize(code, offset = 0) {
|
||||
let tokens = [];
|
||||
let locEnabled = offset !== -1;
|
||||
let trim = () => {
|
||||
// trim whitespace at start, update offset
|
||||
offset += code.length - code.trimStart().length;
|
||||
// trim start and end to not confuse parser
|
||||
return code.trim();
|
||||
};
|
||||
code = trim();
|
||||
while (code.length > 0) {
|
||||
code = trim();
|
||||
const token = this.next_token(code, locEnabled ? offset : -1);
|
||||
code = code.slice(token.value.length);
|
||||
offset += token.value.length;
|
||||
tokens.push(token);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
// take code, return abstract syntax tree
|
||||
parse(code, offset) {
|
||||
this.code = code;
|
||||
this.offset = offset;
|
||||
this.tokens = this.tokenize(code, offset);
|
||||
const expressions = [];
|
||||
while (this.tokens.length) {
|
||||
expressions.push(this.parse_expr());
|
||||
}
|
||||
if (expressions.length === 0) {
|
||||
// empty case
|
||||
return { type: 'list', children: [] };
|
||||
}
|
||||
// do we have multiple top level expressions or a single non list?
|
||||
if (expressions.length > 1 || expressions[0].type !== 'list') {
|
||||
return {
|
||||
type: 'list',
|
||||
children: this.desugar(expressions),
|
||||
};
|
||||
}
|
||||
// we have a single list
|
||||
return expressions[0];
|
||||
}
|
||||
// parses any valid expression
|
||||
parse_expr() {
|
||||
if (!this.tokens[0]) {
|
||||
throw new Error(`unexpected end of file`);
|
||||
// TODO: could we allow that? like (((((((( s bd
|
||||
// return { type: 'list', children: [] };
|
||||
}
|
||||
let next = this.tokens[0]?.type;
|
||||
if (next === 'open_list') {
|
||||
return this.parse_list();
|
||||
}
|
||||
if (next === 'open_angle') {
|
||||
return this.parse_angle();
|
||||
}
|
||||
if (next === 'open_square') {
|
||||
return this.parse_square();
|
||||
}
|
||||
if (next === 'open_curly') {
|
||||
return this.parse_curly();
|
||||
}
|
||||
return this.consume(next);
|
||||
}
|
||||
// Token[] => Token[][], e.g. (x , y z) => [['x'],['y','z']]
|
||||
split_children(children, split_type) {
|
||||
const chunks = [];
|
||||
while (true) {
|
||||
let splitIndex = children.findIndex((child) => child.type === split_type);
|
||||
if (splitIndex === -1) break;
|
||||
const chunk = children.slice(0, splitIndex);
|
||||
chunks.push(chunk);
|
||||
children = children.slice(splitIndex + 1);
|
||||
}
|
||||
chunks.push(children);
|
||||
return chunks;
|
||||
}
|
||||
desugar_split(children, split_type, next) {
|
||||
const chunks = this.split_children(children, split_type);
|
||||
if (chunks.length === 1) {
|
||||
return next(children);
|
||||
}
|
||||
// collect args of stack function
|
||||
const args = chunks
|
||||
.map((chunk) => {
|
||||
if (!chunk.length) {
|
||||
return; // useful for things like "$ s bd $ s hh*8" (first chunk is empty)
|
||||
}
|
||||
if (chunk.length === 1) {
|
||||
// chunks of one element can be added to the stack as is
|
||||
return chunk[0];
|
||||
}
|
||||
// chunks of multiple args
|
||||
chunk = next(chunk);
|
||||
return { type: 'list', children: chunk };
|
||||
})
|
||||
.filter(Boolean); // ignore empty chunks
|
||||
return [{ type: 'plain', value: split_type }, ...args];
|
||||
}
|
||||
// prevents to get a list, e.g. ((x y)) => (x y)
|
||||
unwrap_children(children) {
|
||||
if (children.length === 1) {
|
||||
return children[0].children;
|
||||
}
|
||||
return children;
|
||||
}
|
||||
desugar_ops(children) {
|
||||
while (true) {
|
||||
let opIndex = children.findIndex((child) => child.type === 'op');
|
||||
if (opIndex === -1) break;
|
||||
const op = { type: 'plain', value: children[opIndex].value };
|
||||
if (opIndex === children.length - 1) {
|
||||
//throw new Error(`cannot use operator as last child.`);
|
||||
children[opIndex] = op; // ignore operator if last child.. e.g. "note [c -]"
|
||||
continue;
|
||||
}
|
||||
if (opIndex === 0) {
|
||||
// regular function call (assuming each operator exists as function)
|
||||
children[opIndex] = op;
|
||||
continue;
|
||||
}
|
||||
// convert infix to prefix notation
|
||||
const left = children[opIndex - 1];
|
||||
const right = children[opIndex + 1];
|
||||
if (left.type === 'pipe') {
|
||||
// "x !* 2" => (* 2 x)
|
||||
children[opIndex] = op;
|
||||
continue;
|
||||
}
|
||||
// some careful error handling
|
||||
if (left.type === 'op') {
|
||||
throw new Error(`got 2 ops in a row: "${left.value}${op.value}"`);
|
||||
}
|
||||
if (right.type === 'op') {
|
||||
let err = `got 2 ops in a row: "${op.value}${right.value}"`;
|
||||
if (op.value === '-') {
|
||||
// yes i know this file is not supposed to know about rests x.X
|
||||
err += '. you probably want a rest, which is "_" in mondo!';
|
||||
}
|
||||
throw new Error(err);
|
||||
}
|
||||
const call = { type: 'list', children: [op, right, left] };
|
||||
// insert call while keeping other siblings
|
||||
children = [...children.slice(0, opIndex - 1), call, ...children.slice(opIndex + 2)];
|
||||
children = this.unwrap_children(children);
|
||||
}
|
||||
return children;
|
||||
}
|
||||
get_lambda(args, children) {
|
||||
// (.fast 2) = (fn (_) (fast _ 2))
|
||||
children = this.desugar(children);
|
||||
const body = children.length === 1 ? children[0] : { type: 'list', children };
|
||||
return [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, body];
|
||||
}
|
||||
// returns location range of given ast (even if desugared)
|
||||
get_range(ast, range = [Infinity, 0]) {
|
||||
let union = (a, b) => [Math.min(a[0], b[0]), Math.max(a[1], b[1])];
|
||||
if (ast.loc) {
|
||||
return union(range, ast.loc);
|
||||
}
|
||||
if (ast.type !== 'list') {
|
||||
return range;
|
||||
}
|
||||
return ast.children.reduce((range, child) => {
|
||||
const childrange = this.get_range(child, range);
|
||||
return union(range, childrange);
|
||||
}, range);
|
||||
}
|
||||
errorhead(ast) {
|
||||
return `[mondo ${this.get_range(ast)?.join(':') || '?'}]`;
|
||||
}
|
||||
// returns original user code where the given ast originates (even if desugared)
|
||||
get_code_snippet(ast) {
|
||||
const [min, max] = this.get_range(ast);
|
||||
return this.code.slice(min - this.offset, max - this.offset);
|
||||
}
|
||||
desugar_pipes(children) {
|
||||
let chunks = this.split_children(children, 'pipe');
|
||||
while (chunks.length > 1) {
|
||||
let [left, right, ...rest] = chunks;
|
||||
|
||||
if (!left.length) {
|
||||
const arg = { type: 'plain', value: '_' };
|
||||
return this.get_lambda([arg], [arg, ...children]);
|
||||
}
|
||||
// s jazz hh.fast 2 => (fast 2 (s jazz hh))
|
||||
const call = left.length > 1 ? { type: 'list', children: left } : left[0];
|
||||
chunks = [[...right, call], ...rest];
|
||||
}
|
||||
// return next(chunks[0]);
|
||||
return chunks[0];
|
||||
}
|
||||
parse_pair(open_type, close_type) {
|
||||
const begin = this.tokens[0].loc?.[0];
|
||||
this.consume(open_type);
|
||||
const children = [];
|
||||
while (this.tokens[0]?.type !== close_type) {
|
||||
children.push(this.parse_expr());
|
||||
}
|
||||
const end = this.tokens[0].loc?.[1];
|
||||
this.consume(close_type);
|
||||
const node = { type: 'list', children };
|
||||
if (begin !== undefined) {
|
||||
node.loc = [begin, end];
|
||||
node.raw = this.code.slice(begin, end);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
desugar(children, type) {
|
||||
// if type is given, the first element is expected to contain it as plain value
|
||||
// e.g. with (square a b, c), we want to split (a b, c) and ignore "square"
|
||||
children = type ? children.slice(1) : children;
|
||||
children = this.desugar_split(children, 'stack', (children) =>
|
||||
this.desugar_split(children, 'or', (children) => {
|
||||
// chunks of multiple args
|
||||
if (type) {
|
||||
// the type we've removed before splitting needs to be added back
|
||||
children = [{ type: 'plain', value: type }, ...children];
|
||||
}
|
||||
children = this.desugar_ops(children);
|
||||
// children = this.desugar_pipes(children, (children) => this.desugar_dollars(children));
|
||||
children = this.desugar_pipes(children);
|
||||
return children;
|
||||
}),
|
||||
);
|
||||
return children;
|
||||
}
|
||||
parse_list() {
|
||||
let node = this.parse_pair('open_list', 'close_list');
|
||||
node.children = this.desugar(node.children);
|
||||
return node;
|
||||
}
|
||||
parse_angle() {
|
||||
let node = this.parse_pair('open_angle', 'close_angle');
|
||||
node.children.unshift({ type: 'plain', value: 'angle' });
|
||||
node.children = this.desugar(node.children, 'angle');
|
||||
return node;
|
||||
}
|
||||
parse_square() {
|
||||
let node = this.parse_pair('open_square', 'close_square');
|
||||
node.children.unshift({ type: 'plain', value: 'square' });
|
||||
node.children = this.desugar(node.children, 'square');
|
||||
return node;
|
||||
}
|
||||
parse_curly() {
|
||||
let node = this.parse_pair('open_curly', 'close_curly');
|
||||
node.children.unshift({ type: 'plain', value: 'curly' });
|
||||
node.children = this.desugar(node.children, 'curly');
|
||||
return node;
|
||||
}
|
||||
consume(type) {
|
||||
// shift removes first element and returns it
|
||||
const token = this.tokens.shift();
|
||||
if (token.type !== type) {
|
||||
throw new Error(`expected token type ${type}, got ${token.type}`);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
get_locations(code, offset = 0) {
|
||||
let walk = (ast, locations = []) => {
|
||||
if (ast.type === 'list') {
|
||||
return ast.children.forEach((child) => walk(child, locations));
|
||||
}
|
||||
if (ast.loc) {
|
||||
locations.push(ast.loc);
|
||||
}
|
||||
};
|
||||
const ast = this.parse(code, offset);
|
||||
let locations = [];
|
||||
walk(ast, locations);
|
||||
return locations;
|
||||
}
|
||||
}
|
||||
|
||||
export function printAst(ast, compact = false, lvl = 0) {
|
||||
const br = compact ? '' : '\n';
|
||||
const spaces = compact ? '' : Array(lvl).fill(' ').join('');
|
||||
if (ast.type === 'list') {
|
||||
return `${lvl ? br : ''}${spaces}(${ast.children.map((child) => printAst(child, compact, lvl + 1)).join(' ')}${
|
||||
ast.children.find((child) => child.type === 'list') ? `${br}${spaces})` : ')'
|
||||
}`;
|
||||
}
|
||||
return `${ast.value}`;
|
||||
}
|
||||
|
||||
// lisp runner
|
||||
export class MondoRunner {
|
||||
constructor({ evaluator } = {}) {
|
||||
this.parser = new MondoParser();
|
||||
this.evaluator = evaluator;
|
||||
this.assert(typeof evaluator === 'function', `expected an evaluator function to be passed to new MondoRunner`);
|
||||
}
|
||||
// a helper to check conditions and throw if they are not met
|
||||
assert(condition, error) {
|
||||
if (!condition) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
run(code, scope, offset = 0) {
|
||||
const ast = this.parser.parse(code, offset);
|
||||
//console.log(printAst(ast));
|
||||
return this.evaluate(ast, scope);
|
||||
}
|
||||
evaluate_let(ast, scope) {
|
||||
// (let ((x 3) (y 4)) ...body)
|
||||
// = ((fn (x y) ...body) 3 4)
|
||||
const defs = ast.children[1].children;
|
||||
const args = defs.map((pair) => pair.children[0]);
|
||||
const vals = defs.map((pair) => pair.children[1]);
|
||||
const body = ast.children.slice(2);
|
||||
const lambda = {
|
||||
type: 'list',
|
||||
children: [{ type: 'plain', value: 'fn' }, { type: 'list', children: args }, ...body],
|
||||
};
|
||||
return this.evaluate({ type: 'list', children: [lambda, ...vals] }, scope);
|
||||
}
|
||||
evaluate_def(ast, scope) {
|
||||
// function definition special form?
|
||||
if (ast.children[1].type === 'list') {
|
||||
// (def (add a b) (+ a b))
|
||||
// => (def add (fn (a b) (+ a b)) )
|
||||
const args = ast.children[1].children.slice(1);
|
||||
const lambda = {
|
||||
// lambda
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'fn' },
|
||||
{ type: 'list', children: args },
|
||||
...ast.children.slice(2), // body
|
||||
],
|
||||
};
|
||||
// we mutate to make sure the old ast wont make a mess later
|
||||
ast.children[1] = ast.children[1].children[0];
|
||||
ast.children[2] = lambda;
|
||||
ast.children = ast.children.slice(0, 3); // throw away rest
|
||||
}
|
||||
// (def name body)
|
||||
if (ast.children.length !== 3) {
|
||||
throw new Error(`expected "def" to have 3 children, but got ${ast.children.length}`);
|
||||
}
|
||||
const name = ast.children[1].value;
|
||||
const body = this.evaluate(ast.children[2], scope);
|
||||
scope[name] = body;
|
||||
// def with fall through
|
||||
}
|
||||
evaluate_match(ast, scope) {
|
||||
// (match (p1 e1) (p2 e2) ... (pn en))
|
||||
// = cond in lisp
|
||||
if (ast.children.length < 2) {
|
||||
return;
|
||||
}
|
||||
const [_, ...body] = ast.children;
|
||||
for (let i = 0; i < body.length; ++i) {
|
||||
const [predicate, exp] = body[i].children;
|
||||
if (predicate.value === 'else') {
|
||||
return this.evaluate(exp, scope);
|
||||
}
|
||||
const outcome = this.evaluate(predicate, scope);
|
||||
if (outcome) {
|
||||
return this.evaluate(exp, scope);
|
||||
}
|
||||
}
|
||||
return undefined; // nothing was matched
|
||||
}
|
||||
evaluate_if(ast, scope) {
|
||||
// if is a special case of match
|
||||
if (ast.children.length !== 4) {
|
||||
return;
|
||||
}
|
||||
// (if predicate consequent alternative)
|
||||
const [_, predicate, consequent, alternative] = ast.children;
|
||||
// (match (predicate consequent) (else alternative))
|
||||
const matcher = {
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'match' },
|
||||
{ type: 'list', children: [predicate, consequent] },
|
||||
{ type: 'list', children: [{ type: 'plain', value: 'else' }, alternative] },
|
||||
],
|
||||
};
|
||||
return this.evaluate_match(matcher, scope);
|
||||
}
|
||||
evaluate_lambda(ast, scope) {
|
||||
// (fn (_) (ply 2 _)
|
||||
// ^args ^ body
|
||||
const [_, formalArgs, ...body] = ast.children;
|
||||
return (...args) => {
|
||||
const params = Object.fromEntries(formalArgs.children.map((arg, i) => [arg.value, args[i]]));
|
||||
const closure = {
|
||||
...scope,
|
||||
...params,
|
||||
};
|
||||
// body can have multiple expressions
|
||||
const res = body.map((exp) => this.evaluate(exp, closure));
|
||||
// last expression is the return value
|
||||
return res[res.length - 1];
|
||||
};
|
||||
}
|
||||
evaluate_list(ast, scope) {
|
||||
// evaluate all children before evaluating list (dont mutate!!!)
|
||||
const args = ast.children
|
||||
.filter((child) => child.type !== 'comment') // ignore comments
|
||||
.map((arg) => this.evaluate(arg, scope));
|
||||
const node = { type: 'list', children: args };
|
||||
return this.evaluator(node, scope);
|
||||
}
|
||||
evaluate_leaf(ast, scope) {
|
||||
if (ast.type === 'number') {
|
||||
ast.value = Number(ast.value);
|
||||
} else if (['quotes_double', 'quotes_single'].includes(ast.type)) {
|
||||
ast.value = ast.value.slice(1, -1);
|
||||
ast.type = 'string';
|
||||
}
|
||||
return this.evaluator(ast, scope);
|
||||
}
|
||||
evaluate(ast, scope = {}) {
|
||||
if (ast.type !== 'list') {
|
||||
return this.evaluate_leaf(ast, scope);
|
||||
}
|
||||
const name = ast.children[0]?.value;
|
||||
if (name === 'fn') {
|
||||
return this.evaluate_lambda(ast, scope);
|
||||
}
|
||||
if (name === 'match') {
|
||||
return this.evaluate_match(ast, scope);
|
||||
}
|
||||
if (name === 'if') {
|
||||
return this.evaluate_if(ast, scope);
|
||||
}
|
||||
if (name === 'let') {
|
||||
return this.evaluate_let(ast, scope);
|
||||
}
|
||||
if (name === 'def') {
|
||||
this.evaluate_def(ast, scope);
|
||||
}
|
||||
return this.evaluate_list(ast, scope);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "mondolang",
|
||||
"version": "1.1.1",
|
||||
"description": "a language for functional composition that translates to js",
|
||||
"main": "mondo.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/mondo.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"bench": "vitest bench",
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel/blob/main/packages/mondo/README.md",
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,978 @@
|
||||
/*
|
||||
mondo.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/test/mini.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 { describe, expect, it } from 'vitest';
|
||||
import { MondoParser, printAst, MondoRunner } from '../mondo.mjs';
|
||||
|
||||
const parser = new MondoParser();
|
||||
const p = (code) => parser.parse(code, -1);
|
||||
|
||||
describe('mondo tokenizer', () => {
|
||||
const parser = new MondoParser();
|
||||
it('should tokenize with locations', () =>
|
||||
expect(
|
||||
parser
|
||||
.tokenize('(one two three)')
|
||||
.map((t) => t.value + '=' + t.loc.join('-'))
|
||||
.join(' '),
|
||||
).toEqual('(=0-1 one=1-4 two=5-8 three=9-14 )=14-15'));
|
||||
// it('should parse with loangleions', () => expect(parser.parse('(one two three)')).toEqual());
|
||||
it('should get loangleions', () =>
|
||||
expect(parser.get_locations('s bd rim')).toEqual([
|
||||
[0, 1],
|
||||
[2, 4],
|
||||
[5, 8],
|
||||
]));
|
||||
});
|
||||
describe('mondo s-expressions parser', () => {
|
||||
it('should parse an empty string', () => expect(p('')).toEqual({ type: 'list', children: [] }));
|
||||
it('should parse a single item', () =>
|
||||
expect(p('a')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] }));
|
||||
it('should parse an empty list', () => expect(p('()')).toEqual({ type: 'list', children: [] }));
|
||||
it('should parse a list with 1 item', () =>
|
||||
expect(p('(a)')).toEqual({ type: 'list', children: [{ type: 'plain', value: 'a' }] }));
|
||||
it('should parse a list with 2 items', () =>
|
||||
expect(p('(a b)')).toEqual({
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'a' },
|
||||
{ type: 'plain', value: 'b' },
|
||||
],
|
||||
}));
|
||||
it('should parse a list with 2 items', () =>
|
||||
expect(p('(a (b c))')).toEqual({
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'a' },
|
||||
{
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'b' },
|
||||
{ type: 'plain', value: 'c' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
it('should parse numbers', () =>
|
||||
expect(p('(1 .2 1.2 10 22.3)')).toEqual({
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'number', value: '1' },
|
||||
{ type: 'number', value: '.2' },
|
||||
{ type: 'number', value: '1.2' },
|
||||
{ type: 'number', value: '10' },
|
||||
{ type: 'number', value: '22.3' },
|
||||
],
|
||||
}));
|
||||
it('should parse comments', () =>
|
||||
expect(p('a // hello')).toEqual({
|
||||
type: 'list',
|
||||
children: [
|
||||
{ type: 'plain', value: 'a' },
|
||||
{ type: 'comment', value: '// hello' },
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
let desguar = (a) => {
|
||||
return printAst(parser.parse(a), true);
|
||||
};
|
||||
|
||||
describe('mondo sugar', () => {
|
||||
it('should desugar []', () => expect(desguar('[a b c]')).toEqual('(square a b c)'));
|
||||
it('should desugar [] nested', () => expect(desguar('[a [b c] d]')).toEqual('(square a (square b c) d)'));
|
||||
it('should desugar <>', () => expect(desguar('<a b c>')).toEqual('(angle a b c)'));
|
||||
it('should desugar <> nested', () => expect(desguar('<a <b c> d>')).toEqual('(angle a (angle b c) d)'));
|
||||
it('should desugar mixed [] <>', () => expect(desguar('[a <b c>]')).toEqual('(square a (angle b c))'));
|
||||
it('should desugar mixed <> []', () => expect(desguar('<a [b c]>')).toEqual('(angle a (square b c))'));
|
||||
|
||||
it('should desugar #', () => expect(desguar('s jazz # fast 2')).toEqual('(fast 2 (s jazz))'));
|
||||
it('should desugar # square', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))'));
|
||||
it('should desugar # twice', () => expect(desguar('s jazz # fast 2 # slow 2')).toEqual('(slow 2 (fast 2 (s jazz)))'));
|
||||
it('should desugar # nested', () => expect(desguar('(s cp # fast 2)')).toEqual('(fast 2 (s cp))'));
|
||||
it('should desugar # within []', () => expect(desguar('[bd cp # fast 2]')).toEqual('(fast 2 (square bd cp))'));
|
||||
it('should desugar # within , within []', () =>
|
||||
expect(desguar('[bd cp # fast 2, x]')).toEqual('(stack (fast 2 (square bd cp)) x)'));
|
||||
|
||||
// it('should desugar .(.', () => expect(desguar('[jazz hh.(.fast 2)]')).toEqual('(square jazz (fast 2 hh))'));
|
||||
|
||||
it('should desugar , |', () => expect(desguar('[bd, hh | oh]')).toEqual('(stack bd (or hh oh))'));
|
||||
it('should desugar , | of []', () =>
|
||||
expect(desguar('[bd, hh | [oh rim]]')).toEqual('(stack bd (or hh (square oh rim)))'));
|
||||
it('should desugar , square', () => expect(desguar('[bd, hh]')).toEqual('(stack bd hh)'));
|
||||
it('should desugar , square 2', () => expect(desguar('[bd, hh oh]')).toEqual('(stack bd (square hh oh))'));
|
||||
it('should desugar , square 3', () =>
|
||||
expect(desguar('[bd cp, hh oh]')).toEqual('(stack (square bd cp) (square hh oh))'));
|
||||
it('should desugar , angle', () => expect(desguar('<bd, hh>')).toEqual('(stack bd hh)'));
|
||||
it('should desugar , angle 2', () => expect(desguar('<bd, hh oh>')).toEqual('(stack bd (angle hh oh))'));
|
||||
it('should desugar , angle 3', () =>
|
||||
expect(desguar('<bd cp, hh oh>')).toEqual('(stack (angle bd cp) (angle hh oh))'));
|
||||
it('should desugar , ()', () => expect(desguar('(s bd, s cp)')).toEqual('(stack (s bd) (s cp))'));
|
||||
it('should desugar * /', () => expect(desguar('[a b*2 c d/3 e]')).toEqual('(square a (* 2 b) c (/ 3 d) e)'));
|
||||
it('should desugar []*x', () => expect(desguar('[a [b c]*3]')).toEqual('(square a (* 3 (square b c)))'));
|
||||
it('should desugar []*<x y>', () => expect(desguar('[a b*<2 3> c]')).toEqual('(square a (* (angle 2 3) b) c)'));
|
||||
it('should desugar x:y', () => expect(desguar('x:y')).toEqual('(: y x)'));
|
||||
it('should desugar x:y:z', () => expect(desguar('x:y:z')).toEqual('(: z (: y x))'));
|
||||
it('should desugar x:y*x', () => expect(desguar('bd:0*2')).toEqual('(* 2 (: 0 bd))'));
|
||||
it('should desugar a..b', () => expect(desguar('0..2')).toEqual('(.. 2 0)'));
|
||||
/* it('should desugar x $ y', () => expect(desguar('x $ y')).toEqual('(x y)'));
|
||||
it('should desugar x $ y z', () => expect(desguar('x $ y z')).toEqual('(x (y z))'));
|
||||
it('should desugar x $ y . z', () => expect(desguar('x $ y . z')).toEqual('(z (x y))')); */
|
||||
|
||||
it('should desugar README example', () =>
|
||||
expect(desguar('s [bd hh*2 (cp # crush 4) <mt ht lt>] # speed .8')).toEqual(
|
||||
'(speed .8 (s (square bd (* 2 hh) (crush 4 cp) (angle mt ht lt))))',
|
||||
));
|
||||
|
||||
it('should desugar (#)', () => expect(desguar('(#)')).toEqual('(fn (_) _)'));
|
||||
it('should desugar lambda', () => expect(desguar('(# fast 2)')).toEqual('(fn (_) (fast 2 _))'));
|
||||
it('should desugar lambda call', () => expect(desguar('((# mul 2) 2)')).toEqual('((fn (_) (mul 2 _)) 2)'));
|
||||
it('should desugar lambda with pipe', () =>
|
||||
expect(desguar('(# fast 2 # room 1)')).toEqual('(fn (_) (room 1 (fast 2 _)))'));
|
||||
/* const lambda = parser.parse('(lambda (_) (fast 2 _))');
|
||||
const target = { type: 'plain', value: 'xyz' };
|
||||
it('should desugar_lambda', () =>
|
||||
expect(printAst(parser.desugar_lambda(lambda.children, target))).toEqual('(fast 2 xyz)')); */
|
||||
});
|
||||
|
||||
describe('mondo arithmetic', () => {
|
||||
let multi =
|
||||
(op) =>
|
||||
(init, ...rest) =>
|
||||
rest.reduce((acc, arg) => op(acc, arg), init);
|
||||
|
||||
let lib = {
|
||||
'+': multi((a, b) => a + b),
|
||||
add: multi((a, b) => a + b),
|
||||
'-': multi((a, b) => a - b),
|
||||
sub: multi((a, b) => a - b),
|
||||
'*': multi((a, b) => a * b),
|
||||
'/': multi((a, b) => a / b),
|
||||
mod: multi((a, b) => a % b),
|
||||
eq: (a, b) => a === b,
|
||||
lt: (a, b) => a < b,
|
||||
gt: (a, b) => a > b,
|
||||
and: (a, b) => a && b,
|
||||
or: (a, b) => a || b,
|
||||
not: (a) => !a,
|
||||
run: (...args) => args[args.length - 1],
|
||||
def: () => 0,
|
||||
sin: Math.sin,
|
||||
cos: Math.cos,
|
||||
PI: Math.PI,
|
||||
cons: (a, b) => [a, ...(Array.isArray(b) ? b : [b])],
|
||||
car: (pair) => pair[0],
|
||||
cdr: (pair) => pair.slice(1),
|
||||
list: (...items) => items,
|
||||
nil: [],
|
||||
isnull: (items) => items.length === 0,
|
||||
concat: (...msgs) => msgs.join(''),
|
||||
error: (...msgs) => {
|
||||
throw new Error(msgs.join(' '));
|
||||
},
|
||||
};
|
||||
function evaluator(node, scope) {
|
||||
if (node.type !== 'list') {
|
||||
// is leaf
|
||||
return scope[node.value] ?? lib[node.value] ?? node.value;
|
||||
}
|
||||
// is list
|
||||
const [fn, ...args] = node.children;
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`"${fn}": expected function, got ${typeof fn} "${fn}"`);
|
||||
}
|
||||
return fn(...args);
|
||||
}
|
||||
const runner = new MondoRunner({ evaluator });
|
||||
let evaluate = (exp, scope) => runner.run(`run ${exp}`, scope);
|
||||
let pretty = (exp) => printAst(runner.parser.parse(exp), false);
|
||||
//it('should eval nested expression', () => expect(runner.run('add 1 (mul 2 PI)').toFixed(2)).toEqual('7.28'));
|
||||
|
||||
it('eval number', () => expect(evaluate('2')).toEqual(2));
|
||||
it('eval string', () => expect(evaluate('abc')).toEqual('abc'));
|
||||
it('eval list', () => expect(evaluate('(+ 1 2)')).toEqual(3));
|
||||
it('eval nested list', () => expect(evaluate('(+ 1 (+ 2 3))')).toEqual(6));
|
||||
it('def number', () => expect(evaluate('(def a 2) a')).toEqual(2));
|
||||
it('def + ref number', () => expect(evaluate('(def a 2) (* a a)')).toEqual(4));
|
||||
it('def + call lambda', () => expect(evaluate('(def sqr (fn (x) (* x x))) (sqr 3)')).toEqual(9));
|
||||
|
||||
// sicp
|
||||
it('sicp 8.1', () => expect(evaluate('(+ 137 349)')).toEqual(486));
|
||||
it('sicp 8.2', () => expect(evaluate('(- 1000 334)')).toEqual(666));
|
||||
it('sicp 8.3', () => expect(evaluate('(* 5 99)')).toEqual(495));
|
||||
it('sicp 8.4', () => expect(evaluate('(/ 10 5)')).toEqual(2));
|
||||
it('sicp 8.5', () => expect(evaluate('(+ 2.7 10)')).toEqual(12.7));
|
||||
it('sicp 9.1', () => expect(evaluate('(+ 21 35 12 7)')).toEqual(75));
|
||||
it('sicp 9.2', () => expect(evaluate('(* 25 4 12)')).toEqual(1200));
|
||||
it('sicp 9.3', () => expect(evaluate('(+ (* 3 5) (- 10 6))')).toEqual(19));
|
||||
it('sicp 9.4', () =>
|
||||
expect(pretty('(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))')).toEqual(`(+
|
||||
(* 3
|
||||
(+
|
||||
(* 2 4)
|
||||
(+ 3 5)
|
||||
)
|
||||
)
|
||||
(+
|
||||
(- 10 7) 6
|
||||
)
|
||||
)`)); // this is not exactly pretty printing by convention..
|
||||
|
||||
let scope = {};
|
||||
it('sicp 11.1', () => expect(evaluate('(def size 2) (* 5 size)', scope)).toEqual(10));
|
||||
it('sicp 11.2', () =>
|
||||
expect(evaluate('(def pi 3.14159) (def radius 10) (* pi (* radius radius))', scope)).toEqual(314.159));
|
||||
it('sicp 11.3', () => expect(evaluate('(def circumference (* 2 pi radius))', scope)).toEqual(0));
|
||||
it('sicp 11.4', () => expect(evaluate('circumference', scope)).toEqual(62.8318));
|
||||
it('sicp 13.1', () => expect(evaluate('(* (+ 2 (* 4 6)) (+ 3 5 7))')).toEqual(390));
|
||||
it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0));
|
||||
// it('sicp 16.1', () => expect(evaluate('(def (square x) (* x x))', scope)).toEqual(0));
|
||||
it('sicp 17.1', () => expect(evaluate('(square 21)', scope)).toEqual(441));
|
||||
it('sicp 17.2', () => expect(evaluate('(square (+ 2 5))', scope)).toEqual(49));
|
||||
it('sicp 17.3', () => expect(evaluate('(square (square 3))', scope)).toEqual(81));
|
||||
it('sicp 17.4', () => expect(evaluate(`(def (sumofsquares x y) (+ (square x) (square y)))`, scope)).toEqual(0));
|
||||
it('sicp 17.5', () => expect(evaluate(`(sumofsquares 3 4)`, scope)).toEqual(25));
|
||||
it('sicp 17.6', () => expect(evaluate(`(def (f a) (sumofsquares (+ a 1) (* a 2))) (f 5)`, scope)).toEqual(136));
|
||||
it('sicp 21.1', () => expect(evaluate(`(sumofsquares (+ 5 1) (* 5 2))`, scope)).toEqual(136));
|
||||
|
||||
it('sicp 22.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`(def (abs x)
|
||||
(match
|
||||
((gt x 0) x)
|
||||
((eq x 0) 0)
|
||||
((lt x 0) (- 0 x))
|
||||
))`, // sicp was doing (- x), which doesnt work with our -
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
it('sicp gt1', () => expect(evaluate(`(gt -12 0)`, scope)).toEqual(false));
|
||||
it('sicp gt2', () => expect(evaluate(`(gt 0 -12)`, scope)).toEqual(true));
|
||||
it('sicp lt1', () => expect(evaluate(`(lt -12 0)`, scope)).toEqual(true));
|
||||
it('sicp lt2', () => expect(evaluate(`(lt 0 -12)`, scope)).toEqual(false));
|
||||
|
||||
it('sicp 24.1', () => expect(evaluate(`(abs (- 3))`, scope)).toEqual(3));
|
||||
it('sicp 24.2', () => expect(evaluate(`(abs (+ 3))`, scope)).toEqual(3));
|
||||
it('sicp 24.3', () => expect(evaluate(`(abs -12)`, scope)).toEqual(12));
|
||||
|
||||
it('sicp 24.4', () => expect(evaluate(`(def (abs x) (if (lt x 0) (- 0 x) x))`, scope)).toEqual(0));
|
||||
it('sicp 24.5', () => expect(evaluate(`(abs -13)`, scope)).toEqual(13));
|
||||
it('sicp 25.1', () => expect(evaluate(`(and (gt 6 5) (lt 6 10))`, scope)).toEqual(true));
|
||||
it('sicp 25.2', () => expect(evaluate(`(and (gt 4 5) (lt 6 10))`, scope)).toEqual(false));
|
||||
|
||||
it('sicp ex1.1.1', () => expect(evaluate(`(def a 3)`, scope)).toEqual(0));
|
||||
it('sicp ex1.1.2', () => expect(evaluate(`(def b (+ a 1))`, scope)).toEqual(0));
|
||||
it('sicp ex1.1.3', () => expect(evaluate(`(+ a b (* a b))`, scope)).toEqual(19));
|
||||
it('sicp ex1.1.4', () => expect(evaluate(`(if (and (gt b a) (lt b (* a b))) b a)`, scope)).toEqual(4));
|
||||
it('sicp ex1.1.5', () => expect(evaluate(`(match ((eq a 4) 6) ((eq b 4) (+ 6 7 a)) (else 25))`, scope)).toEqual(16));
|
||||
it('sicp ex1.1.6', () => expect(evaluate(`(+ 2 (if (gt b a) b a))`, scope)).toEqual(6));
|
||||
it('sicp ex1.1.7', () =>
|
||||
expect(evaluate(`(* (match ((gt a b) a) ((lt a b) b) (else -1)) (+ a 1))`, scope)).toEqual(16));
|
||||
|
||||
// .. cant use "+" and "-" as standalone expressions, because they are parsed as operators...
|
||||
it('sicp ex1.4.1', () => expect(evaluate(`(def (foo a b) ((if (gt b 0) add sub) a b))`, scope)).toEqual(0));
|
||||
it('sicp ex1.4.1', () => expect(evaluate(`(foo 3 1)`, scope)).toEqual(4));
|
||||
it('sicp ex1.4.2', () => expect(evaluate(`(foo 3 -1)`, scope)).toEqual(4));
|
||||
|
||||
// 1.1.7 Example: Square Roots by Newton’s Method
|
||||
it('sicp 30.1', () =>
|
||||
expect(evaluate(`(def (goodenuf guess x) (lt (abs (- (square guess) x)) 0.001))`, scope)).toEqual(0));
|
||||
it('sicp 30.2', () => expect(evaluate(`(goodenuf 1 1.001)`, scope)).toEqual(true));
|
||||
it('sicp 30.3', () => expect(evaluate(`(goodenuf 1 1.002)`, scope)).toEqual(false));
|
||||
it('sicp 30.4', () => expect(evaluate(`(def (average x y) (/ (+ x y) 2))`, scope)).toEqual(0));
|
||||
it('sicp 30.5', () => expect(evaluate(`(average 18 20)`, scope)).toEqual(19));
|
||||
it('sicp 30.6', () => expect(evaluate(`(def (improve guess x) (average guess (/ x guess)))`, scope)).toEqual(0));
|
||||
it('sicp 31.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`(def (sqrtiter guess x) (if (goodenuf guess x)
|
||||
guess
|
||||
(sqrtiter (improve guess x) x)))`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
it('sicp 31.2', () => expect(evaluate(`(def (sqrt x) (sqrtiter 1.0 x))`, scope)).toEqual(0));
|
||||
it('sicp 31.3', () => expect(evaluate(`(sqrt 9)`, scope)).toEqual(3.00009155413138));
|
||||
it('sicp 31.4', () => expect(evaluate(`(sqrt (+ 100 37))`, scope)).toEqual(11.704699917758145));
|
||||
// eslint-disable-next-line no-loss-of-precision
|
||||
it('sicp 31.5', () => expect(evaluate(`(sqrt (+ (sqrt 2) (sqrt 3)))`, scope)).toEqual(1.77392790232078925));
|
||||
it('sicp 31.6', () => expect(evaluate(`(square (sqrt 1000))`, scope)).toEqual(1000.000369924366));
|
||||
|
||||
// lexical scoping
|
||||
it('sicp 39.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (sqrt x)
|
||||
(def (goodenough guess)
|
||||
(lt (abs (- (square guess) x)) 0.001))
|
||||
(def (improve guess)
|
||||
(average guess (/ x guess)))
|
||||
(def (sqrt-iter guess)
|
||||
(if (goodenough guess) guess (sqrt-iter (improve guess))))
|
||||
(sqrtiter 1.0))
|
||||
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
// recursive fac
|
||||
it('sicp 41.1', () => expect(evaluate(`(def (fac n) (if (eq n 1) 1 (* n (fac (- n 1)))))`, scope)).toEqual(0));
|
||||
it('sicp 41.2', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24));
|
||||
|
||||
// iterative fac
|
||||
it('sicp 41.3', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (factorial n) (factiter 1 1 n))
|
||||
(def (factiter product counter maxcount)
|
||||
(if (gt counter maxcount)
|
||||
product
|
||||
(factiter (* counter product)
|
||||
(+ counter 1)
|
||||
maxcount)))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
it('sicp 41.4', () => expect(evaluate(`(fac 4)`, scope)).toEqual(24));
|
||||
|
||||
// 46.1
|
||||
/* (def (+ a b)
|
||||
(if (= a 0) b (inc (+ (dec a) b))))
|
||||
(def (+ a b)
|
||||
(if (= a 0) b (+ (dec a) (inc b)))) */
|
||||
|
||||
// Exercise 1.10
|
||||
// Ackermann’s function
|
||||
it('sicp 47.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (A x y) (match ((eq y 0) 0)
|
||||
((eq x 0) (* 2 y))
|
||||
((eq y 1) 2)
|
||||
(else (A (- x 1) (A x (- y 1))))))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
it('sicp 47.2', () => expect(evaluate(`(A 1 10)`, scope)).toEqual(1024));
|
||||
it('sicp 47.3', () => expect(evaluate(`(A 2 4)`, scope)).toEqual(65536));
|
||||
it('sicp 47.4', () => expect(evaluate(`(A 3 3)`, scope)).toEqual(65536));
|
||||
it('sicp 47.5', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (f n) (A 0 n))
|
||||
(def (g n) (A 1 n)))
|
||||
(def (h n) (A 2 n))
|
||||
(def (k n) (* 5 n n))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
// Tree Recursion
|
||||
// recursive process
|
||||
it('sicp 48.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (fib n) (match ((eq n 0) 0) ((eq n 1) 1)
|
||||
(else (+ (fib (- n 1)) (fib (- n 2))))))
|
||||
(fib 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(13));
|
||||
|
||||
// iterative process
|
||||
it('sicp 48.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (fib n) (fibiter 1 0 n))
|
||||
(def (fibiter a b count) (if (eq count 0)
|
||||
b
|
||||
(fibiter (+ a b) a (- count 1))))
|
||||
(fib 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(13));
|
||||
|
||||
// example: counting change
|
||||
it('sicp 52.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (countchange amount) (cc amount 5))
|
||||
(def (cc amount kindsofcoins)
|
||||
(match
|
||||
((eq amount 0) 1)
|
||||
((or (lt amount 0) (eq kindsofcoins 0)) 0)
|
||||
(else (+
|
||||
(cc amount (- kindsofcoins 1))
|
||||
(cc (- amount (firstdenomination kindsofcoins)) kindsofcoins)))))
|
||||
|
||||
(def (firstdenomination kindsofcoins)
|
||||
(match
|
||||
((eq kindsofcoins 1) 1)
|
||||
((eq kindsofcoins 2) 5)
|
||||
((eq kindsofcoins 3) 10)
|
||||
((eq kindsofcoins 4) 25)
|
||||
((eq kindsofcoins 5) 50)))
|
||||
|
||||
(countchange 100)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(292));
|
||||
|
||||
// todo: pascals triangle
|
||||
it('sicp 57.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (cube x) (* x x x))
|
||||
(def (p x) (sub (* 3 x) (* 4 (cube x))))
|
||||
(def (sine angle)
|
||||
(if (not (gt (abs angle) 0.1)) angle
|
||||
(p (sine (/ angle 3.0)))))
|
||||
|
||||
(sine 12.15)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(-0.39980345741334));
|
||||
|
||||
// exponentiation recursive
|
||||
it('sicp 57.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (expt b n) (if (eq n 0) 1 (* b (expt b (- n 1)))))
|
||||
(expt 2 4)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(16));
|
||||
|
||||
// exponentiation iterative
|
||||
it('sicp 58.1b', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (expt b n) (exptiter b n 1))
|
||||
(def (exptiter b counter product) (if (eq counter 0)
|
||||
product
|
||||
(exptiter b (- counter 1) (* b product))))
|
||||
(expt 2 5)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(32));
|
||||
|
||||
// exponentiation fast
|
||||
it('sicp 58.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (fastexpt b n) (match ((eq n 0) 1)
|
||||
((iseven n) (square (fastexpt b (/ n 2)))) (else (* b (fastexpt b (- n 1))))))
|
||||
(def (iseven n)
|
||||
(eq (mod n 2) 0))
|
||||
(fastexpt 2 5)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(32));
|
||||
|
||||
// * = repeated addition
|
||||
it('sicp 60.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`(def (mult a b) (if (eq b 0)
|
||||
0
|
||||
(+ a (* a (- b 1)))))
|
||||
(mult 3 15)
|
||||
`,
|
||||
),
|
||||
).toEqual(45));
|
||||
|
||||
// gcd / euclid
|
||||
it('sicp 63.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`(def (gcd a b) (if (eq b 0)
|
||||
a
|
||||
(gcd b (mod a b))))
|
||||
(gcd 20 6)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(2));
|
||||
|
||||
// 65 smallest divisor
|
||||
// 67 fermat test
|
||||
// ....
|
||||
|
||||
// higher order procedures
|
||||
|
||||
it('sicp 77.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (sum term a next b)
|
||||
(if (gt a b) 0 (+ (term a)
|
||||
(sum term (next a) next b))))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
it('sicp 78.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (inc n) (+ n 1))
|
||||
(def (cube a) (* a a a))
|
||||
(def (sumcubes a b)
|
||||
(sum cube a inc b))
|
||||
(sumcubes 1 10)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(3025));
|
||||
|
||||
it('sicp 78.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (identity x) x)
|
||||
(def (sumintegers a b)
|
||||
(sum identity a inc b))
|
||||
(sumintegers 1 10)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(55));
|
||||
|
||||
// pisum
|
||||
it('sicp 79.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (pisum a b)
|
||||
(def (piterm x)
|
||||
(/ 1.0 (* x (+ x 2))))
|
||||
(def (pinext x) (+ x 4))
|
||||
(sum piterm a pinext b))
|
||||
(* 8 (pisum 1 1000))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(3.139592655589783));
|
||||
|
||||
// integral
|
||||
it('sicp 79.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (integral f a b dx)
|
||||
(def (adddx x) (+ x dx))
|
||||
(* (sum f (+ a (/ dx 2.0)) adddx b) dx))
|
||||
(integral cube 0 1 0.01)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0.24998750000000042));
|
||||
// maximum callstack...
|
||||
//it('sicp 79.3', () => expect(evaluate(`(integral cube 0 1 0.001)`, scope)).toEqual(0.249999875000001));
|
||||
|
||||
//lambdas
|
||||
it('sicp 83.1', () => expect(evaluate(`((fn (x) (+ x 4)) 5)`)).toEqual(9));
|
||||
|
||||
it('sicp 83.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (pisum a b)
|
||||
(sum (fn (x) (/ 1.0 (* x (+ x 2))))
|
||||
a
|
||||
(fn (x) (+ x 4))
|
||||
b))
|
||||
(* 8 (pisum 1 1000))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(3.139592655589783));
|
||||
it('sicp 83.3', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (integral f a b dx)
|
||||
(* (sum f
|
||||
(+ a (/ dx 2.0))
|
||||
(fn (x) (+ x dx))
|
||||
b)
|
||||
dx))
|
||||
(integral cube 0 1 0.01)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0.24998750000000042));
|
||||
it('sicp 84.1', () => expect(evaluate(`((fn (x y z) (+ x y (square z))) 1 2 3)`, scope)).toEqual(12));
|
||||
|
||||
// let expressions
|
||||
it('sicp 87.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(+ (let ((x 3))
|
||||
(+ x (* x 10))) x)
|
||||
`,
|
||||
{ x: 5 },
|
||||
),
|
||||
).toEqual(38));
|
||||
it('sicp 87.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(let ((x 3)
|
||||
(y (+ x 2)))
|
||||
(* x y))
|
||||
`,
|
||||
{ x: 2 },
|
||||
),
|
||||
).toEqual(12));
|
||||
it('sicp 88.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (f g) (g 2))
|
||||
(f square)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(4));
|
||||
it('sicp 88.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (f g) (g 2))
|
||||
(f (fn (z) (* z (+ z 1))))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(6));
|
||||
|
||||
// Finding roots of equations by the half-interval method
|
||||
it('sicp 89.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (search f negpoint pospoint)
|
||||
(let ((midpoint (average negpoint pospoint)))
|
||||
(if (closeenough negpoint pospoint)
|
||||
midpoint
|
||||
(let ((testvalue (f midpoint)))
|
||||
(match ((positive testvalue)
|
||||
(search f negpoint midpoint))
|
||||
((negative testvalue)
|
||||
(search f midpoint pospoint))
|
||||
(else midpoint))))))
|
||||
|
||||
(def (closeenough x y) (lt (abs (- x y)) 0.001))
|
||||
|
||||
(def (negative x) (lt x 0))
|
||||
(def (positive x) (gt x 0))
|
||||
|
||||
(def (halfintervalmethod f a b) (let ((avalue (f a))
|
||||
(bvalue (f b)))
|
||||
(match ((and (negative avalue) (positive bvalue))
|
||||
(search f a b))
|
||||
((and (negative bvalue) (positive avalue))
|
||||
(search f b a)) (else
|
||||
(error "Values are not of opposite sign" a b)))))
|
||||
|
||||
(halfintervalmethod sin 2.0 4.0)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(3.14111328125));
|
||||
|
||||
it('sicp 89.1', () =>
|
||||
expect(evaluate(`(halfintervalmethod (fn (x) (- (* x x x) (* 2 x) 3)) 1.0 2.0)`, scope)).toEqual(1.89306640625));
|
||||
|
||||
// Finding fixed points of functions
|
||||
it('sicp 92.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def tolerance 0.00001)
|
||||
(def (fixedpoint f first-guess)
|
||||
(def (closeenough v1 v2) (lt (abs (- v1 v2)) tolerance))
|
||||
(def (try guess)
|
||||
(let ((next (f guess)))
|
||||
(if (closeenough guess next) next (try next))))
|
||||
(try first-guess))
|
||||
|
||||
(fixedpoint cos 1.0)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0.7390822985224023));
|
||||
it('sicp 93.1', () =>
|
||||
expect(evaluate(`(fixedpoint (fn (y) (+ (sin y) (cos y))) 1.0)`, scope)).toEqual(1.2587315962971173));
|
||||
// Maximum call stack size exceeded (expected)
|
||||
/* it('sicp 93.2', () =>
|
||||
expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (/ x y)) 1.0)) (sqrt 4)`, scope)).toEqual(0)); */
|
||||
it('sicp 93.3', () =>
|
||||
expect(evaluate(`(def (sqrt x) (fixedpoint (fn (y) (average y (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual(
|
||||
2.6457513110645907,
|
||||
));
|
||||
// Procedures as Returned Values
|
||||
it('sicp 97.1', () =>
|
||||
expect(evaluate(`(def (averagedamp f) (fn (x) (average x (f x)))) ((averagedamp square) 10)`, scope)).toEqual(55));
|
||||
it('sicp 98.1', () =>
|
||||
expect(evaluate(`(def (sqrt x) (fixedpoint (averagedamp (fn (y) (/ x y))) 1.0)) (sqrt 7)`, scope)).toEqual(
|
||||
2.6457513110645907,
|
||||
));
|
||||
it('sicp 98.2', () =>
|
||||
expect(
|
||||
evaluate(`(def (cuberoot x) (fixedpoint (averagedamp (fn (y) (/ x (square y)))) 1.0)) (cuberoot 7)`, scope),
|
||||
).toEqual(1.912934258514886));
|
||||
it('sicp 99.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (deriv g) (fn (x) (/ (- (g (+ x dx)) (g x)) dx)))
|
||||
(def dx 0.00001)
|
||||
(def (cube x) (* x x x))
|
||||
((deriv cube) 5)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(75.00014999664018));
|
||||
// With the aid of deriv, we can express Newton’s method as a fixed-point process:
|
||||
it('sicp 100.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (newtontransform g)
|
||||
(fn (x) (- x (/ (g x) ((deriv g) x)))))
|
||||
(def (newtonsmethod g guess) (fixedpoint (newtontransform g) guess))
|
||||
(def (sqrt x) (newtonsmethod (fn (y) (- (square y) x)) 1.0))
|
||||
(sqrt 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(2.6457513110645907));
|
||||
// whatever this is
|
||||
it('sicp 101.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (fixedpointoftransform g transform guess) (fixedpoint (transform g) guess))
|
||||
(def (sqrt x) (fixedpointoftransform
|
||||
(fn (y) (/ x y)) averagedamp 1.0))
|
||||
(sqrt 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(2.6457513110645907));
|
||||
it('sicp 101.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (sqrt x) (fixedpointoftransform
|
||||
(fn (y) (- (square y) x)) newtontransform 1.0))
|
||||
(sqrt 7)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(2.6457513110645907));
|
||||
|
||||
// data abstraction
|
||||
|
||||
// rational arithmetic
|
||||
it('sicp 114.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (addrat x y)
|
||||
(makerat (+ (*
|
||||
(numer x) (denom y))
|
||||
(* (numer y) (denom x)))
|
||||
(* (denom x) (denom y))))
|
||||
(def (subrat x y)
|
||||
(makerat (- (* (numer x) (denom y))
|
||||
(* (numer y) (denom x)))
|
||||
(* (denom x) (denom y))))
|
||||
(def (mulrat x y)
|
||||
(makerat (* (numer x) (numer y))
|
||||
(* (denom x) (denom y))))
|
||||
(def (divrat x y)
|
||||
(makerat (* (numer x) (denom y))
|
||||
(* (denom x) (numer y))))
|
||||
(def (equalrat x y)
|
||||
(eq (* (numer x) (denom y))
|
||||
(* (numer y) (denom x))))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
// markerat number denom
|
||||
it('sicp 117.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (makerat n d) (cons n d))
|
||||
(def (numer x) (car x))
|
||||
(def (denom x) (cdr x))
|
||||
(def (printrat x) (concat (numer x) ':' (denom x)))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(0));
|
||||
|
||||
it('sicp 117.1', () => expect(evaluate(`(def onehalf (makerat 1 2)) (printrat onehalf)`, scope)).toEqual('1:2'));
|
||||
it('sicp 117.2', () =>
|
||||
expect(evaluate(`(def onethird (makerat 1 3)) (printrat (addrat onehalf onethird))`, scope)).toEqual('5:6'));
|
||||
it('sicp 117.3', () => expect(evaluate(`(printrat (mulrat onehalf onethird))`, scope)).toEqual('1:6'));
|
||||
it('sicp 117.4', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('6:9'));
|
||||
it('sicp 118.1', () =>
|
||||
expect(evaluate(`(def (makerat n d) (let ((g (gcd n d))) (cons (/ n g) (/ d g))))`, scope)).toEqual(0));
|
||||
it('sicp 118.1', () => expect(evaluate(`(printrat (addrat onethird onethird))`, scope)).toEqual('2:3'));
|
||||
|
||||
let lscope = {};
|
||||
// pairs with lambda
|
||||
it('sicp 124.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (cons x y)
|
||||
(def (dispatch m)
|
||||
(match
|
||||
((eq m 0) x)
|
||||
((eq m 1) y)
|
||||
(else (error "argument not 0 or 1: CONS" m)))
|
||||
) dispatch)
|
||||
(def (car z) (z 0))
|
||||
(def (cdr z) (z 1))
|
||||
`,
|
||||
lscope,
|
||||
),
|
||||
).toEqual(0));
|
||||
it('sicp 124.1', () => expect(evaluate(`(car (cons first second))`, lscope)).toEqual('first'));
|
||||
it('sicp 124.2', () => expect(evaluate(`(cdr (cons first second))`, lscope)).toEqual('second'));
|
||||
// lists
|
||||
it('sicp 135.1', () => expect(evaluate(`(list 1 2 3 4)`)).toEqual([1, 2, 3, 4]));
|
||||
it('sicp 137.1', () => expect(evaluate(`(car (list 1 2 3 4))`)).toEqual(1));
|
||||
it('sicp 137.2', () => expect(evaluate(`(cdr (list 1 2 3 4))`)).toEqual([2, 3, 4]));
|
||||
it('sicp 137.3', () => expect(evaluate(`(car (cdr (list 1 2 3 4)))`)).toEqual(2));
|
||||
it('sicp 137.4', () => expect(evaluate(`(cons 10 (list 1 2 3 4))`)).toEqual([10, 1, 2, 3, 4]));
|
||||
// listref
|
||||
it('sicp 138.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (listref items n) (if (eq n 0) (car items)
|
||||
(listref (cdr items) (- n 1))))
|
||||
(def squares (list 1 4 9 16 25))
|
||||
(listref squares 3)`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(16));
|
||||
// length recursive
|
||||
it('sicp 138.2', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (length items)
|
||||
(if (isnull items) 0
|
||||
(+ 1 (length (cdr items)))))
|
||||
(def odds (list 1 3 5 7))
|
||||
(length odds)`,
|
||||
),
|
||||
).toEqual(4));
|
||||
// length iterative
|
||||
it('sicp 139.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (length items)
|
||||
(def (lengthiter a count)
|
||||
(if (isnull a) count
|
||||
(lengthiter (cdr a) (+ 1 count))))
|
||||
(lengthiter items 0))
|
||||
(def odds (list 1 3 5 7))
|
||||
(length odds)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual(4));
|
||||
// append
|
||||
it('sicp 139.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (append list1 list2)
|
||||
(if (isnull list1)
|
||||
list2
|
||||
(cons (car list1) (append (cdr list1) list2))))
|
||||
(append squares odds)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual([1, 4, 9, 16, 25, 1, 3, 5, 7]));
|
||||
// (define (f x y . z) ⟨body⟩) <- tbd: variable argument count
|
||||
// Mapping over lists
|
||||
|
||||
it('sicp 143.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (scalelist items factor) (if (isnull items) nil
|
||||
(cons (* (car items) factor)
|
||||
(scalelist (cdr items) factor))))
|
||||
(scalelist (list 1 2 3 4 5) 10)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual([10, 20, 30, 40, 50]));
|
||||
|
||||
it('sicp 143.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (map proc items) (if (isnull items) nil
|
||||
(cons (proc (car items))
|
||||
(map proc (cdr items)))))
|
||||
(map abs (list -10 2.5 -11.6 17))
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual([10, 2.5, 11.6, 17]));
|
||||
it('sicp 143.1', () => expect(evaluate(`(map (fn (x) (* x x)) (list 1 2 3 4))`, scope)).toEqual([1, 4, 9, 16]));
|
||||
it('sicp 143.1', () =>
|
||||
expect(
|
||||
evaluate(
|
||||
`
|
||||
(def (scalelist items factor) (map (fn (x) (* x factor)) items))
|
||||
(scalelist (list 1 2 3 4 5) 10)
|
||||
`,
|
||||
scope,
|
||||
),
|
||||
).toEqual([10, 20, 30, 40, 50]));
|
||||
});
|
||||
@@ -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, 'mondo.mjs'),
|
||||
formats: ['es'],
|
||||
fileName: (ext) => ({ es: 'mondo.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
// external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
# @strudel/mondough
|
||||
|
||||
connects mondo to strudel.
|
||||
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
strudelScope,
|
||||
reify,
|
||||
fast,
|
||||
slow,
|
||||
seq,
|
||||
stepcat,
|
||||
extend,
|
||||
expand,
|
||||
pace,
|
||||
chooseIn,
|
||||
degradeBy,
|
||||
silence,
|
||||
} from '@strudel/core';
|
||||
import { registerLanguage } from '@strudel/transpiler';
|
||||
import { MondoRunner } from 'mondolang';
|
||||
|
||||
const tail = (friend, pat) => pat.fmap((a) => (b) => (Array.isArray(a) ? [...a, b] : [a, b])).appLeft(friend);
|
||||
|
||||
const arrayRange = (start, stop, step = 1) =>
|
||||
Array.from({ length: Math.abs(stop - start) / step + 1 }, (_, index) =>
|
||||
start < stop ? start + index * step : start - index * step,
|
||||
);
|
||||
const range = (max, min) => min.squeezeBind((a) => max.bind((b) => seq(...arrayRange(a, b))));
|
||||
|
||||
let nope = (...args) => args[args.length - 1];
|
||||
|
||||
let lib = {};
|
||||
lib['nope'] = nope;
|
||||
lib['-'] = (a, b) => b.early(a);
|
||||
lib['+'] = (a, b) => b.late(a);
|
||||
lib['_'] = silence;
|
||||
lib['~'] = silence;
|
||||
lib.curly = stepcat;
|
||||
lib.square = (...args) => stepcat(...args).setSteps(1);
|
||||
lib.angle = (...args) => stepcat(...args).pace(1);
|
||||
lib['*'] = fast;
|
||||
lib['/'] = slow;
|
||||
lib['!'] = extend;
|
||||
lib['@'] = expand;
|
||||
lib['%'] = pace;
|
||||
lib['?'] = degradeBy; // todo: default 0.5 not working..
|
||||
lib[':'] = tail;
|
||||
lib['..'] = range;
|
||||
lib['def'] = () => silence;
|
||||
lib['or'] = (...children) => chooseIn(...children); // always has structure but is cyclewise.. e.g. "s oh*8.dec[.04 | .5]"
|
||||
//lib['or'] = (...children) => chooseOut(...children); // "s oh*8.dec[.04 | .5]" is better but "dec[.04 | .5].s oh*8" has no struct
|
||||
|
||||
function evaluator(node, scope) {
|
||||
const { type } = node;
|
||||
// node is list
|
||||
if (type === 'list') {
|
||||
const { children } = node;
|
||||
const [name, ...args] = children;
|
||||
// some functions wont be reified to make sure they work (e.g. see extend below)
|
||||
if (typeof name === 'function') {
|
||||
return name(...args);
|
||||
}
|
||||
if (name.value === 'def') {
|
||||
return silence;
|
||||
}
|
||||
// name is expected to be a pattern of functions!
|
||||
const first = name.firstCycle(true)[0];
|
||||
const type = typeof first?.value;
|
||||
if (type !== 'function') {
|
||||
throw new Error(`[mondough] expected function, got "${first?.value}"`);
|
||||
}
|
||||
return name
|
||||
.fmap((fn) => {
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`[mondough] "${fn}" is not a function b`);
|
||||
}
|
||||
return fn(...args);
|
||||
})
|
||||
.innerJoin();
|
||||
}
|
||||
// node is leaf
|
||||
let { value } = node;
|
||||
if (type === 'plain' && scope[value]) {
|
||||
return reify(scope[value]); // -> local scope has no location
|
||||
}
|
||||
const variable = lib[value] ?? strudelScope[value];
|
||||
// problem: collisions when we want a string that happens to also be a variable name
|
||||
// example: "s sine" -> sine is also a variable
|
||||
let pat;
|
||||
if (type === 'plain' && typeof variable !== 'undefined') {
|
||||
// some function names are not patternable, so we skip reification here
|
||||
if (['!', 'extend', '@', 'expand', 'square', 'angle'].includes(value)) {
|
||||
return variable;
|
||||
}
|
||||
pat = reify(variable);
|
||||
} else {
|
||||
pat = reify(value);
|
||||
}
|
||||
if (node.loc) {
|
||||
pat = pat.withLoc(node.loc[0], node.loc[1]);
|
||||
}
|
||||
return pat;
|
||||
}
|
||||
|
||||
let runner = new MondoRunner({ evaluator });
|
||||
|
||||
export function mondo(code, offset = 0) {
|
||||
if (Array.isArray(code)) {
|
||||
code = code.join('');
|
||||
}
|
||||
const pat = runner.run(code, undefined, offset);
|
||||
return pat.markcss('color: var(--caret,--foreground);text-decoration:underline');
|
||||
}
|
||||
|
||||
export let getLocations = (code, offset) => runner.parser.get_locations(code, offset);
|
||||
|
||||
export const mondi = (str, offset) => {
|
||||
const code = `[${str}]`;
|
||||
return mondo(code, offset);
|
||||
};
|
||||
|
||||
// tell transpiler how to get locations for mondo`` calls
|
||||
registerLanguage('mondo', {
|
||||
getLocations,
|
||||
});
|
||||
|
||||
// this is like mondo, but with a zero offset
|
||||
export const mondolang = (code) => mondo(code, 0);
|
||||
registerLanguage('mondolang', {
|
||||
getLocations: (code) => getLocations(code, 0),
|
||||
});
|
||||
// uncomment the following to use mondo as mini notation language
|
||||
/* registerLanguage('minilang', {
|
||||
name: 'mondi',
|
||||
getLocations,
|
||||
}); */
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@strudel/mondo",
|
||||
"version": "1.1.4",
|
||||
"description": "mondo notation for strudel",
|
||||
"main": "mondough.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/mondough.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"bench": "vitest bench",
|
||||
"build:parser": "peggy -o krill-parser.js --format es ./krill.pegjs",
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
"mondolang": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mondo": "*",
|
||||
"vite": "^6.0.11",
|
||||
"vitest": "^3.0.4"
|
||||
}
|
||||
}
|
||||
@@ -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, 'mondough.mjs'),
|
||||
formats: ['es'],
|
||||
fileName: (ext) => ({ es: 'mondough.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/motion",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"description": "DeviceMotion API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mqtt",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"description": "MQTT API for strudel",
|
||||
"main": "mqtt.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -60,7 +60,7 @@ export function parseControlsFromHap(hap, cps) {
|
||||
|
||||
const collator = new ClockCollator({});
|
||||
|
||||
export async function oscTrigger(t_deprecate, hap, currentTime, cps = 1, targetTime) {
|
||||
export async function oscTrigger(hap, currentTime, cps = 1, targetTime) {
|
||||
const osc = await connect();
|
||||
const controls = parseControlsFromHap(hap, cps);
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/osc",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
||||
import { isTauri } from '../desktopbridge/utils.mjs';
|
||||
/* import { oscTriggerTauri } from '../desktopbridge/oscbridge.mjs';
|
||||
import { isTauri } from '../desktopbridge/utils.mjs'; */
|
||||
import { oscTrigger } from './osc.mjs';
|
||||
|
||||
const trigger = isTauri() ? oscTriggerTauri : oscTrigger;
|
||||
const trigger = /* isTauri() ? oscTriggerTauri : */ oscTrigger;
|
||||
|
||||
export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => {
|
||||
const currentTime = performance.now() / 1000;
|
||||
return trigger(null, hap, currentTime, cps, targetTime);
|
||||
return trigger(hap, currentTime, cps, targetTime);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/reference",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"description": "Headless reference of all strudel functions",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/repl",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.6",
|
||||
"description": "Strudel REPL as a Web Component",
|
||||
"module": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function prebake() {
|
||||
samples(`${ds}/tidal-drum-machines.json`),
|
||||
samples(`${ds}/piano.json`),
|
||||
samples(`${ds}/Dirt-Samples.json`),
|
||||
samples(`${ds}/EmuSP12.json`),
|
||||
samples(`${ds}/uzu-drumkit.json`),
|
||||
samples(`${ds}/vcsl.json`),
|
||||
samples(`${ds}/mridangam.json`),
|
||||
]);
|
||||
|
||||
@@ -20,3 +20,13 @@ samples('http://localhost:5432')
|
||||
LOG=1 npx @strudel/sampler # adds logging
|
||||
PORT=5555 npx @strudel/sampler # changes port
|
||||
```
|
||||
|
||||
## static json
|
||||
|
||||
when running with `--json`, you will simply get the json logged back:
|
||||
|
||||
```sh
|
||||
npx --yes @strudel/sampler --json > strudel.json
|
||||
```
|
||||
|
||||
this is useful if you want to create a sample pack from the current folder.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/sampler",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.3",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import cowsay from 'cowsay';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { createReadStream, existsSync, writeFileSync } from 'fs';
|
||||
import { readdir } from 'fs/promises';
|
||||
import http from 'http';
|
||||
import { join, sep } from 'path';
|
||||
import { join, resolve, sep } from 'path';
|
||||
import readline from 'readline';
|
||||
import os from 'os';
|
||||
|
||||
// eslint-disable-next-line
|
||||
const LOG = !!process.env.LOG || false;
|
||||
const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg'];
|
||||
|
||||
console.log(
|
||||
cowsay.say({
|
||||
text: 'welcome to @strudel/sampler',
|
||||
e: 'oO',
|
||||
T: 'U ',
|
||||
}),
|
||||
);
|
||||
const isAudioFile = (f) => {
|
||||
const ext = f.split('.').slice(-1)[0].toLowerCase();
|
||||
return VALID_AUDIO_EXTENSIONS.includes(ext);
|
||||
};
|
||||
|
||||
async function getFilesInDirectory(directory) {
|
||||
let files = [];
|
||||
@@ -29,42 +27,90 @@ async function getFilesInDirectory(directory) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const subFiles = (await getFilesInDirectory(fullPath)).filter((f) =>
|
||||
['wav', 'mp3', 'ogg'].includes(f.split('.').slice(-1)[0].toLowerCase()),
|
||||
);
|
||||
const subFiles = (await getFilesInDirectory(fullPath)).filter(isAudioFile);
|
||||
files = files.concat(subFiles);
|
||||
LOG && console.log(`${dirent.name} (${subFiles.length})`);
|
||||
} catch (err) {
|
||||
LOG && console.warn(`skipped due to error: ${fullPath}`);
|
||||
}
|
||||
} else {
|
||||
files.push(fullPath);
|
||||
isAudioFile(fullPath) && files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
async function getBanks(directory) {
|
||||
async function getBanks(directory, flat = false) {
|
||||
let files = await getFilesInDirectory(directory);
|
||||
let banks = {};
|
||||
directory = directory.split(sep).join('/');
|
||||
files = files.map((path) => {
|
||||
path = path.split(sep).join('/');
|
||||
const [bank] = path.split('/').slice(-2);
|
||||
const subDir = path.replace(directory, '');
|
||||
const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore
|
||||
const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension
|
||||
let bank = flat ? subDirFlatStem : path.split('/').slice(-2)[0];
|
||||
banks[bank] = banks[bank] || [];
|
||||
const relativeUrl = path.replace(directory, '');
|
||||
banks[bank].push(relativeUrl);
|
||||
return relativeUrl;
|
||||
banks[bank].push(subDir);
|
||||
return subDir;
|
||||
});
|
||||
banks._base = `http://localhost:5432`;
|
||||
return { banks, files };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
const directory = process.cwd();
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function getArgValue(flag) {
|
||||
const i = args.indexOf(flag);
|
||||
if (i !== -1) {
|
||||
const nextIsFlag = args[i + 1]?.startsWith('--') ?? true;
|
||||
if (nextIsFlag) return true;
|
||||
return args[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
function getInput(query) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) =>
|
||||
rl.question(query, (response) => {
|
||||
rl.close();
|
||||
resolve(response);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let directory = getArgValue('--dir') || process.cwd();
|
||||
directory = resolve(directory);
|
||||
if (args.includes('--json')) {
|
||||
const { banks } = await getBanks(directory, getArgValue('--flat'));
|
||||
const json = JSON.stringify(banks);
|
||||
const outFile = resolve(directory, 'strudel.json');
|
||||
if (existsSync(outFile)) {
|
||||
const answer = await getInput(`Warning: File already exists at ${outFile}. Overwrite? (y/N): `);
|
||||
if (answer.toLowerCase() !== 'y') {
|
||||
console.log('Aborted.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
writeFileSync(outFile, json, 'utf8');
|
||||
console.log(`Wrote json to ${outFile}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
cowsay.say({
|
||||
text: 'welcome to @strudel/sampler',
|
||||
e: 'oO',
|
||||
T: 'U ',
|
||||
}),
|
||||
);
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
const { banks, files } = await getBanks(directory);
|
||||
const { banks, files } = await getBanks(directory, getArgValue('--flat'));
|
||||
if (req.url === '/') {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
return res.end(JSON.stringify(banks));
|
||||
@@ -72,7 +118,7 @@ const server = http.createServer(async (req, res) => {
|
||||
let subpath = decodeURIComponent(req.url);
|
||||
const filePath = join(directory, subpath.split('/').join(sep));
|
||||
|
||||
//console.log('GET:', filePath);
|
||||
// console.log('GET:', filePath);
|
||||
const isFound = existsSync(filePath);
|
||||
if (!isFound) {
|
||||
res.statusCode = 404;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/serial",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"description": "Webserial API for strudel",
|
||||
"main": "serial.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -537,7 +537,7 @@ export default {
|
||||
],
|
||||
gm_synth_bass_1: [
|
||||
// Synth Bass 1: Bass
|
||||
'0380_Aspirin_sf2_file',
|
||||
// '0380_Aspirin_sf2_file', // broken in safari https://codeberg.org/uzu/strudel/issues/1384
|
||||
'0380_Chaos_sf2_file',
|
||||
'0380_FluidR3_GM_sf2_file',
|
||||
// 0380_GeneralUserGS_sf2_file // laut
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/soundfonts",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.5",
|
||||
"description": "Soundsfont support for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getAudioContext, registerSound } from '@strudel/webaudio';
|
||||
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
||||
|
||||
Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||
return this.onTrigger((time_deprecate, h, ct, cps, targetTime) => {
|
||||
return this.onTrigger((h, ct, cps, targetTime) => {
|
||||
const ctx = getAudioContext();
|
||||
const note = getPlayableNoteValue(h);
|
||||
const preset = sf.presets[n % sf.presets.length];
|
||||
|
||||
@@ -74,6 +74,6 @@ export const dough = async (code) => {
|
||||
worklet.node.connect(ac.destination);
|
||||
};
|
||||
|
||||
export function doughTrigger(time_deprecate, hap, currentTime, cps, targetTime) {
|
||||
export function doughTrigger(hap, currentTime, cps, targetTime) {
|
||||
window.postMessage({ time: targetTime, dough: hap.value, currentTime, duration: hap.duration, cps });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { getAudioContext } from './superdough.mjs';
|
||||
import { clamp, nanFallback } from './util.mjs';
|
||||
import { getNoiseBuffer } from './noise.mjs';
|
||||
|
||||
export const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||
|
||||
export function gainNode(value) {
|
||||
const node = getAudioContext().createGain();
|
||||
@@ -171,7 +174,7 @@ let curves = ['linear', 'exponential'];
|
||||
export function getPitchEnvelope(param, value, t, holdEnd) {
|
||||
// envelope is active when any of these values is set
|
||||
const hasEnvelope = value.pattack ?? value.pdecay ?? value.psustain ?? value.prelease ?? value.penv;
|
||||
if (!hasEnvelope) {
|
||||
if (hasEnvelope === undefined) {
|
||||
return;
|
||||
}
|
||||
const penv = nanFallback(value.penv, 1, true);
|
||||
@@ -206,19 +209,46 @@ export function getVibratoOscillator(param, value, t) {
|
||||
// ConstantSource inherits AudioScheduledSourceNode, which has scheduling abilities
|
||||
// a bit of a hack, but it works very well :)
|
||||
export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
||||
const constantNode = audioContext.createConstantSource();
|
||||
constantNode.start(startTime);
|
||||
constantNode.stop(stopTime);
|
||||
const constantNode = new ConstantSourceNode(audioContext);
|
||||
|
||||
// Certain browsers requires audio nodes to be connected in order for their onended events
|
||||
// to fire, so we _mute it_ and then connect it to the destination
|
||||
const zeroGain = gainNode(0);
|
||||
zeroGain.connect(audioContext.destination);
|
||||
constantNode.connect(zeroGain);
|
||||
|
||||
// Schedule the `onComplete` callback to occur at `stopTime`
|
||||
constantNode.onended = () => {
|
||||
// Ensure garbage collection
|
||||
try {
|
||||
zeroGain.disconnect();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
try {
|
||||
constantNode.disconnect();
|
||||
} catch {
|
||||
// pass
|
||||
}
|
||||
onComplete();
|
||||
};
|
||||
constantNode.start(startTime);
|
||||
constantNode.stop(stopTime);
|
||||
return constantNode;
|
||||
}
|
||||
const mod = (freq, range = 1, type = 'sine') => {
|
||||
const ctx = getAudioContext();
|
||||
const osc = ctx.createOscillator();
|
||||
osc.type = type;
|
||||
osc.frequency.value = freq;
|
||||
let osc;
|
||||
if (noises.includes(type)) {
|
||||
osc = ctx.createBufferSource();
|
||||
osc.buffer = getNoiseBuffer(type, 2);
|
||||
osc.loop = true;
|
||||
} else {
|
||||
osc = ctx.createOscillator();
|
||||
osc.type = type;
|
||||
osc.frequency.value = freq;
|
||||
}
|
||||
|
||||
osc.start();
|
||||
const g = new GainNode(ctx, { gain: range });
|
||||
osc.connect(g); // -range, range
|
||||
@@ -253,7 +283,7 @@ export function applyFM(param, value, begin) {
|
||||
|
||||
modulator = fmmod.node;
|
||||
stop = fmmod.stop;
|
||||
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].find((v) => v !== undefined)) {
|
||||
if (![fmAttack, fmDecay, fmSustain, fmRelease, fmVelocity].some((v) => v !== undefined)) {
|
||||
// no envelope by default
|
||||
modulator.connect(param);
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
let log = (msg) => console.log(msg);
|
||||
|
||||
export function errorLogger(e, origin = 'cyclist') {
|
||||
//TODO: add some kind of debug flag that enables this while in dev mode
|
||||
// console.error(e);
|
||||
logger(`[${origin}] error: ${e.message}`);
|
||||
}
|
||||
|
||||
export const logger = (...args) => log(...args);
|
||||
|
||||
export const setLogger = (fn) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getAudioContext } from './superdough.mjs';
|
||||
let noiseCache = {};
|
||||
|
||||
// lazy generates noise buffers and keeps them forever
|
||||
function getNoiseBuffer(type, density) {
|
||||
export function getNoiseBuffer(type, density) {
|
||||
const ac = getAudioContext();
|
||||
if (noiseCache[type]) {
|
||||
return noiseCache[type];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superdough",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.5",
|
||||
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import reverbGen from './reverbGen.mjs';
|
||||
import { clamp } from './util.mjs';
|
||||
|
||||
if (typeof AudioContext !== 'undefined') {
|
||||
AudioContext.prototype.adjustLength = function (duration, buffer) {
|
||||
AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) {
|
||||
const sampleOffset = Math.floor(clamp(offsetAmount, 0, 1) * buffer.length);
|
||||
const newLength = buffer.sampleRate * duration;
|
||||
const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate);
|
||||
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
|
||||
@@ -9,22 +11,30 @@ if (typeof AudioContext !== 'undefined') {
|
||||
let newData = newBuffer.getChannelData(channel);
|
||||
|
||||
for (let i = 0; i < newLength; i++) {
|
||||
newData[i] = oldData[i] || 0;
|
||||
// loop the buffer around to prevent
|
||||
let position = (sampleOffset + i * Math.abs(speed)) % oldData.length;
|
||||
if (speed < 1) {
|
||||
position = position * -1;
|
||||
}
|
||||
|
||||
newData[i] = oldData.at(position) || 0;
|
||||
}
|
||||
}
|
||||
return newBuffer;
|
||||
};
|
||||
|
||||
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir) {
|
||||
AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) {
|
||||
const convolver = this.createConvolver();
|
||||
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir) => {
|
||||
convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir, irspeed, irbegin) => {
|
||||
convolver.duration = d;
|
||||
convolver.fade = fade;
|
||||
convolver.lp = lp;
|
||||
convolver.dim = dim;
|
||||
convolver.ir = ir;
|
||||
convolver.irspeed = irspeed;
|
||||
convolver.irbegin = irbegin;
|
||||
if (ir) {
|
||||
convolver.buffer = this.adjustLength(d, ir);
|
||||
convolver.buffer = this.adjustLength(d, ir, irspeed, irbegin);
|
||||
} else {
|
||||
reverbGen.generateReverb(
|
||||
{
|
||||
@@ -41,7 +51,7 @@ if (typeof AudioContext !== 'undefined') {
|
||||
);
|
||||
}
|
||||
};
|
||||
convolver.generate(duration, fade, lp, dim, ir);
|
||||
convolver.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
|
||||
return convolver;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,13 +78,23 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
||||
const bufferSource = ac.createBufferSource();
|
||||
bufferSource.buffer = buffer;
|
||||
bufferSource.playbackRate.value = playbackRate;
|
||||
|
||||
const { s, loopBegin = 0, loopEnd = 1, begin = 0, end = 1 } = hapValue;
|
||||
|
||||
const { s, loopBegin = 0, loopEnd = 1, begin, end, beginSeconds, endSeconds } = hapValue;
|
||||
let offset = 0;
|
||||
let endTime = bufferSource.buffer.duration;
|
||||
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
||||
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
||||
// the midway point through a 10-second audio buffer is still 5."
|
||||
const offset = begin * bufferSource.buffer.duration;
|
||||
if (begin != null) {
|
||||
offset = begin * bufferSource.buffer.duration;
|
||||
} else if (beginSeconds != null) {
|
||||
offset = beginSeconds;
|
||||
}
|
||||
|
||||
if (end != null) {
|
||||
endTime = end * bufferSource.buffer.duration;
|
||||
} else if (endSeconds != null) {
|
||||
endTime = endSeconds;
|
||||
}
|
||||
|
||||
const loop = s.startsWith('wt_') ? 1 : hapValue.loop;
|
||||
if (loop) {
|
||||
@@ -93,7 +103,7 @@ export const getSampleBufferSource = async (hapValue, bank, resolveUrl) => {
|
||||
bufferSource.loopEnd = loopEnd * bufferSource.buffer.duration - offset;
|
||||
}
|
||||
const bufferDuration = bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||
const sliceDuration = (end - begin) * bufferDuration;
|
||||
const sliceDuration = (endTime - offset) / bufferSource.playbackRate.value;
|
||||
return { bufferSource, offset, bufferDuration, sliceDuration };
|
||||
};
|
||||
|
||||
@@ -196,6 +206,52 @@ function getSamplesPrefixHandler(url) {
|
||||
return;
|
||||
}
|
||||
|
||||
export async function fetchSampleMap(url) {
|
||||
// check if custom prefix handler
|
||||
const handler = getSamplesPrefixHandler(url);
|
||||
if (handler) {
|
||||
return handler(url);
|
||||
}
|
||||
url = resolveSpecialPaths(url);
|
||||
if (url.startsWith('github:')) {
|
||||
url = githubPath(url, 'strudel.json');
|
||||
}
|
||||
if (url.startsWith('local:')) {
|
||||
url = `http://localhost:5432`;
|
||||
}
|
||||
if (url.startsWith('shabda:')) {
|
||||
let [_, path] = url.split('shabda:');
|
||||
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
|
||||
}
|
||||
if (url.startsWith('shabda/speech')) {
|
||||
let [_, path] = url.split('shabda/speech');
|
||||
path = path.startsWith('/') ? path.substring(1) : path;
|
||||
let [params, words] = path.split(':');
|
||||
let gender = 'f';
|
||||
let language = 'en-GB';
|
||||
if (params) {
|
||||
[language, gender] = params.split('/');
|
||||
}
|
||||
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
|
||||
}
|
||||
if (typeof fetch !== 'function') {
|
||||
// not a browser
|
||||
return;
|
||||
}
|
||||
const base = url.split('/').slice(0, -1).join('/');
|
||||
if (typeof fetch === 'undefined') {
|
||||
// skip fetch when in node / testing
|
||||
return;
|
||||
}
|
||||
const json = await fetch(url)
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
throw new Error(`error loading "${url}"`);
|
||||
});
|
||||
return [json, json._base || base];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a collection of samples to use with `s`
|
||||
* @example
|
||||
@@ -217,49 +273,8 @@ function getSamplesPrefixHandler(url) {
|
||||
|
||||
export const samples = async (sampleMap, baseUrl = sampleMap._base || '', options = {}) => {
|
||||
if (typeof sampleMap === 'string') {
|
||||
// check if custom prefix handler
|
||||
const handler = getSamplesPrefixHandler(sampleMap);
|
||||
if (handler) {
|
||||
return handler(sampleMap);
|
||||
}
|
||||
sampleMap = resolveSpecialPaths(sampleMap);
|
||||
if (sampleMap.startsWith('github:')) {
|
||||
sampleMap = githubPath(sampleMap, 'strudel.json');
|
||||
}
|
||||
if (sampleMap.startsWith('local:')) {
|
||||
sampleMap = `http://localhost:5432`;
|
||||
}
|
||||
if (sampleMap.startsWith('shabda:')) {
|
||||
let [_, path] = sampleMap.split('shabda:');
|
||||
sampleMap = `https://shabda.ndre.gr/${path}.json?strudel=1`;
|
||||
}
|
||||
if (sampleMap.startsWith('shabda/speech')) {
|
||||
let [_, path] = sampleMap.split('shabda/speech');
|
||||
path = path.startsWith('/') ? path.substring(1) : path;
|
||||
let [params, words] = path.split(':');
|
||||
let gender = 'f';
|
||||
let language = 'en-GB';
|
||||
if (params) {
|
||||
[language, gender] = params.split('/');
|
||||
}
|
||||
sampleMap = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
|
||||
}
|
||||
if (typeof fetch !== 'function') {
|
||||
// not a browser
|
||||
return;
|
||||
}
|
||||
const base = sampleMap.split('/').slice(0, -1).join('/');
|
||||
if (typeof fetch === 'undefined') {
|
||||
// skip fetch when in node / testing
|
||||
return;
|
||||
}
|
||||
return fetch(sampleMap)
|
||||
.then((res) => res.json())
|
||||
.then((json) => samples(json, baseUrl || json._base || base, options))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
throw new Error(`error loading "${sampleMap}"`);
|
||||
});
|
||||
const [json, base] = await fetchSampleMap(sampleMap);
|
||||
return samples(json, baseUrl || base, options);
|
||||
}
|
||||
const { prebake, tag } = options;
|
||||
processSampleMap(
|
||||
@@ -342,6 +357,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
||||
out.disconnect();
|
||||
onended();
|
||||
};
|
||||
|
||||
let envEnd = holdEnd + release + 0.01;
|
||||
bufferSource.stop(envEnd);
|
||||
const stop = (endTime) => {
|
||||
|
||||
@@ -7,11 +7,11 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
import './feedbackdelay.mjs';
|
||||
import './reverb.mjs';
|
||||
import './vowel.mjs';
|
||||
import { clamp, nanFallback, _mod } from './util.mjs';
|
||||
import { clamp, nanFallback, _mod, cycleToSeconds, secondsToCycle } from './util.mjs';
|
||||
import workletsUrl from './worklets.mjs?audioworklet';
|
||||
import { createFilter, gainNode, getCompressor, getWorklet } from './helpers.mjs';
|
||||
import { createFilter, gainNode, getCompressor, getWorklet, webAudioTimeout } from './helpers.mjs';
|
||||
import { map } from 'nanostores';
|
||||
import { logger } from './logger.mjs';
|
||||
import { logger, errorLogger } from './logger.mjs';
|
||||
import { loadBuffer } from './sampler.mjs';
|
||||
|
||||
export const DEFAULT_MAX_POLYPHONY = 128;
|
||||
@@ -28,6 +28,13 @@ export function setMultiChannelOrbits(bool) {
|
||||
multiChannelOrbits = bool == true;
|
||||
}
|
||||
|
||||
function getModulationShapeInput(val) {
|
||||
if (typeof val === 'number') {
|
||||
return val % 5;
|
||||
}
|
||||
return { tri: 0, triangle: 0, sine: 1, ramp: 2, saw: 3, square: 4 }[val] ?? 0;
|
||||
}
|
||||
|
||||
export const soundMap = map();
|
||||
|
||||
export function registerSound(key, onTrigger, data = {}) {
|
||||
@@ -106,7 +113,24 @@ export async function aliasBank(...args) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an alias for a sound.
|
||||
* @param {string} original - The original sound name
|
||||
* @param {string} alias - The alias to use for the sound
|
||||
*/
|
||||
export function soundAlias(original, alias) {
|
||||
if (getSound(original) == null) {
|
||||
logger('soundAlias: original sound not found');
|
||||
return;
|
||||
}
|
||||
soundMap.setKey(alias, getSound(original));
|
||||
}
|
||||
|
||||
export function getSound(s) {
|
||||
if (typeof s !== 'string') {
|
||||
console.warn(`getSound: expected string got "${s}". fall back to triangle`);
|
||||
return soundMap.get().triangle; // is this good?
|
||||
}
|
||||
return soundMap.get()[s.toLowerCase()];
|
||||
}
|
||||
|
||||
@@ -122,7 +146,7 @@ export const getAudioDevices = async () => {
|
||||
return devicesMap;
|
||||
};
|
||||
|
||||
const defaultDefaultValues = {
|
||||
let defaultDefaultValues = {
|
||||
s: 'triangle',
|
||||
gain: 0.8,
|
||||
postgain: 1,
|
||||
@@ -139,13 +163,24 @@ const defaultDefaultValues = {
|
||||
delay: 0,
|
||||
byteBeatExpression: '0',
|
||||
delayfeedback: 0.5,
|
||||
delaytime: 0.25,
|
||||
delaysync: 3 / 16,
|
||||
orbit: 1,
|
||||
i: 1,
|
||||
velocity: 1,
|
||||
fft: 8,
|
||||
};
|
||||
|
||||
const defaultDefaultDefaultValues = Object.freeze({ ...defaultDefaultValues });
|
||||
|
||||
export function setDefault(control, value) {
|
||||
// const main = getControlName(control); // we cant do this because superdough is independent of strudel/core
|
||||
defaultDefaultValues[control] = value;
|
||||
}
|
||||
|
||||
export function resetDefaults() {
|
||||
defaultDefaultValues = { ...defaultDefaultDefaultValues };
|
||||
}
|
||||
|
||||
let defaultControls = new Map(Object.entries(defaultDefaultValues));
|
||||
|
||||
export function setDefaultValue(key, value) {
|
||||
@@ -174,7 +209,7 @@ export const resetLoadedSounds = () => soundMap.set({});
|
||||
let audioContext;
|
||||
|
||||
export const setDefaultAudioContext = () => {
|
||||
audioContext = new AudioContext();
|
||||
audioContext = new AudioContext({ latencyHint: 'playback' });
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
@@ -190,11 +225,17 @@ export function getAudioContextCurrentTime() {
|
||||
return getAudioContext().currentTime;
|
||||
}
|
||||
|
||||
let externalWorklets = [];
|
||||
export function registerWorklet(url) {
|
||||
externalWorklets.push(url);
|
||||
}
|
||||
|
||||
let workletsLoading;
|
||||
function loadWorklets() {
|
||||
if (!workletsLoading) {
|
||||
const audioCtx = getAudioContext();
|
||||
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
|
||||
const allWorkletURLs = externalWorklets.concat([workletsUrl]);
|
||||
workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL)));
|
||||
}
|
||||
|
||||
return workletsLoading;
|
||||
@@ -260,7 +301,6 @@ export async function initAudioOnFirstClick(options) {
|
||||
return audioReady;
|
||||
}
|
||||
|
||||
let delays = {};
|
||||
const maxfeedback = 0.98;
|
||||
|
||||
let channelMerger, destinationGain;
|
||||
@@ -304,35 +344,44 @@ export const panic = () => {
|
||||
channelMerger == null;
|
||||
};
|
||||
|
||||
function getDelay(orbit, delaytime, delayfeedback, t, channels) {
|
||||
function getDelay(orbit, delaytime, delayfeedback, t) {
|
||||
if (delayfeedback > maxfeedback) {
|
||||
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
|
||||
}
|
||||
delayfeedback = clamp(delayfeedback, 0, 0.98);
|
||||
if (!delays[orbit]) {
|
||||
let delayNode = orbits[orbit].delayNode;
|
||||
if (delayNode === undefined) {
|
||||
const ac = getAudioContext();
|
||||
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
||||
dly.start?.(t); // for some reason, this throws when audion extension is installed..
|
||||
connectToDestination(dly, channels);
|
||||
delays[orbit] = dly;
|
||||
delayNode = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
||||
delayNode.start?.(t); // for some reason, this throws when audion extension is installed..
|
||||
connectToOrbit(delayNode, orbit);
|
||||
orbits[orbit].delayNode = delayNode;
|
||||
}
|
||||
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
|
||||
delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t);
|
||||
return delays[orbit];
|
||||
delayNode.delayTime.value !== delaytime && delayNode.delayTime.setValueAtTime(delaytime, t);
|
||||
delayNode.feedback.value !== delayfeedback && delayNode.feedback.setValueAtTime(delayfeedback, t);
|
||||
return delayNode;
|
||||
}
|
||||
|
||||
export function getLfo(audioContext, time, end, properties = {}) {
|
||||
return getWorklet(audioContext, 'lfo-processor', {
|
||||
export function getLfo(audioContext, begin, end, properties = {}) {
|
||||
const { shape = 0, ...props } = properties;
|
||||
const { dcoffset = -0.5, depth = 1 } = properties;
|
||||
const lfoprops = {
|
||||
frequency: 1,
|
||||
depth: 1,
|
||||
skew: 0,
|
||||
depth,
|
||||
skew: 0.5,
|
||||
phaseoffset: 0,
|
||||
time,
|
||||
time: begin,
|
||||
begin,
|
||||
end,
|
||||
shape: 1,
|
||||
dcoffset: -0.5,
|
||||
...properties,
|
||||
});
|
||||
shape: getModulationShapeInput(shape),
|
||||
dcoffset,
|
||||
min: dcoffset * depth,
|
||||
max: dcoffset * depth + depth,
|
||||
curve: 1,
|
||||
...props,
|
||||
};
|
||||
|
||||
return getWorklet(audioContext, 'lfo-processor', lfoprops);
|
||||
}
|
||||
|
||||
function getPhaser(time, end, frequency = 1, depth = 0.5, centerFrequency = 1000, sweep = 2000) {
|
||||
@@ -366,31 +415,95 @@ function getFilterType(ftype) {
|
||||
return typeof ftype === 'number' ? filterTypes[Math.floor(_mod(ftype, filterTypes.length))] : ftype;
|
||||
}
|
||||
|
||||
let reverbs = {};
|
||||
let hasChanged = (now, before) => now !== undefined && now !== before;
|
||||
function getReverb(orbit, duration, fade, lp, dim, ir, channels) {
|
||||
// If no reverb has been created for a given orbit, create one
|
||||
if (!reverbs[orbit]) {
|
||||
const ac = getAudioContext();
|
||||
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
|
||||
connectToDestination(reverb, channels);
|
||||
reverbs[orbit] = reverb;
|
||||
// type orbit {
|
||||
// output: GainNode,
|
||||
// reverbNode: ConvolverNode
|
||||
// delayNode: FeedbackDelayNode
|
||||
// }
|
||||
let orbits = {};
|
||||
function connectToOrbit(node, orbit) {
|
||||
if (orbits[orbit] == null) {
|
||||
errorLogger(new Error('target orbit does not exist'), 'superdough');
|
||||
}
|
||||
node.connect(orbits[orbit].output);
|
||||
}
|
||||
|
||||
function setOrbit(audioContext, orbit, channels) {
|
||||
if (orbits[orbit] == null) {
|
||||
orbits[orbit] = {
|
||||
// Setup output node through which all audio filters prior to hitting
|
||||
// the destination (and thus allows for global volume automation)
|
||||
output: new GainNode(audioContext, { gain: 1, channelCount: 2, channelCountMode: 'explicit' }),
|
||||
};
|
||||
connectToDestination(orbits[orbit].output, channels);
|
||||
}
|
||||
}
|
||||
|
||||
function duckOrbit(audioContext, targetOrbit, t, onsettime = 0, attacktime = 0.1, duckdepth = 1) {
|
||||
const targetArr = [targetOrbit].flat();
|
||||
const onsetArr = [onsettime].flat();
|
||||
const attackArr = [attacktime].flat();
|
||||
const depthArr = [duckdepth].flat();
|
||||
|
||||
targetArr.forEach((target, idx) => {
|
||||
if (orbits[target] == null) {
|
||||
errorLogger(new Error(`duck target orbit ${target} does not exist`), 'superdough');
|
||||
return;
|
||||
}
|
||||
const onset = onsetArr[idx] ?? onsetArr[0];
|
||||
const attack = Math.max(attackArr[idx] ?? attackArr[0], 0.002);
|
||||
const depth = depthArr[idx] ?? depthArr[0];
|
||||
const gainParam = orbits[target].output.gain;
|
||||
webAudioTimeout(
|
||||
audioContext,
|
||||
() => {
|
||||
const now = audioContext.currentTime;
|
||||
|
||||
// cancelScheduledValues and setValueAtTime together emulate cancelAndHoldAtTime
|
||||
// on browsers which lack that method
|
||||
const currVal = gainParam.value;
|
||||
gainParam.cancelScheduledValues(now);
|
||||
gainParam.setValueAtTime(currVal, now);
|
||||
|
||||
const t0 = Math.max(t, now); // guard against now > t
|
||||
const duckedVal = clamp(1 - Math.sqrt(depth), 0.01, currVal);
|
||||
gainParam.exponentialRampToValueAtTime(duckedVal, t0 + onset);
|
||||
gainParam.exponentialRampToValueAtTime(1, t0 + onset + attack);
|
||||
},
|
||||
0,
|
||||
t - 0.01,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let hasChanged = (now, before) => now !== undefined && now !== before;
|
||||
function getReverb(orbit, duration, fade, lp, dim, ir, irspeed, irbegin) {
|
||||
// If no reverb has been created for a given orbit, create one
|
||||
let reverbNode = orbits[orbit].reverbNode;
|
||||
if (reverbNode === undefined) {
|
||||
const ac = getAudioContext();
|
||||
reverbNode = ac.createReverb(duration, fade, lp, dim, ir, irspeed, irbegin);
|
||||
connectToOrbit(reverbNode, orbit);
|
||||
orbits[orbit].reverbNode = reverbNode;
|
||||
}
|
||||
|
||||
if (
|
||||
hasChanged(duration, reverbs[orbit].duration) ||
|
||||
hasChanged(fade, reverbs[orbit].fade) ||
|
||||
hasChanged(lp, reverbs[orbit].lp) ||
|
||||
hasChanged(dim, reverbs[orbit].dim) ||
|
||||
reverbs[orbit].ir !== ir
|
||||
hasChanged(duration, reverbNode.duration) ||
|
||||
hasChanged(fade, reverbNode.fade) ||
|
||||
hasChanged(lp, reverbNode.lp) ||
|
||||
hasChanged(dim, reverbNode.dim) ||
|
||||
hasChanged(irspeed, reverbNode.irspeed) ||
|
||||
hasChanged(irbegin, reverbNode.irbegin) ||
|
||||
reverbNode.ir !== ir
|
||||
) {
|
||||
// only regenerate when something has changed
|
||||
// avoids endless regeneration on things like
|
||||
// stack(s("a"), s("b").rsize(8)).room(.5)
|
||||
// this only works when args may stay undefined until here
|
||||
// setting default values breaks this
|
||||
reverbs[orbit].generate(duration, fade, lp, dim, ir);
|
||||
reverbNode.generate(duration, fade, lp, dim, ir, irspeed, irbegin);
|
||||
}
|
||||
return reverbs[orbit];
|
||||
return reverbNode;
|
||||
}
|
||||
|
||||
export let analysers = {},
|
||||
@@ -433,8 +546,7 @@ function effectSend(input, effect, wet) {
|
||||
}
|
||||
|
||||
export function resetGlobalEffects() {
|
||||
delays = {};
|
||||
reverbs = {};
|
||||
orbits = {};
|
||||
analysers = {};
|
||||
analysersData = {};
|
||||
}
|
||||
@@ -446,9 +558,10 @@ function mapChannelNumbers(channels) {
|
||||
return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
||||
}
|
||||
|
||||
export const superdough = async (value, t, hapDuration, cps) => {
|
||||
export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => {
|
||||
// new: t is always expected to be the absolute target onset time
|
||||
const ac = getAudioContext();
|
||||
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
|
||||
|
||||
let { stretch } = value;
|
||||
if (stretch != null) {
|
||||
//account for phase vocoder latency
|
||||
@@ -474,15 +587,26 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
}
|
||||
// destructure
|
||||
let {
|
||||
tremolo,
|
||||
tremolosync,
|
||||
tremolodepth = 1,
|
||||
tremoloskew,
|
||||
tremolophase = 0,
|
||||
tremoloshape,
|
||||
s = getDefaultValue('s'),
|
||||
bank,
|
||||
source,
|
||||
gain = getDefaultValue('gain'),
|
||||
postgain = getDefaultValue('postgain'),
|
||||
density = getDefaultValue('density'),
|
||||
duckorbit,
|
||||
duckonset,
|
||||
duckattack,
|
||||
duckdepth,
|
||||
// filters
|
||||
fanchor = getDefaultValue('fanchor'),
|
||||
drive = 0.69,
|
||||
release = 0,
|
||||
// low pass
|
||||
cutoff,
|
||||
lpenv,
|
||||
@@ -515,7 +639,9 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
phasercenter,
|
||||
//
|
||||
coarse,
|
||||
|
||||
crush,
|
||||
dry,
|
||||
shape,
|
||||
shapevol = getDefaultValue('shapevol'),
|
||||
distort,
|
||||
@@ -524,7 +650,8 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
vowel,
|
||||
delay = getDefaultValue('delay'),
|
||||
delayfeedback = getDefaultValue('delayfeedback'),
|
||||
delaytime = getDefaultValue('delaytime'),
|
||||
delaysync = getDefaultValue('delaysync'),
|
||||
delaytime,
|
||||
orbit = getDefaultValue('orbit'),
|
||||
room,
|
||||
roomfade,
|
||||
@@ -532,6 +659,8 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
roomdim,
|
||||
roomsize,
|
||||
ir,
|
||||
irspeed,
|
||||
irbegin,
|
||||
i = getDefaultValue('i'),
|
||||
velocity = getDefaultValue('velocity'),
|
||||
analyze, // analyser wet
|
||||
@@ -543,10 +672,18 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
compressorRelease,
|
||||
} = value;
|
||||
|
||||
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
||||
|
||||
const orbitChannels = mapChannelNumbers(
|
||||
multiChannelOrbits && orbit > 0 ? [orbit * 2 - 1, orbit * 2] : getDefaultValue('channels'),
|
||||
);
|
||||
|
||||
const channels = value.channels != null ? mapChannelNumbers(value.channels) : orbitChannels;
|
||||
setOrbit(ac, orbit, channels, t, cycle, cps);
|
||||
|
||||
if (duckorbit != null) {
|
||||
duckOrbit(ac, duckorbit, t, duckonset, duckattack, duckdepth);
|
||||
}
|
||||
|
||||
gain = applyGainCurve(nanFallback(gain, 1));
|
||||
postgain = applyGainCurve(postgain);
|
||||
@@ -554,8 +691,11 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
distortvol = applyGainCurve(distortvol);
|
||||
delay = applyGainCurve(delay);
|
||||
velocity = applyGainCurve(velocity);
|
||||
tremolodepth = applyGainCurve(tremolodepth);
|
||||
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||
|
||||
const end = t + hapDuration;
|
||||
const endWithRelease = end + release;
|
||||
const chainID = Math.round(Math.random() * 1000000);
|
||||
|
||||
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
|
||||
@@ -571,6 +711,9 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
|
||||
let audioNodes = [];
|
||||
|
||||
if (['-', '~', '_'].includes(s)) {
|
||||
return;
|
||||
}
|
||||
if (bank && s) {
|
||||
s = `${bank}_${s}`;
|
||||
value.s = s;
|
||||
@@ -627,7 +770,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
lprelease,
|
||||
lpenv,
|
||||
t,
|
||||
t + hapDuration,
|
||||
end,
|
||||
fanchor,
|
||||
ftype,
|
||||
drive,
|
||||
@@ -651,7 +794,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
hprelease,
|
||||
hpenv,
|
||||
t,
|
||||
t + hapDuration,
|
||||
end,
|
||||
fanchor,
|
||||
);
|
||||
chain.push(hp());
|
||||
@@ -662,20 +805,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
|
||||
if (bandf !== undefined) {
|
||||
let bp = () =>
|
||||
createFilter(
|
||||
ac,
|
||||
'bandpass',
|
||||
bandf,
|
||||
bandq,
|
||||
bpattack,
|
||||
bpdecay,
|
||||
bpsustain,
|
||||
bprelease,
|
||||
bpenv,
|
||||
t,
|
||||
t + hapDuration,
|
||||
fanchor,
|
||||
);
|
||||
createFilter(ac, 'bandpass', bandf, bandq, bpattack, bpdecay, bpsustain, bprelease, bpenv, t, end, fanchor);
|
||||
chain.push(bp());
|
||||
if (ftype === '24db') {
|
||||
chain.push(bp());
|
||||
@@ -693,6 +823,33 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape, postgain: shapevol }));
|
||||
distort !== undefined && chain.push(getWorklet(ac, 'distort-processor', { distort, postgain: distortvol }));
|
||||
|
||||
if (tremolosync != null) {
|
||||
tremolo = cps * tremolosync;
|
||||
}
|
||||
|
||||
if (tremolo !== undefined) {
|
||||
// Allow clipping of modulator for more dynamic possiblities, and to prevent speaker overload
|
||||
// EX: a triangle waveform will clip like this /-\ when the depth is above 1
|
||||
const gain = Math.max(1 - tremolodepth, 0);
|
||||
const amGain = new GainNode(ac, { gain });
|
||||
|
||||
const time = cycle / cps;
|
||||
const lfo = getLfo(ac, t, endWithRelease, {
|
||||
skew: tremoloskew ?? (tremoloshape != null ? 0.5 : 1),
|
||||
frequency: tremolo,
|
||||
depth: tremolodepth,
|
||||
time,
|
||||
dcoffset: 0,
|
||||
shape: tremoloshape,
|
||||
phaseoffset: tremolophase,
|
||||
min: 0,
|
||||
max: 1,
|
||||
curve: 1.5,
|
||||
});
|
||||
lfo.connect(amGain.gain);
|
||||
chain.push(amGain);
|
||||
}
|
||||
|
||||
compressorThreshold !== undefined &&
|
||||
chain.push(
|
||||
getCompressor(ac, compressorThreshold, compressorRatio, compressorKnee, compressorAttack, compressorRelease),
|
||||
@@ -706,20 +863,19 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
}
|
||||
// phaser
|
||||
if (phaser !== undefined && phaserdepth > 0) {
|
||||
const phaserFX = getPhaser(t, t + hapDuration, phaser, phaserdepth, phasercenter, phasersweep);
|
||||
const phaserFX = getPhaser(t, endWithRelease, phaser, phaserdepth, phasercenter, phasersweep);
|
||||
chain.push(phaserFX);
|
||||
}
|
||||
|
||||
// last gain
|
||||
const post = new GainNode(ac, { gain: postgain });
|
||||
chain.push(post);
|
||||
connectToDestination(post, channels);
|
||||
|
||||
// delay
|
||||
let delaySend;
|
||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||
const delyNode = getDelay(orbit, delaytime, delayfeedback, t, orbitChannels);
|
||||
delaySend = effectSend(post, delyNode, delay);
|
||||
const delayNode = getDelay(orbit, delaytime, delayfeedback, t);
|
||||
delaySend = effectSend(post, delayNode, delay);
|
||||
audioNodes.push(delaySend);
|
||||
}
|
||||
// reverb
|
||||
@@ -736,7 +892,7 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
}
|
||||
roomIR = await loadBuffer(url, ac, ir, 0);
|
||||
}
|
||||
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, orbitChannels);
|
||||
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR, irspeed, irbegin);
|
||||
reverbSend = effectSend(post, reverbNode, room);
|
||||
audioNodes.push(reverbSend);
|
||||
}
|
||||
@@ -748,6 +904,14 @@ export const superdough = async (value, t, hapDuration, cps) => {
|
||||
analyserSend = effectSend(post, analyserNode, 1);
|
||||
audioNodes.push(analyserSend);
|
||||
}
|
||||
if (dry != null) {
|
||||
dry = applyGainCurve(dry);
|
||||
const dryGain = new GainNode(ac, { gain: dry });
|
||||
chain.push(dryGain);
|
||||
connectToOrbit(dryGain, orbit);
|
||||
} else {
|
||||
connectToOrbit(post, orbit);
|
||||
}
|
||||
|
||||
// connect chain elements together
|
||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { clamp, midiToFreq, noteToMidi } from './util.mjs';
|
||||
import { registerSound, getAudioContext, getLfo } from './superdough.mjs';
|
||||
import { registerSound, getAudioContext, soundMap, getLfo } from './superdough.mjs';
|
||||
import {
|
||||
applyFM,
|
||||
gainNode,
|
||||
@@ -9,12 +9,13 @@ import {
|
||||
getVibratoOscillator,
|
||||
webAudioTimeout,
|
||||
getWorklet,
|
||||
noises,
|
||||
} from './helpers.mjs';
|
||||
import { getNoiseMix, getNoiseOscillator } from './noise.mjs';
|
||||
|
||||
const getFrequencyFromValue = (value) => {
|
||||
const getFrequencyFromValue = (value, defaultNote = 36) => {
|
||||
let { note, freq } = value;
|
||||
note = note || 36;
|
||||
note = note || defaultNote;
|
||||
if (typeof note === 'string') {
|
||||
note = noteToMidi(note); // e.g. c3 => 48
|
||||
}
|
||||
@@ -34,7 +35,23 @@ function destroyAudioWorkletNode(node) {
|
||||
}
|
||||
|
||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
|
||||
const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||
const waveformAliases = [
|
||||
['tri', 'triangle'],
|
||||
['sqr', 'square'],
|
||||
['saw', 'sawtooth'],
|
||||
['sin', 'sine'],
|
||||
];
|
||||
|
||||
function makeSaturationCurve(amount, n_samples) {
|
||||
const k = typeof amount === 'number' ? amount : 50;
|
||||
const curve = new Float32Array(n_samples);
|
||||
|
||||
for (let i = 0; i < n_samples; i++) {
|
||||
const x = (i * 2) / n_samples - 1;
|
||||
curve[i] = Math.tanh(x * k);
|
||||
}
|
||||
return curve;
|
||||
}
|
||||
|
||||
export function registerSynthSounds() {
|
||||
[...waveforms].forEach((s) => {
|
||||
@@ -78,6 +95,75 @@ export function registerSynthSounds() {
|
||||
{ type: 'synth', prebake: true },
|
||||
);
|
||||
});
|
||||
|
||||
registerSound(
|
||||
'sbd',
|
||||
(t, value, onended) => {
|
||||
const { duration, decay = 0.5, pdecay = 0.5, penv = 36, clip } = value;
|
||||
const ctx = getAudioContext();
|
||||
const attackhold = 0.02;
|
||||
const noiselvl = 1.2;
|
||||
const noisedecay = 0.025;
|
||||
const mixGain = 1;
|
||||
|
||||
const o = ctx.createOscillator();
|
||||
o.type = 'triangle';
|
||||
o.frequency.value = getFrequencyFromValue(value, 29);
|
||||
o.detune.setValueAtTime(penv * 100, 0);
|
||||
o.detune.setValueAtTime(penv * 100, t);
|
||||
o.detune.exponentialRampToValueAtTime(0.001, t + pdecay);
|
||||
const g = gainNode(1);
|
||||
g.gain.setValueAtTime(1, t + attackhold);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, t + attackhold + decay);
|
||||
o.start(t);
|
||||
|
||||
const noise = getNoiseOscillator('brown', t, 2);
|
||||
const noiseGain = gainNode(1);
|
||||
noiseGain.gain.setValueAtTime(noiselvl, t);
|
||||
noiseGain.gain.exponentialRampToValueAtTime(0.001, t + noisedecay);
|
||||
|
||||
const sat = new WaveShaperNode(ctx);
|
||||
// tri to sine diode shaper emulation
|
||||
sat.curve = makeSaturationCurve(2, ctx.sampleRate);
|
||||
|
||||
const mix = gainNode(mixGain);
|
||||
|
||||
o.onended = () => {
|
||||
o.disconnect();
|
||||
g.disconnect();
|
||||
sat.disconnect();
|
||||
noise.node.disconnect();
|
||||
noiseGain.disconnect();
|
||||
mix.disconnect();
|
||||
onended();
|
||||
};
|
||||
|
||||
const node = o.connect(sat).connect(g).connect(mix);
|
||||
noise.node.connect(noiseGain).connect(mix);
|
||||
|
||||
const holdEnd = t + decay;
|
||||
let end = holdEnd + 0.01;
|
||||
if (clip != null) {
|
||||
end = Math.min(t + clip * duration, end);
|
||||
}
|
||||
|
||||
// prevent clicking
|
||||
mix.gain.setValueAtTime(mixGain, end - 0.01);
|
||||
mix.gain.linearRampToValueAtTime(0, end);
|
||||
|
||||
o.stop(end);
|
||||
noise.stop(end);
|
||||
|
||||
return {
|
||||
node,
|
||||
stop: (endTime) => {
|
||||
o.stop(endTime);
|
||||
},
|
||||
};
|
||||
},
|
||||
{ type: 'synth', prebake: true },
|
||||
);
|
||||
|
||||
registerSound(
|
||||
'supersaw',
|
||||
(begin, value, onended) => {
|
||||
@@ -342,6 +428,7 @@ export function registerSynthSounds() {
|
||||
{ type: 'synth', prebake: true },
|
||||
);
|
||||
});
|
||||
waveformAliases.forEach(([alias, actual]) => soundMap.set({ ...soundMap.get(), [alias]: soundMap.get()[actual] }));
|
||||
}
|
||||
|
||||
export function waveformN(partials, type) {
|
||||
|
||||
@@ -7,7 +7,7 @@ export const tokenizeNote = (note) => {
|
||||
if (typeof note !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)(-?[0-9]*)$/)?.slice(1) || [];
|
||||
if (!pc) {
|
||||
return [];
|
||||
}
|
||||
@@ -68,3 +68,11 @@ export const _mod = (n, m) => ((n % m) + m) % m;
|
||||
export const getSoundIndex = (n, numSounds) => {
|
||||
return _mod(Math.round(nanFallback(n, 0)), numSounds);
|
||||
};
|
||||
|
||||
export function cycleToSeconds(cycle, cps) {
|
||||
return cycle / cps;
|
||||
}
|
||||
|
||||
export function secondsToCycle(t, cps) {
|
||||
return t * cps;
|
||||
}
|
||||
|
||||
@@ -8,18 +8,28 @@ import FFT from './fft.js';
|
||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
const _mod = (n, m) => ((n % m) + m) % m;
|
||||
|
||||
// Restrict phase to the range [0, maxPhase) via wrapping
|
||||
function wrapPhase(phase, maxPhase = 1) {
|
||||
if (phase >= maxPhase) {
|
||||
phase -= maxPhase;
|
||||
} else if (phase < 0) {
|
||||
phase += maxPhase;
|
||||
}
|
||||
return phase;
|
||||
}
|
||||
const blockSize = 128;
|
||||
// adjust waveshape to remove frequencies above nyquist to prevent aliasing
|
||||
// Smooth waveshape near discontinuities to remove frequencies above Nyquist and prevent aliasing
|
||||
// referenced from https://www.kvraudio.com/forum/viewtopic.php?t=375517
|
||||
function polyBlep(phase, dt) {
|
||||
// 0 <= phase < 1
|
||||
dt = Math.min(dt, 1 - dt);
|
||||
// Start of cycle
|
||||
if (phase < dt) {
|
||||
phase /= dt;
|
||||
// 2 * (phase - phase^2/2 - 0.5)
|
||||
return phase + phase - phase * phase - 1;
|
||||
}
|
||||
|
||||
// -1 < phase < 0
|
||||
// End of cycle
|
||||
else if (phase > 1 - dt) {
|
||||
phase = (phase - 1) / dt;
|
||||
// 2 * (phase^2/2 + phase + 0.5)
|
||||
@@ -31,7 +41,7 @@ function polyBlep(phase, dt) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// The order is important for dough integration
|
||||
const waveshapes = {
|
||||
tri(phase, skew = 0.5) {
|
||||
const x = 1 - skew;
|
||||
@@ -81,10 +91,12 @@ function getParamValue(block, param) {
|
||||
}
|
||||
return param[0];
|
||||
}
|
||||
|
||||
const waveShapeNames = Object.keys(waveshapes);
|
||||
class LFOProcessor extends AudioWorkletProcessor {
|
||||
static get parameterDescriptors() {
|
||||
return [
|
||||
{ name: 'begin', defaultValue: 0 },
|
||||
{ name: 'time', defaultValue: 0 },
|
||||
{ name: 'end', defaultValue: 0 },
|
||||
{ name: 'frequency', defaultValue: 0.5 },
|
||||
@@ -92,7 +104,10 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
{ name: 'depth', defaultValue: 1 },
|
||||
{ name: 'phaseoffset', defaultValue: 0 },
|
||||
{ name: 'shape', defaultValue: 0 },
|
||||
{ name: 'curve', defaultValue: 1 },
|
||||
{ name: 'dcoffset', defaultValue: 0 },
|
||||
{ name: 'min', defaultValue: 0 },
|
||||
{ name: 'max', defaultValue: 1 },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -109,10 +124,13 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters) {
|
||||
// eslint-disable-next-line no-undef
|
||||
const begin = parameters['begin'][0];
|
||||
if (currentTime >= parameters.end[0]) {
|
||||
return false;
|
||||
}
|
||||
if (currentTime <= begin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const output = outputs[0];
|
||||
const frequency = parameters['frequency'][0];
|
||||
@@ -122,7 +140,11 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
const skew = parameters['skew'][0];
|
||||
const phaseoffset = parameters['phaseoffset'][0];
|
||||
|
||||
const curve = parameters['curve'][0];
|
||||
|
||||
const dcoffset = parameters['dcoffset'][0];
|
||||
const min = parameters['min'][0];
|
||||
const max = parameters['max'][0];
|
||||
const shape = waveShapeNames[parameters['shape'][0]];
|
||||
|
||||
const blockSize = output[0].length ?? 0;
|
||||
@@ -130,12 +152,12 @@ class LFOProcessor extends AudioWorkletProcessor {
|
||||
if (this.phase == null) {
|
||||
this.phase = _mod(time * frequency + phaseoffset, 1);
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const dt = frequency / sampleRate;
|
||||
for (let n = 0; n < blockSize; n++) {
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
const modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
|
||||
output[i][n] = modval;
|
||||
let modval = (waveshapes[shape](this.phase, skew) + dcoffset) * depth;
|
||||
modval = Math.pow(modval, curve);
|
||||
output[i][n] = clamp(modval, min, max);
|
||||
}
|
||||
this.incrementPhase(dt);
|
||||
}
|
||||
@@ -291,7 +313,6 @@ class LadderProcessor extends AudioWorkletProcessor {
|
||||
const drive = clamp(Math.exp(parameters.drive[0]), 0.1, 2000);
|
||||
|
||||
let cutoff = parameters.frequency[0];
|
||||
// eslint-disable-next-line no-undef
|
||||
cutoff = (cutoff * 2 * _PI) / sampleRate;
|
||||
cutoff = cutoff > 1 ? 1 : cutoff;
|
||||
|
||||
@@ -424,18 +445,13 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
];
|
||||
}
|
||||
process(input, outputs, params) {
|
||||
// eslint-disable-next-line no-undef
|
||||
if (currentTime <= params.begin[0]) {
|
||||
return true;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
if (currentTime >= params.end[0]) {
|
||||
// this.port.postMessage({ type: 'onended' });
|
||||
return false;
|
||||
}
|
||||
let frequency = params.frequency[0];
|
||||
//apply detune in cents
|
||||
frequency = frequency * Math.pow(2, params.detune[0] / 1200);
|
||||
|
||||
const output = outputs[0];
|
||||
const voices = params.voices[0];
|
||||
@@ -446,9 +462,6 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
|
||||
for (let n = 0; n < voices; n++) {
|
||||
const isOdd = (n & 1) == 1;
|
||||
|
||||
//applies unison "spread" detune in semitones
|
||||
const freq = applySemitoneDetuneToFrequency(frequency, getUnisonDetune(voices, freqspread, n));
|
||||
let gainL = gain1;
|
||||
let gainR = gain2;
|
||||
// invert right and left gain
|
||||
@@ -456,21 +469,21 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
gainL = gain2;
|
||||
gainR = gain1;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
const dt = freq / sampleRate;
|
||||
|
||||
for (let i = 0; i < output[0].length; i++) {
|
||||
// Main detuning
|
||||
let freq = applySemitoneDetuneToFrequency(params.frequency[i] ?? params.frequency[0], params.detune[0] / 100);
|
||||
// Individual voice detuning
|
||||
freq = applySemitoneDetuneToFrequency(freq, getUnisonDetune(voices, freqspread, n));
|
||||
// We must wrap this here because it is passed into sawblep below which
|
||||
// has domain [0, 1]
|
||||
const dt = _mod(freq / sampleRate, 1);
|
||||
this.phase[n] = this.phase[n] ?? Math.random();
|
||||
const v = waveshapes.sawblep(this.phase[n], dt);
|
||||
|
||||
output[0][i] = output[0][i] + v * gainL;
|
||||
output[1][i] = output[1][i] + v * gainR;
|
||||
|
||||
this.phase[n] += dt;
|
||||
|
||||
if (this.phase[n] > 1.0) {
|
||||
this.phase[n] = this.phase[n] - 1;
|
||||
}
|
||||
this.phase[n] = wrapPhase(this.phase[n] + dt);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -479,7 +492,7 @@ class SuperSawOscillatorProcessor extends AudioWorkletProcessor {
|
||||
|
||||
registerProcessor('supersaw-oscillator', SuperSawOscillatorProcessor);
|
||||
|
||||
// Phase Vocoder sourced from // sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
|
||||
// Phase Vocoder sourced from https://github.com/olvb/phaze/tree/master?tab=readme-ov-file
|
||||
const BUFFERED_BLOCK_SIZE = 2048;
|
||||
|
||||
function genHannWindow(length) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pattern.wav
|
||||
@@ -0,0 +1,3 @@
|
||||
# supradough
|
||||
|
||||
platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.
|
||||
@@ -0,0 +1,123 @@
|
||||
// this is a poc of how a pattern can be rendered as a wav file using node
|
||||
// run via: node dough-export.mjs
|
||||
import fs from 'node:fs';
|
||||
import WavEncoder from 'wav-encoder';
|
||||
import { evalScope } from '@strudel/core';
|
||||
import { miniAllStrings } from '@strudel/mini';
|
||||
import { Dough } from './dough.mjs';
|
||||
|
||||
await evalScope(
|
||||
import('@strudel/core'),
|
||||
import('@strudel/mini'),
|
||||
import('@strudel/tonal'),
|
||||
// import('@strudel/tonal'),
|
||||
);
|
||||
|
||||
miniAllStrings(); // allows using single quotes for mini notation / skip transpilation
|
||||
|
||||
let sampleRate = 48000,
|
||||
cps = 0.4;
|
||||
|
||||
/* await doughsamples('github:eddyflux/crate');
|
||||
await doughsamples('github:eddyflux/wax'); */
|
||||
|
||||
let pat = note('c,eb,g,<bb c4 d4 eb4>')
|
||||
.s('sine')
|
||||
.press()
|
||||
.add(note(24))
|
||||
.fmi(3)
|
||||
.fmh(5.01)
|
||||
.dec(0.4)
|
||||
.delay('.6:<.12 .22>:.8')
|
||||
.jux(press)
|
||||
.rarely(add(note('12')))
|
||||
.lpf(400)
|
||||
.lpq(0.2)
|
||||
.lpd(0.4)
|
||||
.lpenv(3)
|
||||
.fmdecay(0.4)
|
||||
.fmenv(1)
|
||||
.postgain(0.6)
|
||||
.stack(s('<pink white>*8').dec(0.07).rarely(ply('2')).delay(0.5).hpf(sine.range(200, 2000).slow(4)).hpq(0.2))
|
||||
.stack(
|
||||
s('[- white@3]*2')
|
||||
.dec(0.4)
|
||||
.hpf('<2000!3 <4000 8000>>*4')
|
||||
.hpq(0.6)
|
||||
.ply('<1 2>*4')
|
||||
.postgain(0.5)
|
||||
.delay(0.5)
|
||||
.jux(rev)
|
||||
.lpf(5000),
|
||||
)
|
||||
.stack(
|
||||
note('<c2 - [- f1] ->*2')
|
||||
.s('square')
|
||||
.lpf(sine.range(100, 300).slow(4))
|
||||
.lpe(1)
|
||||
.segment(8)
|
||||
.lpd(0.3)
|
||||
.lpq(0.2)
|
||||
.dec(0.2)
|
||||
.speed('<1 2>')
|
||||
.ply('<1 2>')
|
||||
.postgain(1),
|
||||
)
|
||||
.stack(
|
||||
chord('<Cm Cm7 Cm9 Cm11 Fm Fm7 Fm9 Fm11>')
|
||||
.voicing()
|
||||
.s('<sine>')
|
||||
.clip(1)
|
||||
.rel(0.4)
|
||||
.vib('4:.2')
|
||||
.gain(0.7)
|
||||
.hpf(1200)
|
||||
.fm(0.5)
|
||||
.att(1)
|
||||
.lpa(0.5)
|
||||
.lpf(200)
|
||||
.lpenv(4)
|
||||
.chorus(0.8),
|
||||
)
|
||||
.slow(1 / cps);
|
||||
|
||||
let cycles = 30;
|
||||
let seconds = cycles + 1; // 1s release tail
|
||||
const haps = pat.queryArc(0, cycles);
|
||||
|
||||
const dough = new Dough(sampleRate);
|
||||
|
||||
console.log('spawn voices...');
|
||||
haps.forEach((hap) => {
|
||||
hap.value._begin = Number(hap.whole.begin);
|
||||
hap.value._duration = hap.duration /* / cps */;
|
||||
dough.scheduleSpawn(hap.value);
|
||||
});
|
||||
console.log(`render ${seconds}s long buffer, each dot is 1 second:`);
|
||||
const buffers = [new Float32Array(seconds * sampleRate), new Float32Array(seconds * sampleRate)];
|
||||
let t = performance.now();
|
||||
while (dough.t <= buffers[0].length) {
|
||||
dough.update();
|
||||
buffers[0][dough.t] = dough.out[0];
|
||||
buffers[1][dough.t] = dough.out[1];
|
||||
if (dough.t % sampleRate === 0) {
|
||||
process.stdout.write('.');
|
||||
}
|
||||
}
|
||||
const took = (performance.now() - t) / 1000;
|
||||
const load = (took / seconds) * 100;
|
||||
const speed = (seconds / took).toFixed(2);
|
||||
console.log('');
|
||||
console.log(`done!
|
||||
rendered ${seconds}s in ${took.toFixed(2)}s
|
||||
speed: ${speed}x
|
||||
load: ${load.toFixed(2)}%`);
|
||||
|
||||
const patternAudio = {
|
||||
sampleRate,
|
||||
channelData: buffers,
|
||||
};
|
||||
|
||||
WavEncoder.encode(patternAudio).then((buffer) => {
|
||||
fs.writeFileSync('pattern.wav', new Float32Array(buffer));
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Dough } from './dough.mjs';
|
||||
|
||||
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
|
||||
class DoughProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.dough = new Dough(sampleRate, currentTime);
|
||||
this.port.onmessage = (event) => {
|
||||
if (event.data.spawn) {
|
||||
this.dough.scheduleSpawn(event.data.spawn);
|
||||
} else if (event.data.sample) {
|
||||
this.dough.loadSample(event.data.sample, event.data.channels, event.data.sampleRate);
|
||||
} else if (event.data.samples) {
|
||||
event.data.samples.forEach(([name, channels, sampleRate]) => {
|
||||
this.dough.loadSample(name, channels, sampleRate);
|
||||
});
|
||||
} else {
|
||||
console.log('unrecognized event type', event.data);
|
||||
}
|
||||
};
|
||||
}
|
||||
process(inputs, outputs, params) {
|
||||
if (this.disconnected) {
|
||||
return false;
|
||||
}
|
||||
const output = outputs[0];
|
||||
for (let i = 0; i < output[0].length; i++) {
|
||||
this.dough.update();
|
||||
for (let c = 0; c < output.length; c++) {
|
||||
//prevent speaker blowout via clipping if threshold exceeds
|
||||
output[c][i] = clamp(this.dough.out[c], -1, 1);
|
||||
}
|
||||
}
|
||||
return true; // keep the audio processing going
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('dough-processor', DoughProcessor);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
// import _workletUrl from './dough-worklet.mjs?url'; // only for dev (breaks for production build)
|
||||
import _workletUrl from './dough-worklet.mjs?audioworklet'; // only for prod (breaks in development?!)
|
||||
|
||||
export * from './dough.mjs';
|
||||
export const workletUrl = _workletUrl;
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "supradough",
|
||||
"version": "1.2.3",
|
||||
"description": "platform agnostic synth and sampler intended for live coding. a reimplementation of superdough.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-bundle-audioworklet": "workspace:*",
|
||||
"wav-encoder": "^1.3.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/tonal",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -7,73 +7,130 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
// import { strict as assert } from 'assert';
|
||||
|
||||
import '../tonal.mjs'; // need to import this to add prototypes
|
||||
import { pure, n, seq, note } from '@strudel/core';
|
||||
import { pure, n, seq, note, noteToMidi } from '@strudel/core';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mini } from '../../mini/mini.mjs';
|
||||
|
||||
describe('tonal', () => {
|
||||
it('Should run tonal functions ', () => {
|
||||
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
|
||||
describe('scaleTranspose', () => {
|
||||
it('transposes notes by scale degrees', () => {
|
||||
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
|
||||
});
|
||||
});
|
||||
it('scale with plain values', () => {
|
||||
expect(
|
||||
seq(0, 1, 2)
|
||||
.scale('C major')
|
||||
.note()
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
describe('scale', () => {
|
||||
it('converts plain values', () => {
|
||||
expect(
|
||||
seq(0, 1, 2)
|
||||
.scale('C major')
|
||||
.note()
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('converts n values', () => {
|
||||
expect(
|
||||
n(seq(0, 1, 2))
|
||||
.scale('C major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('converts n values (mini notation)', () => {
|
||||
expect(
|
||||
n(seq(0, 1, 2))
|
||||
.scale('C:major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('converts n values (no tonic)', () => {
|
||||
expect(
|
||||
n(seq(0, 1, 2))
|
||||
.scale('major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('converts n values (explicit mini notation)', () => {
|
||||
expect(
|
||||
n(seq(0, 1, 2))
|
||||
.scale(mini('C:major'))
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C3', 'D3', 'E3']);
|
||||
});
|
||||
it('converts decorated n values', () => {
|
||||
expect(
|
||||
n(seq('0b', '1#', '-2', '3##', '4bb'))
|
||||
.scale('C major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['B2', 'Eb3', 'A2', 'G3', 'F3']);
|
||||
});
|
||||
it('produces silence for mixed sharps and flats', () => {
|
||||
expect(
|
||||
n(seq('0b#', '1#b', '2#b#'))
|
||||
.scale('C major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual([]);
|
||||
});
|
||||
it('snaps notes (upwards) to scale', () => {
|
||||
const inputNotes = ['Cb', 'Eb', 'G', 'A#', 'Bb'];
|
||||
const expectedNotes = ['B2', 'E3', 'G3', 'B3', 'B3'];
|
||||
|
||||
expect(
|
||||
note(seq(inputNotes))
|
||||
.scale('C major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(expectedNotes);
|
||||
});
|
||||
it('snaps notes to the correct octave', () => {
|
||||
const inputNotes = ['Cb0', 'Eb4', 'G1', 'A#19', 'Bb8'];
|
||||
const expectedNotes = ['B#-1', 'D#4', 'G#1', 'A#19', 'A#8'];
|
||||
|
||||
expect(
|
||||
note(seq(inputNotes))
|
||||
.scale('A# minor') // A#, B#, C#, D#, E#, F#, G#
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(expectedNotes);
|
||||
});
|
||||
it('handles scale names provided with colons', () => {
|
||||
const inputNotes = ['Cb', 'E', 'G', 'A#', 'Bb'];
|
||||
const expectedNotes = ['A#2', 'D#3', 'G#3', 'A#3', 'A#3'];
|
||||
|
||||
expect(
|
||||
note(seq(inputNotes))
|
||||
.scale('F#:pentatonic') // F#, G#, A#, C#, and D#
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(expectedNotes);
|
||||
});
|
||||
});
|
||||
it('scale with n values', () => {
|
||||
expect(
|
||||
n(0, 1, 2)
|
||||
.scale('C major')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).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']);
|
||||
});
|
||||
it('transposes note numbers with interval numbers', () => {
|
||||
expect(
|
||||
note(40, 40, 40)
|
||||
.transpose(0, 1, 2)
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual([40, 41, 42]);
|
||||
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
|
||||
});
|
||||
it('transposes note numbers with interval strings', () => {
|
||||
expect(
|
||||
note(40, 40, 40)
|
||||
.transpose('1P', '2M', '3m')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual([40, 42, 43]);
|
||||
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
|
||||
});
|
||||
it('transposes note strings with interval numbers', () => {
|
||||
expect(
|
||||
note('c', 'c', 'c')
|
||||
.transpose(0, 1, 2)
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C', 'Db', 'D']);
|
||||
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
|
||||
});
|
||||
it('transposes note strings with interval strings', () => {
|
||||
expect(
|
||||
note('c', 'c', 'c')
|
||||
.transpose('1P', '2M', '3m')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C', 'D', 'Eb']);
|
||||
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
|
||||
describe('transpose', () => {
|
||||
it('transposes note numbers with interval numbers', () => {
|
||||
expect(
|
||||
note(seq(40, 40, 40))
|
||||
.transpose(0, 1, 2)
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual([40, 41, 42]);
|
||||
expect(seq(40, 40, 40).transpose(0, 1, 2).firstCycleValues).toEqual([40, 41, 42]);
|
||||
});
|
||||
it('transposes note numbers with interval strings', () => {
|
||||
expect(
|
||||
note(seq(40, 40, 40))
|
||||
.transpose('1P', '2M', '3m')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual([40, 42, 43]);
|
||||
expect(seq(40, 40, 40).transpose('1P', '2M', '3m').firstCycleValues).toEqual([40, 42, 43]);
|
||||
});
|
||||
it('transposes note strings with interval numbers', () => {
|
||||
expect(
|
||||
note(seq('c', 'c', 'c'))
|
||||
.transpose(0, 1, 2)
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C', 'Db', 'D']);
|
||||
expect(seq('c', 'c', 'c').transpose(0, 1, 2).firstCycleValues).toEqual(['C', 'Db', 'D']);
|
||||
});
|
||||
it('transposes note strings with interval strings', () => {
|
||||
expect(
|
||||
note(seq('c', 'c', 'c'))
|
||||
.transpose('1P', '2M', '3m')
|
||||
.firstCycleValues.map((h) => h.note),
|
||||
).toEqual(['C', 'D', 'Eb']);
|
||||
expect(seq('c', 'c', 'c').transpose('1P', '2M', '3m').firstCycleValues).toEqual(['C', 'D', 'Eb']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+122
-69
@@ -6,19 +6,28 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
||||
import { register, _mod, silence, logger, pure, isNote } from '@strudel/core';
|
||||
import { stepInNamedScale } from './tonleiter.mjs';
|
||||
import { stepInNamedScale, nearestNumberIndex } from './tonleiter.mjs';
|
||||
import { noteToMidi } from '../core/util.mjs';
|
||||
|
||||
const octavesInterval = (octaves) => (octaves <= 0 ? -1 : 1) + octaves * 7 + 'P';
|
||||
|
||||
function scaleStep(step, scale) {
|
||||
scale = scale.replaceAll(':', ' ');
|
||||
step = Math.ceil(step);
|
||||
let { intervals, tonic, empty } = Scale.get(scale);
|
||||
if ((empty && isNote(scale)) || (!empty && !tonic)) {
|
||||
throw new Error(`incomplete scale. Make sure to use ":" instead of spaces, example: .scale("C:major")`);
|
||||
function getScale(scaleName) {
|
||||
scaleName = scaleName.replaceAll(':', ' ');
|
||||
const scale = Scale.get(scaleName);
|
||||
const { tonic, empty } = scale;
|
||||
if ((empty && isNote(scaleName)) || (empty && !tonic)) {
|
||||
throw new Error(
|
||||
`Scale name ${scaleName} is incomplete. Make sure to use ":" instead of spaces, example: .scale("C:major")`,
|
||||
);
|
||||
} else if (empty) {
|
||||
throw new Error(`invalid scale "${scale}"`);
|
||||
throw new Error(`Invalid scale name "${scaleName}"`);
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
function scaleStep(step, scale) {
|
||||
step = Math.ceil(step);
|
||||
let { intervals, tonic } = getScale(scale);
|
||||
tonic = tonic || 'C';
|
||||
const { pc, oct = 3 } = Note.get(tonic);
|
||||
const octaveOffset = Math.floor(step / intervals.length);
|
||||
@@ -30,8 +39,7 @@ function scaleStep(step, scale) {
|
||||
// transpose note inside scale by offset steps
|
||||
// function scaleOffset(scale: string, offset: number, note: string) {
|
||||
function scaleOffset(scale, offset, note) {
|
||||
let [tonic, scaleName] = Scale.tokenize(scale);
|
||||
let { notes } = Scale.get(`${tonic} ${scaleName}`);
|
||||
let { notes } = getScale(scale);
|
||||
notes = notes.map((note) => Note.get(note).pc); // use only pc!
|
||||
offset = Number(offset);
|
||||
if (isNaN(offset)) {
|
||||
@@ -88,13 +96,14 @@ function scaleOffset(scale, offset, note) {
|
||||
* @returns Pattern
|
||||
* @memberof Pattern
|
||||
* @name transpose
|
||||
* @synonyms trans
|
||||
* @example
|
||||
* "c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note()
|
||||
* @example
|
||||
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()
|
||||
*/
|
||||
|
||||
export const transpose = register('transpose', function (intervalOrSemitones, pat) {
|
||||
export const { transpose, trans } = register(['transpose', 'trans'], function transposeFn(intervalOrSemitones, pat) {
|
||||
return pat.withHap((hap) => {
|
||||
const note = hap.value.note ?? hap.value;
|
||||
if (typeof note === 'number') {
|
||||
@@ -119,10 +128,7 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa
|
||||
const interval = !isNaN(Number(intervalOrSemitones))
|
||||
? Interval.fromSemitones(intervalOrSemitones)
|
||||
: String(intervalOrSemitones);
|
||||
// TODO: move simplify to player to preserve enharmonics
|
||||
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
|
||||
// TODO: check if this is still relevant..
|
||||
const targetNote = Note.simplify(Note.transpose(note, interval));
|
||||
const targetNote = Note.transpose(note, interval);
|
||||
if (typeof hap.value === 'object') {
|
||||
return hap.withValue(() => ({ ...hap.value, note: targetNote }));
|
||||
}
|
||||
@@ -142,6 +148,7 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa
|
||||
* @name scaleTranspose
|
||||
* @param {offset} offset number of steps inside the scale
|
||||
* @returns Pattern
|
||||
* @synonyms scaleTrans, strans
|
||||
* @example
|
||||
* "-8 [2,4,6]"
|
||||
* .scale('C4 bebop major')
|
||||
@@ -149,25 +156,79 @@ export const transpose = register('transpose', function (intervalOrSemitones, pa
|
||||
* .note()
|
||||
*/
|
||||
|
||||
export const scaleTranspose = register('scaleTranspose', function (offset /* : number | string */, pat) {
|
||||
return pat.withHap((hap) => {
|
||||
if (!hap.context.scale) {
|
||||
throw new Error('can only use scaleTranspose after .scale');
|
||||
export const { scaleTranspose, scaleTrans, strans } = register(
|
||||
['scaleTranspose', 'scaleTrans', 'strans'],
|
||||
function (offset /* : number | string */, pat) {
|
||||
return pat.withHap((hap) => {
|
||||
if (!hap.context.scale) {
|
||||
throw new Error('can only use scaleTranspose after .scale');
|
||||
}
|
||||
if (typeof hap.value === 'object')
|
||||
return hap.withValue(() => ({
|
||||
...hap.value,
|
||||
note: scaleOffset(hap.context.scale, Number(offset), hap.value.note),
|
||||
}));
|
||||
if (typeof hap.value !== 'string') {
|
||||
throw new Error('can only use scaleTranspose with notes');
|
||||
}
|
||||
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Converts a step value, which is a number optionally decorated with sharps and flats,
|
||||
// to a number and an `offset` number of semitones
|
||||
function _convertStepToNumberAndOffset(step) {
|
||||
let asNumber = Number(step);
|
||||
let offset = 0;
|
||||
if (isNaN(asNumber)) {
|
||||
step = String(step);
|
||||
// Check to see if the step matches the expected format:
|
||||
// - A number (possibly negative)
|
||||
// - Some number of sharps or flats (but not both)
|
||||
const match = /^(-?\d+)(#+|b+)?$/.exec(step);
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`invalid scale step "${step}", expected number or integer with optional # b suffixes`);
|
||||
}
|
||||
if (typeof hap.value === 'object')
|
||||
return hap.withValue(() => ({
|
||||
...hap.value,
|
||||
note: scaleOffset(hap.context.scale, Number(offset), hap.value.note),
|
||||
}));
|
||||
if (typeof hap.value !== 'string') {
|
||||
throw new Error('can only use scaleTranspose with notes');
|
||||
}
|
||||
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
|
||||
});
|
||||
});
|
||||
asNumber = Number(match[1]);
|
||||
// These decorations will determine the semitone offset based on the number of
|
||||
// sharps or flats
|
||||
const decorations = match[2] || '';
|
||||
offset = decorations[0] === '#' ? decorations.length : -decorations.length;
|
||||
}
|
||||
return [asNumber, offset];
|
||||
}
|
||||
|
||||
let scaleToMidisAndNotes = {};
|
||||
// Finds the nearest scale note to `note`
|
||||
function _getNearestScaleNote(scaleName, note, preferHigher = true) {
|
||||
let noteMidi = typeof note === 'string' ? noteToMidi(note) : note;
|
||||
if (scaleToMidisAndNotes[scaleName] === undefined) {
|
||||
const { intervals, tonic } = getScale(scaleName);
|
||||
const { pc } = Note.get(tonic);
|
||||
const expandedIntervals = intervals.concat('8P'); // add the octave for wrapping
|
||||
const sNotes = expandedIntervals.map((interval) => Note.transpose(pc + '0', interval));
|
||||
const sMidi = sNotes.map(noteToMidi);
|
||||
// Cache
|
||||
scaleToMidisAndNotes[scaleName] = [sMidi, sNotes];
|
||||
}
|
||||
const [scaleMidis, scaleNotes] = scaleToMidisAndNotes[scaleName];
|
||||
const rootMidi = scaleMidis[0];
|
||||
const octaveDiff = Math.floor((noteMidi - rootMidi) / 12);
|
||||
const alignedMidis = scaleMidis.map((m) => m + 12 * octaveDiff);
|
||||
const noteIdx = nearestNumberIndex(noteMidi, alignedMidis, preferHigher);
|
||||
const noteMatch = scaleNotes[noteIdx];
|
||||
return Note.transpose(noteMatch, Interval.fromSemitones(12 * octaveDiff));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) or quantizes notes to a scale.
|
||||
*
|
||||
* When describing notes via numbers, note that negative numbers can be used to wrap backwards
|
||||
* in the scale as well as sharps or flats (but not both) to produce notes outside of the scale.
|
||||
*
|
||||
* Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
||||
*
|
||||
* 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).
|
||||
*
|
||||
@@ -186,6 +247,12 @@ export const scaleTranspose = register('scaleTranspose', function (offset /* : n
|
||||
* n(rand.range(0,12).segment(8))
|
||||
* .scale("C:ritusen")
|
||||
* .s("piano")
|
||||
* @example
|
||||
* n("<[0,7b] [-4# -4] [-2,7##] 4 [0,7] [-4# -4b] [-2,7###] 4b>*4")
|
||||
* .scale("C:<major minor>/2")
|
||||
* .s("piano")
|
||||
* @example
|
||||
* note("C1*16").transpose(irand(36)).scale('Cb2 major').scaleTranspose(3)
|
||||
*/
|
||||
|
||||
export const scale = register(
|
||||
@@ -199,49 +266,35 @@ export const scale = register(
|
||||
pat
|
||||
.fmap((value) => {
|
||||
const isObject = typeof value === 'object';
|
||||
let step = isObject ? value.n : value;
|
||||
if (isObject) {
|
||||
// The case where the note has been defined via `n` or `pure`
|
||||
if (!isObject || (isObject && ('n' in value || 'value' in value))) {
|
||||
const step = isObject ? (value.n ?? value.value) : value;
|
||||
delete value.n; // remove n so it won't cause trouble
|
||||
}
|
||||
if (isNote(step)) {
|
||||
// legacy..
|
||||
return pure(step);
|
||||
}
|
||||
let asNumber = Number(step);
|
||||
let semitones = 0;
|
||||
if (isNaN(asNumber)) {
|
||||
step = String(step);
|
||||
if (!/^[-+]?\d+(#*|b*){1}$/.test(step)) {
|
||||
logger(
|
||||
`[tonal] invalid scale step "${step}", expected number or integer with optional # b suffixes`,
|
||||
'error',
|
||||
);
|
||||
if (isNote(step)) {
|
||||
// legacy..
|
||||
return pure(step);
|
||||
}
|
||||
try {
|
||||
const [number, offset] = _convertStepToNumberAndOffset(step);
|
||||
let note;
|
||||
if (isObject && value.anchor) {
|
||||
note = stepInNamedScale(number, scale, value.anchor);
|
||||
} else {
|
||||
note = scaleStep(number, scale);
|
||||
}
|
||||
if (offset != 0) note = Note.transpose(note, Interval.fromSemitones(offset));
|
||||
value = pure(isObject ? { ...value, note } : note);
|
||||
} catch (err) {
|
||||
logger(`[tonal] ${err.message}`, 'error');
|
||||
return silence;
|
||||
}
|
||||
const isharp = step.indexOf('#');
|
||||
if (isharp >= 0) {
|
||||
asNumber = Number(step.substring(0, isharp));
|
||||
semitones = step.length - isharp;
|
||||
} else {
|
||||
const iflat = step.indexOf('b');
|
||||
asNumber = Number(step.substring(0, iflat));
|
||||
semitones = iflat - step.length;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
let note;
|
||||
if (isObject && value.anchor) {
|
||||
note = stepInNamedScale(asNumber, scale, value.anchor);
|
||||
} else {
|
||||
note = scaleStep(asNumber, scale);
|
||||
}
|
||||
if (semitones != 0) note = Note.transpose(note, Interval.fromSemitones(semitones));
|
||||
value = pure(isObject ? { ...value, note } : note);
|
||||
} catch (err) {
|
||||
logger(`[tonal] ${err.message}`, 'error');
|
||||
value = silence;
|
||||
// The case where the note has been defined via `note`
|
||||
else {
|
||||
const note = _getNearestScaleNote(scale, value.note);
|
||||
return pure(isObject ? { ...value, note } : note);
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.outerJoin()
|
||||
// legacy:
|
||||
|
||||
@@ -101,11 +101,11 @@ export function nearestNumberIndex(target, numbers, preferHigher) {
|
||||
let scaleSteps = {}; // [scaleName]: semitones[]
|
||||
|
||||
export function stepInNamedScale(step, scale, anchor, preferHigher) {
|
||||
let [root, scaleName] = Scale.tokenize(scale);
|
||||
const [root, scaleName] = Scale.tokenize(scale);
|
||||
const rootMidi = x2midi(root);
|
||||
const rootChroma = midi2chroma(rootMidi);
|
||||
if (!scaleSteps[scaleName]) {
|
||||
let { intervals } = Scale.get(`C ${scaleName}`);
|
||||
const { intervals } = Scale.get(`C ${scaleName}`);
|
||||
// cache result
|
||||
scaleSteps[scaleName] = intervals.map(step2semitones);
|
||||
}
|
||||
@@ -222,6 +222,7 @@ export const Note = {
|
||||
};
|
||||
|
||||
// TODO: support octave numbers
|
||||
// Example: Note("Bb3").transpose("c3")
|
||||
export function transpose(note, step) {
|
||||
// example: E, 3
|
||||
const stepNumber = Step.tokenize(step)[1]; // 3
|
||||
@@ -235,5 +236,3 @@ export function transpose(note, step) {
|
||||
const offsetAccidentals = accidentalString(Step.accidentals(step) + Note.accidentals(note) + stepIndex - indexOffset); // "we need to add a # to to the G to make it a major third from E"
|
||||
return [targetNote, offsetAccidentals].join('');
|
||||
}
|
||||
|
||||
//Note("Bb3").transpose("c3")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/transpiler",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -8,6 +8,16 @@ export function registerWidgetType(type) {
|
||||
widgetMethods.push(type);
|
||||
}
|
||||
|
||||
let languages = new Map();
|
||||
// config = { getLocations: (code: string, offset?: number) => number[][] }
|
||||
// see mondough.mjs for example use
|
||||
// the language will kick in when the code contains a template literal of type
|
||||
// example: mondo`...` will use language of type "mondo"
|
||||
// TODO: refactor tidal.mjs to use this
|
||||
export function registerLanguage(type, config) {
|
||||
languages.set(type, config);
|
||||
}
|
||||
|
||||
export function transpiler(input, options = {}) {
|
||||
const { wrapAsync = false, addReturn = true, emitMiniLocations = true, emitWidgets = true } = options;
|
||||
|
||||
@@ -19,14 +29,33 @@ export function transpiler(input, options = {}) {
|
||||
|
||||
let miniLocations = [];
|
||||
const collectMiniLocations = (value, node) => {
|
||||
const leafLocs = getLeafLocations(`"${value}"`, node.start, input);
|
||||
miniLocations = miniLocations.concat(leafLocs);
|
||||
const minilang = languages.get('minilang');
|
||||
if (minilang) {
|
||||
const code = `[${value}]`;
|
||||
const locs = minilang.getLocations(code, node.start);
|
||||
miniLocations = miniLocations.concat(locs);
|
||||
} else {
|
||||
const leafLocs = getLeafLocations(`"${value}"`, node.start, input);
|
||||
miniLocations = miniLocations.concat(leafLocs);
|
||||
}
|
||||
};
|
||||
let widgets = [];
|
||||
|
||||
walk(ast, {
|
||||
enter(node, parent /* , prop, index */) {
|
||||
if (isTidalTeplateLiteral(node)) {
|
||||
if (isLanguageLiteral(node)) {
|
||||
const { name } = node.tag;
|
||||
const language = languages.get(name);
|
||||
const code = node.quasi.quasis[0].value.raw;
|
||||
const offset = node.quasi.start + 1;
|
||||
if (emitMiniLocations) {
|
||||
const locs = language.getLocations(code, offset);
|
||||
miniLocations = miniLocations.concat(locs);
|
||||
}
|
||||
this.skip();
|
||||
return this.replace(languageWithLocation(name, code, offset));
|
||||
}
|
||||
if (isTemplateLiteral(node, 'tidal')) {
|
||||
const raw = node.quasi.quasis[0].value.raw;
|
||||
const offset = node.quasi.start + 1;
|
||||
if (emitMiniLocations) {
|
||||
@@ -84,8 +113,18 @@ export function transpiler(input, options = {}) {
|
||||
leave(node, parent, prop, index) {},
|
||||
});
|
||||
|
||||
const { body } = ast;
|
||||
if (!body?.[body.length - 1]?.expression) {
|
||||
let { body } = ast;
|
||||
|
||||
if (!body.length) {
|
||||
console.warn('empty body -> fallback to silence');
|
||||
body.push({
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'Identifier',
|
||||
name: 'silence',
|
||||
},
|
||||
});
|
||||
} else if (!body?.[body.length - 1]?.expression) {
|
||||
throw new Error('unexpected ast format without body expression');
|
||||
}
|
||||
|
||||
@@ -120,11 +159,18 @@ function isBackTickString(node, parent) {
|
||||
|
||||
function miniWithLocation(value, node) {
|
||||
const { start: fromOffset } = node;
|
||||
|
||||
const minilang = languages.get('minilang');
|
||||
let name = 'm';
|
||||
if (minilang && minilang.name) {
|
||||
name = minilang.name; // name is expected to be exported from the package of the minilang
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
name: 'm',
|
||||
name,
|
||||
},
|
||||
arguments: [
|
||||
{ type: 'Literal', value },
|
||||
@@ -219,12 +265,16 @@ function labelToP(node) {
|
||||
};
|
||||
}
|
||||
|
||||
function isLanguageLiteral(node) {
|
||||
return node.type === 'TaggedTemplateExpression' && languages.has(node.tag.name);
|
||||
}
|
||||
|
||||
// tidal highlighting
|
||||
// this feels kind of stupid, when we also know the location inside the string op (tidal.mjs)
|
||||
// but maybe it's the only way
|
||||
|
||||
function isTidalTeplateLiteral(node) {
|
||||
return node.type === 'TaggedTemplateExpression' && node.tag.name === 'tidal';
|
||||
function isTemplateLiteral(node, value) {
|
||||
return node.type === 'TaggedTemplateExpression' && node.tag.name === value;
|
||||
}
|
||||
|
||||
function collectHaskellMiniLocations(haskellCode, offset) {
|
||||
@@ -262,3 +312,18 @@ function tidalWithLocation(value, offset) {
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
function languageWithLocation(name, value, offset) {
|
||||
return {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
name: name,
|
||||
},
|
||||
arguments: [
|
||||
{ type: 'Literal', value },
|
||||
{ type: 'Literal', value: offset },
|
||||
],
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/web",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.5",
|
||||
"description": "Easy to setup, opiniated bundle of Strudel for the browser.",
|
||||
"module": "web.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -7,4 +7,5 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
export * from './webaudio.mjs';
|
||||
export * from './scope.mjs';
|
||||
export * from './spectrum.mjs';
|
||||
export * from './supradough.mjs';
|
||||
export * from 'superdough';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/webaudio",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.5",
|
||||
"description": "Web Audio helpers for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
@@ -35,7 +35,8 @@
|
||||
"dependencies": {
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/draw": "workspace:*",
|
||||
"superdough": "workspace:*"
|
||||
"superdough": "workspace:*",
|
||||
"supradough": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^6.0.11"
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Pattern } from '@strudel/core';
|
||||
import { connectToDestination, getAudioContext, getWorklet } from 'superdough';
|
||||
|
||||
let doughWorklet;
|
||||
|
||||
function initDoughWorklet() {
|
||||
const ac = getAudioContext();
|
||||
doughWorklet = getWorklet(
|
||||
ac,
|
||||
'dough-processor',
|
||||
{},
|
||||
{
|
||||
outputChannelCount: [2],
|
||||
},
|
||||
);
|
||||
connectToDestination(doughWorklet); // channels?
|
||||
}
|
||||
|
||||
const soundMap = new Map();
|
||||
const loadedSounds = new Map();
|
||||
|
||||
Pattern.prototype.supradough = function () {
|
||||
return this.onTrigger((hap, __, cps, begin) => {
|
||||
hap.value._begin = begin;
|
||||
hap.value._duration = hap.duration / cps;
|
||||
!doughWorklet && initDoughWorklet();
|
||||
const s = (hap.value.bank ? hap.value.bank + '_' : '') + hap.value.s;
|
||||
const n = hap.value.n ?? 0;
|
||||
const soundKey = `${s}:${n}`;
|
||||
if (soundMap.has(s)) {
|
||||
hap.value.s = soundKey; // dough.mjs is unaware of bank and n (only maps keys to buffers)
|
||||
}
|
||||
if (soundMap.has(s) && !loadedSounds.has(soundKey)) {
|
||||
const urls = soundMap.get(s);
|
||||
const url = urls[n % urls.length];
|
||||
console.log(`load ${soundKey} from ${url}`);
|
||||
const loadSample = fetchSample(url);
|
||||
loadedSounds.set(soundKey, loadSample);
|
||||
loadSample.then(({ channels, sampleRate }) =>
|
||||
doughWorklet.port.postMessage({
|
||||
sample: soundKey,
|
||||
channels,
|
||||
sampleRate,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
doughWorklet.port.postMessage({ spawn: hap.value });
|
||||
}, 1);
|
||||
};
|
||||
|
||||
function githubPath(base, subpath = '') {
|
||||
if (!base.startsWith('github:')) {
|
||||
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||
}
|
||||
let [_, path] = base.split('github:');
|
||||
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
if (path.split('/').length === 2) {
|
||||
// assume main as default branch if none set
|
||||
path += '/main';
|
||||
}
|
||||
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
||||
}
|
||||
export async function fetchSampleMap(url) {
|
||||
if (url.startsWith('github:')) {
|
||||
url = githubPath(url, 'strudel.json');
|
||||
}
|
||||
if (url.startsWith('local:')) {
|
||||
url = `http://localhost:5432`;
|
||||
}
|
||||
if (url.startsWith('shabda:')) {
|
||||
let [_, path] = url.split('shabda:');
|
||||
url = `https://shabda.ndre.gr/${path}.json?strudel=1`;
|
||||
}
|
||||
if (url.startsWith('shabda/speech')) {
|
||||
let [_, path] = url.split('shabda/speech');
|
||||
path = path.startsWith('/') ? path.substring(1) : path;
|
||||
let [params, words] = path.split(':');
|
||||
let gender = 'f';
|
||||
let language = 'en-GB';
|
||||
if (params) {
|
||||
[language, gender] = params.split('/');
|
||||
}
|
||||
url = `https://shabda.ndre.gr/speech/${words}.json?gender=${gender}&language=${language}&strudel=1'`;
|
||||
}
|
||||
if (typeof fetch !== 'function') {
|
||||
// not a browser
|
||||
return;
|
||||
}
|
||||
const base = url.split('/').slice(0, -1).join('/');
|
||||
if (typeof fetch === 'undefined') {
|
||||
// skip fetch when in node / testing
|
||||
return;
|
||||
}
|
||||
const json = await fetch(url)
|
||||
.then((res) => res.json())
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
throw new Error(`error loading "${url}"`);
|
||||
});
|
||||
return [json, json._base || base];
|
||||
}
|
||||
|
||||
// for some reason, only piano and flute work.. is it because mp3??
|
||||
|
||||
async function fetchSample(url) {
|
||||
const buffer = await fetch(url)
|
||||
.then((res) => res.arrayBuffer())
|
||||
.then((buf) => getAudioContext().decodeAudioData(buf));
|
||||
let channels = [];
|
||||
for (let i = 0; i < buffer.numberOfChannels; i++) {
|
||||
channels.push(buffer.getChannelData(i));
|
||||
}
|
||||
return { channels, sampleRate: buffer.sampleRate };
|
||||
}
|
||||
|
||||
export async function doughsamples(sampleMap, baseUrl) {
|
||||
if (typeof sampleMap === 'string') {
|
||||
const [json, base] = await fetchSampleMap(sampleMap);
|
||||
// console.log('json', json, 'base', base);
|
||||
return doughsamples(json, base);
|
||||
}
|
||||
Object.entries(sampleMap).map(async ([key, urls]) => {
|
||||
if (key !== '_base') {
|
||||
urls = urls.map((url) => baseUrl + url);
|
||||
// console.log('set', key, urls);
|
||||
soundMap.set(key, urls);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -5,7 +5,12 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import * as strudel from '@strudel/core';
|
||||
import { superdough, getAudioContext, setLogger, doughTrigger } from 'superdough';
|
||||
import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough';
|
||||
import './supradough.mjs';
|
||||
import { workletUrl } from 'supradough';
|
||||
|
||||
registerWorklet(workletUrl);
|
||||
|
||||
const { Pattern, logger, repl } = strudel;
|
||||
|
||||
setLogger(logger);
|
||||
@@ -15,13 +20,10 @@ const hap2value = (hap) => {
|
||||
return hap.value;
|
||||
};
|
||||
|
||||
export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(hap), t - ct, hap.duration / cps, cps);
|
||||
// uses more precise, absolute t if available, see https://codeberg.org/uzu/strudel/pulls/1004
|
||||
export const webaudioOutput = (hap, deadline, hapDuration, cps, t) =>
|
||||
superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration);
|
||||
|
||||
Pattern.prototype.webaudio = function () {
|
||||
return this.onTrigger(webaudioOutputTrigger);
|
||||
// uses more precise, absolute t if available, see https://github.com/tidalcycles/strudel/pull/1004
|
||||
// TODO: refactor output callbacks to eliminate deadline
|
||||
export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => {
|
||||
return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf());
|
||||
};
|
||||
|
||||
export function webaudioRepl(options = {}) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/xen",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.4",
|
||||
"description": "Xenharmonic API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -139,10 +139,10 @@ Tune.prototype.MIDI = function(stepIn,octaveIn) {
|
||||
|
||||
/* Load a new scale */
|
||||
|
||||
Tune.prototype.loadScale = function(name){
|
||||
Tune.prototype.loadScale = function(scale){
|
||||
|
||||
/* load the scale */
|
||||
var freqs = TuningList[name].frequencies
|
||||
var freqs = isArrayOfNumbers(scale) ? scale : TuningList[scale].frequencies
|
||||
this.scale = []
|
||||
for (var i=0;i<freqs.length-1;i++) {
|
||||
this.scale.push(freqs[i]/freqs[0])
|
||||
@@ -207,8 +207,13 @@ Tune.prototype.search = function(letters) {
|
||||
return possible
|
||||
}
|
||||
|
||||
Tune.prototype.isValidScale = function(name) {
|
||||
return !!TuningList[name];
|
||||
function isArrayOfNumbers(arg) {
|
||||
return Array.isArray(arg) && arg.length > 0 && arg.every(item => typeof item === 'number' && !isNaN(item));
|
||||
}
|
||||
|
||||
/* allow an array of values too */
|
||||
Tune.prototype.isValidScale = function(scale) {
|
||||
return !!TuningList[scale] || isArrayOfNumbers(scale) ;
|
||||
}
|
||||
|
||||
/* Return a collection of notes as an array */
|
||||
|
||||
Generated
+151
-10
@@ -201,8 +201,8 @@ importers:
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
'@replit/codemirror-vim':
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
specifier: ^6.3.0
|
||||
version: 6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
'@replit/codemirror-vscode-keymap':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2(@codemirror/autocomplete@6.18.4)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
@@ -347,6 +347,37 @@ importers:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
|
||||
packages/mondo:
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
vitest:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
|
||||
packages/mondough:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@strudel/transpiler':
|
||||
specifier: workspace:*
|
||||
version: link:../transpiler
|
||||
mondolang:
|
||||
specifier: workspace:*
|
||||
version: link:../mondo
|
||||
devDependencies:
|
||||
mondo:
|
||||
specifier: '*'
|
||||
version: 0.4.4
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
vitest:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(@types/debug@4.1.12)(@types/node@22.10.10)(@vitest/ui@3.0.4)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
|
||||
packages/motion:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
@@ -486,6 +517,18 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../vite-plugin-bundle-audioworklet
|
||||
|
||||
packages/supradough:
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
version: 6.0.11(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.37.0)(yaml@2.7.0)
|
||||
vite-plugin-bundle-audioworklet:
|
||||
specifier: workspace:*
|
||||
version: link:../vite-plugin-bundle-audioworklet
|
||||
wav-encoder:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
|
||||
packages/tidal:
|
||||
dependencies:
|
||||
'@strudel/core':
|
||||
@@ -594,6 +637,9 @@ importers:
|
||||
superdough:
|
||||
specifier: workspace:*
|
||||
version: link:../superdough
|
||||
supradough:
|
||||
specifier: workspace:*
|
||||
version: link:../supradough
|
||||
devDependencies:
|
||||
vite:
|
||||
specifier: ^6.0.11
|
||||
@@ -683,6 +729,9 @@ importers:
|
||||
'@strudel/mini':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/mini
|
||||
'@strudel/mondo':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/mondough
|
||||
'@strudel/motion':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/motion
|
||||
@@ -2198,14 +2247,14 @@ packages:
|
||||
'@codemirror/state': ^6.0.1
|
||||
'@codemirror/view': ^6.3.0
|
||||
|
||||
'@replit/codemirror-vim@6.2.1':
|
||||
resolution: {integrity: sha512-qDAcGSHBYU5RrdO//qCmD8K9t6vbP327iCj/iqrkVnjbrpFhrjOt92weGXGHmTNRh16cUtkUZ7Xq7rZf+8HVow==}
|
||||
'@replit/codemirror-vim@6.3.0':
|
||||
resolution: {integrity: sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==}
|
||||
peerDependencies:
|
||||
'@codemirror/commands': ^6.0.0
|
||||
'@codemirror/language': ^6.1.0
|
||||
'@codemirror/search': ^6.2.0
|
||||
'@codemirror/state': ^6.0.1
|
||||
'@codemirror/view': ^6.0.3
|
||||
'@codemirror/commands': 6.x.x
|
||||
'@codemirror/language': 6.x.x
|
||||
'@codemirror/search': 6.x.x
|
||||
'@codemirror/state': 6.x.x
|
||||
'@codemirror/view': 6.x.x
|
||||
|
||||
'@replit/codemirror-vscode-keymap@6.0.2':
|
||||
resolution: {integrity: sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==}
|
||||
@@ -2975,6 +3024,10 @@ packages:
|
||||
resolution: {integrity: sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ==}
|
||||
engines: {node: '>= 14.0.0'}
|
||||
|
||||
amdefine@1.0.1:
|
||||
resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==}
|
||||
engines: {node: '>=0.4.2'}
|
||||
|
||||
ansi-align@3.0.1:
|
||||
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
||||
|
||||
@@ -3103,6 +3156,9 @@ packages:
|
||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
async@0.2.10:
|
||||
resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==}
|
||||
|
||||
async@3.2.6:
|
||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||
|
||||
@@ -3337,6 +3393,9 @@ packages:
|
||||
claviature@0.1.0:
|
||||
resolution: {integrity: sha512-Ai12axNwQ7x/F9QAj64RYKsgvi5Y33+X3GUSKAC/9s/adEws8TSSc0efeiqhKNGKBo6rT/c+CSCwSXzXxwxZzQ==}
|
||||
|
||||
cldr-plurals@1.0.0:
|
||||
resolution: {integrity: sha512-xGkehDsjj/ng1LtYiAzdiqDgqTQ/qmWafDlB6R8CfXbpXe4Ge2Tl4l7gxiA+yaD1WCTP3EVfwerdmVwfco7vxw==}
|
||||
|
||||
clean-stack@2.2.0:
|
||||
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -4378,6 +4437,9 @@ packages:
|
||||
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
globalize@0.1.1:
|
||||
resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==}
|
||||
|
||||
globals@11.12.0:
|
||||
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -4439,6 +4501,11 @@ packages:
|
||||
h3@1.14.0:
|
||||
resolution: {integrity: sha512-ao22eiONdgelqcnknw0iD645qW0s9NnrJHr5OBz4WOMdBdycfSas1EQf1wXRsm+PcB2Yoj43pjBPwqIpJQTeWg==}
|
||||
|
||||
handlebars@1.1.2:
|
||||
resolution: {integrity: sha512-6lekK3aSHE7XnEqEs+JWQJRSRdCqJYHEVjEfWWZv9pjLUYZNeP26EtlgOrpDCSX+k5N1m74urTbztgBOCko1Kg==}
|
||||
engines: {node: '>=0.4.7'}
|
||||
hasBin: true
|
||||
|
||||
handlebars@4.7.8:
|
||||
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
|
||||
engines: {node: '>=0.4.7'}
|
||||
@@ -5586,6 +5653,10 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
mondo@0.4.4:
|
||||
resolution: {integrity: sha512-3ot6iKwC9KZvwmyZ9dgPCnlt0O1GuBnK8QZyhnoJdtkIiRL9X/cmQuu88CbFSznQx6bq19xQdwJHpv8hggH62Q==}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
|
||||
mrmime@2.0.0:
|
||||
resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -5658,6 +5729,7 @@ packages:
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
node-fetch-native@1.6.6:
|
||||
resolution: {integrity: sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==}
|
||||
@@ -5827,6 +5899,9 @@ packages:
|
||||
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
optimist@0.3.7:
|
||||
resolution: {integrity: sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -6725,6 +6800,10 @@ packages:
|
||||
source-map-support@0.5.21:
|
||||
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
||||
|
||||
source-map@0.1.43:
|
||||
resolution: {integrity: sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
|
||||
source-map@0.5.7:
|
||||
resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -6740,6 +6819,7 @@ packages:
|
||||
source-map@0.8.0-beta.0:
|
||||
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
|
||||
engines: {node: '>= 8'}
|
||||
deprecated: The work that was done in this beta branch won't be included in future versions
|
||||
|
||||
sourcemap-codec@1.4.8:
|
||||
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
|
||||
@@ -7155,6 +7235,11 @@ packages:
|
||||
ufo@1.5.4:
|
||||
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
|
||||
|
||||
uglify-js@2.3.6:
|
||||
resolution: {integrity: sha512-T2LWWydxf5+Btpb0S/Gg/yKFmYjnX9jtQ4mdN9YRq73BhN21EhU0Dvw3wYDLqd3TooGUJlCKf3Gfyjjy/RTcWA==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
uglify-js@3.19.3:
|
||||
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
@@ -7173,6 +7258,9 @@ packages:
|
||||
underscore@1.13.7:
|
||||
resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==}
|
||||
|
||||
underscore@1.5.2:
|
||||
resolution: {integrity: sha512-yejOFsRnTJs0N9CK5Apzf6maDO2djxGoLLrlZlvGs2o9ZQuhIhDL18rtFyy4FBIbOkzA6+4hDgXbgz5EvDQCXQ==}
|
||||
|
||||
undici-types@6.20.0:
|
||||
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
|
||||
|
||||
@@ -7464,6 +7552,9 @@ packages:
|
||||
walk-up-path@3.0.1:
|
||||
resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==}
|
||||
|
||||
wav-encoder@1.3.0:
|
||||
resolution: {integrity: sha512-FXJdEu2qDOI+wbVYZpu21CS1vPEg5NaxNskBr4SaULpOJMrLE6xkH8dECa7PiS+ZoeyvP7GllWUAxPN3AvFSEw==}
|
||||
|
||||
wav@1.0.2:
|
||||
resolution: {integrity: sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==}
|
||||
|
||||
@@ -7555,6 +7646,10 @@ packages:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
wordwrap@0.0.3:
|
||||
resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
wordwrap@1.0.0:
|
||||
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
|
||||
|
||||
@@ -7582,6 +7677,7 @@ packages:
|
||||
|
||||
workbox-google-analytics@7.0.0:
|
||||
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
|
||||
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
|
||||
|
||||
workbox-navigation-preload@7.0.0:
|
||||
resolution: {integrity: sha512-juWCSrxo/fiMz3RsvDspeSLGmbgC0U9tKqcUPZBCf35s64wlaLXyn2KdHHXVQrb2cqF7I0Hc9siQalainmnXJA==}
|
||||
@@ -9520,7 +9616,7 @@ snapshots:
|
||||
'@codemirror/state': 6.5.1
|
||||
'@codemirror/view': 6.36.2
|
||||
|
||||
'@replit/codemirror-vim@6.2.1(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)':
|
||||
'@replit/codemirror-vim@6.3.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)':
|
||||
dependencies:
|
||||
'@codemirror/commands': 6.8.0
|
||||
'@codemirror/language': 6.10.8
|
||||
@@ -10438,6 +10534,9 @@ snapshots:
|
||||
'@algolia/requester-fetch': 5.20.0
|
||||
'@algolia/requester-node-http': 5.20.0
|
||||
|
||||
amdefine@1.0.1:
|
||||
optional: true
|
||||
|
||||
ansi-align@3.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@@ -10647,6 +10746,9 @@ snapshots:
|
||||
|
||||
async-function@1.0.0: {}
|
||||
|
||||
async@0.2.10:
|
||||
optional: true
|
||||
|
||||
async@3.2.6: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
@@ -10904,6 +11006,8 @@ snapshots:
|
||||
|
||||
claviature@0.1.0: {}
|
||||
|
||||
cldr-plurals@1.0.0: {}
|
||||
|
||||
clean-stack@2.2.0: {}
|
||||
|
||||
cli-boxes@3.0.0: {}
|
||||
@@ -12046,6 +12150,8 @@ snapshots:
|
||||
minipass: 4.2.8
|
||||
path-scurry: 1.11.1
|
||||
|
||||
globalize@0.1.1: {}
|
||||
|
||||
globals@11.12.0: {}
|
||||
|
||||
globals@14.0.0: {}
|
||||
@@ -12112,6 +12218,12 @@ snapshots:
|
||||
uncrypto: 0.1.3
|
||||
unenv: 1.10.0
|
||||
|
||||
handlebars@1.1.2:
|
||||
dependencies:
|
||||
optimist: 0.3.7
|
||||
optionalDependencies:
|
||||
uglify-js: 2.3.6
|
||||
|
||||
handlebars@4.7.8:
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
@@ -13695,6 +13807,13 @@ snapshots:
|
||||
requirejs: 2.3.7
|
||||
requirejs-config-file: 4.0.0
|
||||
|
||||
mondo@0.4.4:
|
||||
dependencies:
|
||||
cldr-plurals: 1.0.0
|
||||
globalize: 0.1.1
|
||||
handlebars: 1.1.2
|
||||
underscore: 1.5.2
|
||||
|
||||
mrmime@2.0.0: {}
|
||||
|
||||
ms@2.0.0: {}
|
||||
@@ -13987,6 +14106,10 @@ snapshots:
|
||||
is-docker: 2.2.1
|
||||
is-wsl: 2.2.0
|
||||
|
||||
optimist@0.3.7:
|
||||
dependencies:
|
||||
wordwrap: 0.0.3
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -15094,6 +15217,11 @@ snapshots:
|
||||
buffer-from: 1.1.2
|
||||
source-map: 0.6.1
|
||||
|
||||
source-map@0.1.43:
|
||||
dependencies:
|
||||
amdefine: 1.0.1
|
||||
optional: true
|
||||
|
||||
source-map@0.5.7: {}
|
||||
|
||||
source-map@0.6.1: {}
|
||||
@@ -15561,6 +15689,13 @@ snapshots:
|
||||
|
||||
ufo@1.5.4: {}
|
||||
|
||||
uglify-js@2.3.6:
|
||||
dependencies:
|
||||
async: 0.2.10
|
||||
optimist: 0.3.7
|
||||
source-map: 0.1.43
|
||||
optional: true
|
||||
|
||||
uglify-js@3.19.3:
|
||||
optional: true
|
||||
|
||||
@@ -15577,6 +15712,8 @@ snapshots:
|
||||
|
||||
underscore@1.13.7: {}
|
||||
|
||||
underscore@1.5.2: {}
|
||||
|
||||
undici-types@6.20.0: {}
|
||||
|
||||
unenv@1.10.0:
|
||||
@@ -15839,6 +15976,8 @@ snapshots:
|
||||
|
||||
walk-up-path@3.0.1: {}
|
||||
|
||||
wav-encoder@1.3.0: {}
|
||||
|
||||
wav@1.0.2:
|
||||
dependencies:
|
||||
buffer-alloc: 1.2.0
|
||||
@@ -15957,6 +16096,8 @@ snapshots:
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
wordwrap@0.0.3: {}
|
||||
|
||||
wordwrap@1.0.0: {}
|
||||
|
||||
workbox-background-sync@7.0.0:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user