Compare commits
339 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f17a4d045f | |||
| 8e1e83f3cf | |||
| 2d64b538e7 | |||
| b717615558 | |||
| 8c4f10464b | |||
| 018649c4f3 | |||
| 5ff59a878d | |||
| a20468b85f | |||
| 9648e35de2 | |||
| cd344cefe0 | |||
| 1f6a313f02 | |||
| 5cbeb227d8 | |||
| 2ab2cc5fc8 | |||
| 3dd667e825 | |||
| 482ebb695a | |||
| 218952b8b6 | |||
| 2e8a8b50df | |||
| 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 | |||
| e136e95375 | |||
| 004c0baa59 | |||
| 5f44ae9c4b | |||
| 66828e17d3 | |||
| b2f90ecb3b | |||
| ccf2a6de52 | |||
| 9416f317d4 | |||
| 75b6f59212 | |||
| 84238c8999 | |||
| 2d855cf5e8 | |||
| dd04d49c75 | |||
| 34874862ab | |||
| 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 | |||
| acd5661106 | |||
| b0b2b6a120 | |||
| 664d76cf1b | |||
| a901e2e387 | |||
| 82b14b5292 | |||
| 05bcd5e32f | |||
| 077556e7cb | |||
| 4cc2de9b7e | |||
| 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 | |||
| 01810f379f | |||
| f3c4afaa54 | |||
| a8757fecc8 | |||
| a1d181d609 | |||
| 440f1cb081 | |||
| 95193b0bfd | |||
| 66673d211e | |||
| 64ab3c9ba7 | |||
| 6c4aa12c60 | |||
| a8f2537e31 | |||
| 7e6876c282 | |||
| 1cc15c7e8d | |||
| 10ac7c353c | |||
| 9412bf60bf | |||
| c751bb7e5f | |||
| 28cd7c1681 | |||
| 011139eff8 | |||
| 184cb746f3 | |||
| 351314ccdf | |||
| e52017c0d8 | |||
| b253d437d3 | |||
| bebbe30b50 | |||
| 489d3f8d4d | |||
| cc4cad05b2 | |||
| 2cb22e94b5 | |||
| ee7d783099 | |||
| 9becd0a3f0 | |||
| 9738c90b82 | |||
| f7b58f2e3e | |||
| d856e567c1 | |||
| a7f044e1aa | |||
| 1f7d78c6ce | |||
| 4137f39495 | |||
| 0163c8ba25 | |||
| 912b3270aa | |||
| 2bc3d69fb0 | |||
| aa20526963 | |||
| 47de9e45ff | |||
| 0c193238c5 | |||
| 3428e18e7d | |||
| 8fba92f447 | |||
| 38e7fe606c | |||
| 59e7584bf7 | |||
| 7f50bcebd2 | |||
| d7b83e200c | |||
| db33707e52 | |||
| 84efa664ca | |||
| 94746b5fae | |||
| 1b7fbecf50 | |||
| 117cf7e18a | |||
| ae15bb7274 | |||
| 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 |
@@ -0,0 +1,37 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
# cache: "pnpm"
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Deploy
|
||||
run: |
|
||||
eval $(ssh-agent -s)
|
||||
echo "$SSH_PRIVATE_KEY" | ssh-add -
|
||||
apt update && apt install -y rsync
|
||||
mkdir ~/.ssh
|
||||
ssh-keyscan matrix.toplap.org > ~/.ssh/known_hosts
|
||||
rsync -atv --delete --delete-after --progress ./website/dist/ strudel@matrix.toplap.org:/home/strudel/deploy
|
||||
@@ -1,6 +1,6 @@
|
||||
name: Strudel tests
|
||||
|
||||
on: [push, pull_request]
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -11,6 +11,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: apt install ztd
|
||||
run: apt update && apt install -y zstd
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.2
|
||||
@@ -1,49 +0,0 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on: [workflow_dispatch]
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
deployments: write
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v2
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
# Upload entire repository
|
||||
path: "./website/dist"
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -130,3 +130,4 @@ fabric.properties
|
||||
|
||||
samples/*
|
||||
!samples/README.md
|
||||
.idea/
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# strudel
|
||||
|
||||
[](https://github.com/tidalcycles/strudel/actions) [](https://doi.org/10.5281/zenodo.6659278)
|
||||
Live coding patterns on the web
|
||||
https://strudel.cc/
|
||||
|
||||
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is a bit more stable now, but please continue to tread carefully.
|
||||
Development is moving to https://codeberg.org/uzu/strudel
|
||||
|
||||
- Try it here: <https://strudel.cc>
|
||||
- Docs: <https://strudel.cc/learn>
|
||||
@@ -31,19 +32,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
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ interfaces.
|
||||
|
||||
The Strudel REPL is available at <https://strudel.cc>,
|
||||
including an interactive tutorial. The repository is at
|
||||
<https://github.com/tidalcycles/strudel>, all the code is open source
|
||||
<https://codeberg.org/uzu/strudel/releases>, all the code is open source
|
||||
under the GPL-3.0 License.
|
||||
|
||||
# Acknowledgments
|
||||
@@ -128,7 +128,7 @@ For the future, it is planned to integrate alternative sound engines such as Gli
|
||||
# Links
|
||||
|
||||
The Strudel REPL is available at <https://strudel.cc>, including an interactive tutorial.
|
||||
The repository is at <https://github.com/tidalcycles/strudel>, all the code is open source under the GPL-3.0 License.
|
||||
The repository is at <https://codeberg.org/uzu/strudel/releases>, all the code is open source under the GPL-3.0 License.
|
||||
|
||||
# Acknowledgments
|
||||
|
||||
@@ -0,0 +1,954 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||
<meta name="generator" content="pandoc" />
|
||||
<meta name="date" content="2022-12-14" />
|
||||
<title>Strudel: live coding patterns on the Web</title>
|
||||
<style type="text/css">
|
||||
code {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
pre > code.sourceCode {
|
||||
white-space: pre;
|
||||
position: relative;
|
||||
}
|
||||
pre > code.sourceCode > span {
|
||||
display: inline-block;
|
||||
line-height: 1.25;
|
||||
}
|
||||
pre > code.sourceCode > span:empty {
|
||||
height: 1.2em;
|
||||
}
|
||||
.sourceCode {
|
||||
overflow: visible;
|
||||
}
|
||||
code.sourceCode > span {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
div.sourceCode {
|
||||
margin: 1em 0;
|
||||
}
|
||||
pre.sourceCode {
|
||||
margin: 0;
|
||||
}
|
||||
@media screen {
|
||||
div.sourceCode {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
@media print {
|
||||
pre > code.sourceCode {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
pre > code.sourceCode > span {
|
||||
text-indent: -5em;
|
||||
padding-left: 5em;
|
||||
}
|
||||
}
|
||||
pre.numberSource code {
|
||||
counter-reset: source-line 0;
|
||||
}
|
||||
pre.numberSource code > span {
|
||||
position: relative;
|
||||
left: -4em;
|
||||
counter-increment: source-line;
|
||||
}
|
||||
pre.numberSource code > span > a:first-child::before {
|
||||
content: counter(source-line);
|
||||
position: relative;
|
||||
left: -1em;
|
||||
text-align: right;
|
||||
vertical-align: baseline;
|
||||
border: none;
|
||||
display: inline-block;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
padding: 0 4px;
|
||||
width: 4em;
|
||||
color: #aaaaaa;
|
||||
}
|
||||
pre.numberSource {
|
||||
margin-left: 3em;
|
||||
border-left: 1px solid #aaaaaa;
|
||||
padding-left: 4px;
|
||||
}
|
||||
div.sourceCode {
|
||||
}
|
||||
@media screen {
|
||||
pre > code.sourceCode > span > a:first-child::before {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
code span.al {
|
||||
color: #ff0000;
|
||||
font-weight: bold;
|
||||
} /* Alert */
|
||||
code span.an {
|
||||
color: #60a0b0;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
} /* Annotation */
|
||||
code span.at {
|
||||
color: #7d9029;
|
||||
} /* Attribute */
|
||||
code span.bn {
|
||||
color: #40a070;
|
||||
} /* BaseN */
|
||||
code span.bu {
|
||||
color: #008000;
|
||||
} /* BuiltIn */
|
||||
code span.cf {
|
||||
color: #007020;
|
||||
font-weight: bold;
|
||||
} /* ControlFlow */
|
||||
code span.ch {
|
||||
color: #4070a0;
|
||||
} /* Char */
|
||||
code span.cn {
|
||||
color: #880000;
|
||||
} /* Constant */
|
||||
code span.co {
|
||||
color: #60a0b0;
|
||||
font-style: italic;
|
||||
} /* Comment */
|
||||
code span.cv {
|
||||
color: #60a0b0;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
} /* CommentVar */
|
||||
code span.do {
|
||||
color: #ba2121;
|
||||
font-style: italic;
|
||||
} /* Documentation */
|
||||
code span.dt {
|
||||
color: #902000;
|
||||
} /* DataType */
|
||||
code span.dv {
|
||||
color: #40a070;
|
||||
} /* DecVal */
|
||||
code span.er {
|
||||
color: #ff0000;
|
||||
font-weight: bold;
|
||||
} /* Error */
|
||||
code span.ex {
|
||||
} /* Extension */
|
||||
code span.fl {
|
||||
color: #40a070;
|
||||
} /* Float */
|
||||
code span.fu {
|
||||
color: #06287e;
|
||||
} /* Function */
|
||||
code span.im {
|
||||
color: #008000;
|
||||
font-weight: bold;
|
||||
} /* Import */
|
||||
code span.in {
|
||||
color: #60a0b0;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
} /* Information */
|
||||
code span.kw {
|
||||
color: #007020;
|
||||
font-weight: bold;
|
||||
} /* Keyword */
|
||||
code span.op {
|
||||
color: #666666;
|
||||
} /* Operator */
|
||||
code span.ot {
|
||||
color: #007020;
|
||||
} /* Other */
|
||||
code span.pp {
|
||||
color: #bc7a00;
|
||||
} /* Preprocessor */
|
||||
code span.sc {
|
||||
color: #4070a0;
|
||||
} /* SpecialChar */
|
||||
code span.ss {
|
||||
color: #bb6688;
|
||||
} /* SpecialString */
|
||||
code span.st {
|
||||
color: #4070a0;
|
||||
} /* String */
|
||||
code span.va {
|
||||
color: #19177c;
|
||||
} /* Variable */
|
||||
code span.vs {
|
||||
color: #4070a0;
|
||||
} /* VerbatimString */
|
||||
code span.wa {
|
||||
color: #60a0b0;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
} /* Warning */
|
||||
</style>
|
||||
<link rel="stylesheet" href="css/iclc.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<h1 class="title">Strudel: live coding patterns on the Web</h1>
|
||||
<ul id="authorlist">
|
||||
<li>true</li>
|
||||
<li>true</li>
|
||||
</ul>
|
||||
<h3 class="date">2022-12-14</h3>
|
||||
</div>
|
||||
|
||||
<h2 class="abstract">Abstract</h2>
|
||||
<div id="abstract">
|
||||
<p>
|
||||
This paper introduces Strudel, which brings the TidalCycles approach to live coding algorithmic patterns to
|
||||
native JavaScript and the web. We begin by giving a little background of the first year of development, before
|
||||
sharing some detail about its implementation and examples of use. We go on to outline the wide range of
|
||||
synthesis and other outputs available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), WebSerial and
|
||||
CSound, and introduce Strudel’s REPL live editor, including its built-in visualisations. We then compare Strudel
|
||||
with Tidal, the trade-offs involved between JavaScript and Haskell, and the unique capabilities offered by
|
||||
Strudel for aligning patterns.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h1 data-number="1" id="introduction"><span class="header-section-number">1</span> Introduction</h1>
|
||||
<p>
|
||||
In the following paper, we introduce <em>Strudel</em>, an alternative implementation of the TidalCycles (or
|
||||
‘Tidal’ for short) live coding system, using the JavaScript programming language. Strudel is an attempt to make
|
||||
live coding more accessible, by creating a system that runs entirely in the browser, while opening Tidal’s
|
||||
approach to algorithmic patterns
|
||||
<span class="citation" data-cites="mcleanAlgorithmicPattern2020a">(Mclean 2020)</span> up to modern audio/visual
|
||||
web technologies. The Strudel REPL is a live code editor dedicated to manipulating patterns while they play, with
|
||||
builtin visual feedback. While Strudel is written in JavaScript, the API is optimized for simplicity and
|
||||
readability by applying code transformations on the syntax tree level, allowing language operations that would
|
||||
otherwise be impossible. The application supports multiple ways to output sound, including Tone.js, Web Audio
|
||||
Nodes, OSC (Open Sound Control) messages, Web Serial, Web MIDI and Csound. The project is split into multiple
|
||||
packages, allowing granular reuse in other applications. Apart from TidalCycles, Strudel draws inspiration from
|
||||
many prior existing projects like TidalVortex
|
||||
<span class="citation" data-cites="mcleanTidalVortexZero2022">(McLean et al. 2022)</span>, Gibber
|
||||
<span class="citation" data-cites="robertsGibberLiveCoding2012">(Roberts and Kuchera-morin 2012)</span>, Estuary
|
||||
<span class="citation" data-cites="ogbornEstuaryBrowserbasedCollaborative2017">(Ogborn et al. 2017)</span>, Hydra
|
||||
<span class="citation" data-cites="jackHydra2022">(Jack [2022] 2022)</span>, Ocarina
|
||||
<span class="citation" data-cites="solomonPurescriptocarina2022">(Solomon [2021] 2022)</span> and Feedforward
|
||||
<span class="citation" data-cites="mcleanFeedforward2020">(McLean 2020)</span>. This paper expands the Strudel
|
||||
Demo paper for the Web Audio Conference 2022
|
||||
<span class="citation" data-cites="StrudelWAC2022">(Roos and McLean 2022)</span>.
|
||||
</p>
|
||||
<p>
|
||||
The first tentative commit to the Strudel project was on 22nd January 2022 by Alex McLean, with the core
|
||||
representation implemented over the following few days. Although this was his first attempt at a JavaScript-based
|
||||
application, by 27th January, Alex had managed to upload the initial version to the ‘npm’ javascript package
|
||||
database, sharing with the wider community for comment. By 4th February, Felix Roos had discovered Strudel and
|
||||
contributed a ‘REPL’ user interface to it, and then contributed a scheduler the next day, so that Strudel could
|
||||
already make sound. At this point, Alex and Felix shared ownership to the repository, and the project has since
|
||||
proved to be a productive confluence of Felix’s own work into music representation and visualisation, with Alex’s
|
||||
experience with making Tidal. Felix has since become the primary contributor to Strudel, with Alex continuing to
|
||||
jump between developing both Strudel and Tidal. Aspects of Strudel’s development have therefore fed back into
|
||||
TidalCycles, and both systems have maintained a shared conceptual underpinning. We plan to continue working
|
||||
towards feature parity between these systems, although within the syntactical trade-offs and library ecosystems of
|
||||
JavaScript and Haskell, some divergence is inevitable and healthy.
|
||||
</p>
|
||||
<p>
|
||||
Over the first year of its life, Strudel is now a fully-fledged live coding environment, porting Tidal’s core
|
||||
represention of patterns, pattern transformations, and mininotation for polymetric sequences, combined with a
|
||||
wealth of features for synthesising and visualising those patterns.
|
||||
</p>
|
||||
<h1 data-number="2" id="from-tidal-to-strudel-and-back">
|
||||
<span class="header-section-number">2</span> From Tidal to Strudel and back
|
||||
</h1>
|
||||
<p>
|
||||
As mentioned above, the original Tidal is implemented as a domain specific language (DSL) embedded in the Haskell
|
||||
pure functional programming language, and takes advantage of Haskell’s terse syntax and advanced, ‘strong’ type
|
||||
system. JavaScript on the other hand, is a multi-paradigm programming language, with a dynamic type system.
|
||||
Because Tidal leans heavily on many of Haskell’s more unique features, it was not always clear that it could
|
||||
meaningfully be ported to a multi-paradigm scripting language. However, this possibility was already demonstrated
|
||||
with an earlier port to Python [TidalVortex;
|
||||
<span class="citation" data-cites="mcleanTidalVortexZero2022">McLean et al. (2022)</span>], and we have now
|
||||
successfully implemented Tidal’s pure functional representation of patterns in Strudel, including partial
|
||||
application, currying, and the functor, applicative and monadic structures that underlie Tidal’s expressive
|
||||
pattern transformations. The result is a terse and highly composable system, where everything is either a pattern,
|
||||
or a function for combining and manipulating patterns, offering a rich creative ground for exploration.
|
||||
</p>
|
||||
<p>
|
||||
This development process has been far from a one-way port, however. The process of porting Tidal’s concepts has
|
||||
also opened up new possibilities, some just from revisiting every design decision, and some from the particular
|
||||
affordances and constraints offered by JavaScript. This has lead to new features (and indeed bugfixes) that have
|
||||
found their way back to Tidal where appropriate, and ongoing work that we will return to in the conclusion of this
|
||||
paper.
|
||||
</p>
|
||||
<h1 data-number="3" id="representing-patterns">
|
||||
<span class="header-section-number">3</span> Representing Patterns
|
||||
</h1>
|
||||
<p>
|
||||
Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions,
|
||||
adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern can
|
||||
output a set of events that happen within that time span. It depends on the structure of the Pattern how the
|
||||
events are located in time. From now on, this process of generating events from a time span will be called
|
||||
<strong>querying</strong>. Example:
|
||||
</p>
|
||||
<div class="sourceCode" id="cb1">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> pattern <span class="op">=</span> <span class="fu">sequence</span>(c3<span class="op">,</span> [e3<span class="op">,</span> g3])</span>
|
||||
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> events <span class="op">=</span> pattern<span class="op">.</span><span class="fu">queryArc</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">1</span>)</span>
|
||||
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="bu">console</span><span class="op">.</span><span class="fu">log</span>(events<span class="op">.</span><span class="fu">map</span>(e <span class="kw">=></span> e<span class="op">.</span><span class="fu">show</span>()))</span></code></pre>
|
||||
</div>
|
||||
<p>
|
||||
In this example, we create a pattern using the <code>sequence</code> function and <strong>query</strong> it for
|
||||
the time span from <code>0</code> to <code>1</code>. Those numbers represent units of time called
|
||||
<strong>cycles</strong>. The length of one cycle depends on the tempo, which defaults to one cycle per second. The
|
||||
resulting events are:
|
||||
</p>
|
||||
<div class="sourceCode" id="cb2">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>[{ <span class="dt">value</span><span class="op">:</span> <span class="st">'c3'</span><span class="op">,</span> <span class="dt">begin</span><span class="op">:</span> <span class="dv">0</span><span class="op">,</span> <span class="dt">end</span><span class="op">:</span> <span class="dv">1</span><span class="op">/</span><span class="dv">2</span> }<span class="op">,</span></span>
|
||||
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>{ <span class="dt">value</span><span class="op">:</span> <span class="st">'e3'</span><span class="op">,</span> <span class="dt">begin</span><span class="op">:</span> <span class="dv">1</span><span class="op">/</span><span class="dv">2</span><span class="op">,</span> <span class="dt">end</span><span class="op">:</span> <span class="dv">3</span><span class="op">/</span><span class="dv">4</span> }<span class="op">,</span></span>
|
||||
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>{ <span class="dt">value</span><span class="op">:</span> <span class="st">'g3'</span><span class="op">,</span> <span class="dt">begin</span><span class="op">:</span> <span class="dv">3</span><span class="op">/</span><span class="dv">4</span><span class="op">,</span> <span class="dt">end</span><span class="op">:</span> <span class="dv">1</span> }]</span></code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Each event has a value, a begin time and an end time, where time is represented as a fraction. In the above case,
|
||||
the events are placed in sequential order, where c3 takes the first half, and e3 and g3 together take the second
|
||||
half. This temporal placement is the result of the <code>sequence</code> function, which divides its arguments
|
||||
equally over one cycle. If an argument is an array, the same rule applies to that part of the cycle. In the
|
||||
example, e3 and g3 are divided equally over the second half of the whole cycle.
|
||||
</p>
|
||||
<p>
|
||||
The above examples do not represent how Strudel is used in practice. In the live coding editor, the user only has
|
||||
to type in the pattern itself, the querying will be handled by the scheduler. The scheduler will repeatedly query
|
||||
the pattern for events, which are then scheduled as sound synthesis or other event triggers. Also, the above event
|
||||
data structure has been simplified for readability.
|
||||
</p>
|
||||
<figure>
|
||||
<img
|
||||
src="images/strudel-screenshot2.png"
|
||||
style="width: 60%"
|
||||
alt="Screenshot of the Strudel ‘REPL’ live coding editor, including piano-roll visualisation."
|
||||
/>
|
||||
<figcaption aria-hidden="true">
|
||||
Screenshot of the Strudel ‘REPL’ live coding editor, including piano-roll visualisation.
|
||||
</figcaption>
|
||||
</figure>
|
||||
<h1 data-number="4" id="making-patterns"><span class="header-section-number">4</span> Making Patterns</h1>
|
||||
<p>
|
||||
In practice, the end-user live coder will not deal with constructing patterns directly, but will rather build
|
||||
patterns using Strudel’s extensive combinator library to create, combine and transform patterns.
|
||||
</p>
|
||||
<p>
|
||||
The live coder will rarely use the <code>sequence</code> function as seen above, as sequencing is implicit in many
|
||||
functions. For example in the following, the <code>note</code> function constructs a pattern of notes, sequencing
|
||||
its arguments in the same manner as the previous example.
|
||||
</p>
|
||||
<div class="sourceCode" id="cb3">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="fu">note</span>(c3<span class="op">,</span> [e3<span class="op">,</span> g3])</span></code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Perhaps more often, they will use the mini-notation for even terser notation of rhythmic sequences: [^This last
|
||||
example is also valid Tidal code, albeit the parenthesis is not required in its Haskell syntax in this case. Tidal
|
||||
does not support passing sequences as lists directly to the <code>note</code> function, however.].
|
||||
</p>
|
||||
<div class="sourceCode" id="cb4">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="fu">note</span>(<span class="st">"c3 [e3 g3]"</span>)</span></code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Such sequences are often treated only a starting point for manipulation, where they then undergo pattern
|
||||
transformations such as repetition, symmetry, interference/combination or randomisation, potentially at multiple
|
||||
timescales. Because Strudel patterns are represented as pure functions of time rather than as data structures,
|
||||
very long and complex generative results can be represented and manipulated without having to store the resulting
|
||||
sequences in memory.
|
||||
</p>
|
||||
<h1 data-number="5" id="pattern-example"><span class="header-section-number">5</span> Pattern Example</h1>
|
||||
<p>
|
||||
The following example showcases how patterns can be utilized to create musical complexity from simple parts, using
|
||||
repetition and interference:
|
||||
</p>
|
||||
<div class="sourceCode" id="cb5">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="st">"<0 2 [4 6](3,4,1) 3>"</span></span>
|
||||
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">off</span>(<span class="dv">1</span><span class="op">/</span><span class="dv">4</span><span class="op">,</span> <span class="fu">add</span>(<span class="dv">2</span>))</span>
|
||||
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">off</span>(<span class="dv">1</span><span class="op">/</span><span class="dv">2</span><span class="op">,</span> <span class="fu">add</span>(<span class="dv">6</span>))</span>
|
||||
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">scale</span>(<span class="st">'D minor'</span>)</span>
|
||||
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">legato</span>(<span class="op">.</span><span class="dv">25</span>)</span>
|
||||
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">note</span>()<span class="op">.</span><span class="fu">s</span>(<span class="st">"sawtooth square"</span>)</span>
|
||||
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a><span class="op">.</span><span class="fu">delay</span>(<span class="op">.</span><span class="dv">8</span>)<span class="op">.</span><span class="fu">delaytime</span>(<span class="op">.</span><span class="dv">125</span>)</span></code></pre>
|
||||
</div>
|
||||
<p>
|
||||
The pattern starts with a rhythm of numbers in mini notation, which are later interpreted inside the scale of D
|
||||
minor. The first line could also be expressed without mini notation:
|
||||
</p>
|
||||
<div class="sourceCode" id="cb6">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="fu">cat</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">2</span><span class="op">,</span> [<span class="dv">4</span><span class="op">,</span> <span class="dv">6</span>]<span class="op">.</span><span class="fu">euclid</span>(<span class="dv">3</span><span class="op">,</span> <span class="dv">4</span><span class="op">,</span> <span class="dv">1</span>)<span class="op">,</span> <span class="dv">3</span>)</span></code></pre>
|
||||
</div>
|
||||
<p>
|
||||
These numbers then undergo various pattern transformations. Here is a short description of all the functions used:
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>cat</code>: play elements sequentially, where each lasts one cycle</li>
|
||||
<li><code>brackets</code>: elements inside brackets are divided equally over the time of their parent</li>
|
||||
<li>
|
||||
<code>.euclid(p, s, o)</code>: place p pulses evenly over s steps, with offset o
|
||||
<span class="citation" data-cites="toussaintEuclideanAlgorithmGenerates2005">(Toussaint 2005)</span>
|
||||
</li>
|
||||
<li>
|
||||
<code>.off(n, f)</code>: layers a pattern on top of itself, with the new layer offset by n cycles, and with
|
||||
function f applied
|
||||
</li>
|
||||
<li><code>.legato(n)</code>: multiply the duration of all events in a pattern by a factor of n</li>
|
||||
<li>
|
||||
<code>.echo(t, n, v)</code>: copy each event t times, with n cycles in between each copy, decreasing velocity by
|
||||
v
|
||||
</li>
|
||||
<li><code>.note()</code>: interpretes values as notes</li>
|
||||
<li><code>.s(name)</code>: play back each event with the given sound</li>
|
||||
<li><code>.delay(wet)</code>: add delay</li>
|
||||
<li><code>.delaytime(t)</code>: set delay time</li>
|
||||
</ul>
|
||||
<p>Much of the above will be familiar to Tidal users.</p>
|
||||
<!-- This example shows some of Strudel's unique support for chords and transposition familiar to students of Western music theory. This differs a little from Tidal's approach and thanks to the integration of the javascript library XXX (*TODO* ? or is this all your work Felix?), Strudel's support for tonal transformations such as voice leading is perhaps respects more advanced than Tidal. -->
|
||||
<h1 data-number="6" id="ways-to-make-sound-and-other-events">
|
||||
<span class="header-section-number">6</span> Ways to make Sound (and other events)
|
||||
</h1>
|
||||
<p>To generate sound, Strudel supports bindings for different outputs:</p>
|
||||
<ul>
|
||||
<li>Tone.js (deprecated)</li>
|
||||
<li>Web Audio API</li>
|
||||
<li>WebDirt, a js recreation of Tidal’s <em>Dirt</em> sample engine (deprecated)</li>
|
||||
<li>OSC via osc-js, compatible with superdirt</li>
|
||||
<li>Csound via the Csound WebAssembly build</li>
|
||||
<li>MIDI via WebMIDI</li>
|
||||
<li>Serial via WebSerial</li>
|
||||
</ul>
|
||||
<p>
|
||||
At first, we used Tone.js as sound output, but it proved to be limited for the use case of Strudel, where each
|
||||
individual event could potentially have a completely different audio graph. While the Web Audio API takes a
|
||||
<em>fire-and-forget</em> approach, creating a lot of Tone.js instruments and effects causes performance issues
|
||||
quickly. For that reason, we chose to search for alternatives.
|
||||
</p>
|
||||
<p>
|
||||
Strudel’s new default output uses the Web Audio API to create a new audio graph for each event. It currently
|
||||
supports basic oscillators, sample playback, various effects and an experimental support for soundfonts.
|
||||
</p>
|
||||
<p>
|
||||
WebDirt <span class="citation" data-cites="ogbornDktr0WebDirt2022">(Ogborn [2016] 2022)</span> was created as part
|
||||
of the Estuary Live Coding System
|
||||
<span class="citation" data-cites="ogbornEstuaryBrowserbasedCollaborative2017">(Ogborn et al. 2017)</span>, and
|
||||
proved to be a solid choice for handling samples in Strudel as well. We are however focused on working more
|
||||
directly with the Web Audio API to be able to integrate new features more tightly.
|
||||
</p>
|
||||
<p>
|
||||
Using the OSC protocol via Strudel’s provided Node.js-based OSC proxy server, it is possible to send network
|
||||
messages to trigger events. This is mainly used to render sound using SuperDirt
|
||||
<span class="citation" data-cites="SuperDirt2022">(<em>SuperDirt</em> [2015] 2022)</span>, which is the
|
||||
well-developed Supercollider-based synthesis framework that Tidal live coders generally use as standard.
|
||||
</p>
|
||||
<p>
|
||||
Recently, the experimental integration of Csound proved to bring a new dimension of sound design capabilities to
|
||||
Strudel. Thanks to the WebAssembly distribution of this classic system
|
||||
<span class="citation" data-cites="CsoundWebAssembly">(Yi, Lazzarini, and Costello 2018)</span>, Csound
|
||||
‘orchestra’ synthesisers can be embedded in and then patterned with Strudel code.
|
||||
</p>
|
||||
<p>
|
||||
MIDI output can also be used to send MIDI messages to either external instruments or to other programs on the same
|
||||
device. Unlike OSC, Strudel is able to send MIDI directly without requiring additional proxy software, but only
|
||||
from web browsers that support it (at the time of writing, this means Chromium-based browsers).
|
||||
</p>
|
||||
<p>
|
||||
Finally, Strudel supports Serial output, for example to trigger events via microcontrollers. This has already been
|
||||
explored for robot choreography by Kate Sicchio and Alex McLean, via a performance presented at the International
|
||||
Conference on Live Interfaces 2022.
|
||||
</p>
|
||||
<h1 data-number="7" id="the-strudel-repl"><span class="header-section-number">7</span> The Strudel REPL</h1>
|
||||
<p>
|
||||
While Strudel can be used as a library in any JavaScript codebase, its main, reference user interface is the
|
||||
Strudel REPL[^REPL stands for read, evaluate, print/play, loop. It is friendly jargon for an interactive
|
||||
programming interface from computing heritage, usually for a commandline interface but also applied to live coding
|
||||
editors.], which is a browser-based live coding environment. This live code editor is dedicated to manipulating
|
||||
Strudel patterns while they play. The REPL features built-in visual feedback, which highlights which elements in
|
||||
the patterned (mini-notation) sequences are influencing the event that is currently being played. This feedback is
|
||||
designed to support both learning and live use of Strudel.
|
||||
</p>
|
||||
<p>
|
||||
Besides a UI for playback control and meta information, the main part of the REPL interface is the code editor
|
||||
powered by CodeMirror. In it, the user can edit and evaluate pattern code live, using one of the available
|
||||
synthesis outputs to create music and/or sound art. The control flow of the REPL follows 3 basic steps:
|
||||
</p>
|
||||
<ol type="1">
|
||||
<li>
|
||||
The user writes and updates code. Each update transpiles and evaluates it to create a
|
||||
<code>Pattern</code> instance
|
||||
</li>
|
||||
<li>
|
||||
While the REPL is running, the <code>Scheduler</code> queries the active <code>Pattern</code> by a regular
|
||||
interval, generating <code>Events</code> (also known as <code>Haps</code> in Strudel) for the next time span.
|
||||
</li>
|
||||
<li>
|
||||
For each scheduling tick, all generated <code>Events</code> are triggered by calling their
|
||||
<code>onTrigger</code> method, which is set by the output.
|
||||
</li>
|
||||
</ol>
|
||||
<figure>
|
||||
<img
|
||||
src="https://github.com/tidalcycles/strudel/raw/talk/talk/public/strudelflow.png?raw=true"
|
||||
style="width: 43%"
|
||||
alt="REPL control flow"
|
||||
/>
|
||||
<figcaption aria-hidden="true">REPL control flow</figcaption>
|
||||
</figure>
|
||||
<h2 data-number="7.1" id="user-code"><span class="header-section-number">7.1</span> User Code</h2>
|
||||
<p>To create a <code>Pattern</code> from the user code, two steps are needed:</p>
|
||||
<ol type="1">
|
||||
<li>Transpile the JS input code to make it functional</li>
|
||||
<li>Evaluate the transpiled code</li>
|
||||
</ol>
|
||||
<h3 data-number="7.1.1" id="transpilation-evaluation">
|
||||
<span class="header-section-number">7.1.1</span> Transpilation & Evaluation
|
||||
</h3>
|
||||
<p>
|
||||
In the JavaScript world, using transpilation is a common practise to be able to use language features that are not
|
||||
supported by the base language. Tools like <code>babel</code> will transpile code that contains unsupported
|
||||
language features into a version of the code without those features.
|
||||
</p>
|
||||
<p>
|
||||
In the same tradition, Strudel can add a transpilation step to simplify the user code in the context of live
|
||||
coding. For example, the Strudel REPL lets the user create mini notation patterns using just double quoted
|
||||
strings, while single quoted strings remain what they are:
|
||||
</p>
|
||||
<div class="sourceCode" id="cb7">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="st">"c3 [e3 g3]*2"</span></span></code></pre>
|
||||
</div>
|
||||
<p>is transpiled to:</p>
|
||||
<div class="sourceCode" id="cb8">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="fu">mini</span>(<span class="st">"c3 [e3 g3]*2"</span>)<span class="op">.</span><span class="fu">withMiniLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span>]<span class="op">,</span>[<span class="dv">1</span><span class="op">,</span><span class="dv">14</span><span class="op">,</span><span class="dv">14</span>])</span></code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Here, the string is wrapped in <code>mini</code>, which will create a pattern from a mini notation string.
|
||||
Additionally, the <code>withMiniLocation</code> method passes the original source code location of the string to
|
||||
the pattern, which enables highlighting active events.
|
||||
</p>
|
||||
<p>
|
||||
Other convenient features like pseudo variables, operator overloading and top level await are possible with
|
||||
transpilation.
|
||||
</p>
|
||||
<p>After the transpilation, the code is ready to be evaluated into a <code>Pattern</code>.</p>
|
||||
<p>
|
||||
Behind the scenes, the user code string is parsed with <code>acorn</code>, turning it into an Abstract Syntax Tree
|
||||
(AST). The AST allows changing the structure of the code before generating the transpiled version using
|
||||
<code>escodegen</code>.
|
||||
</p>
|
||||
<h3 data-number="7.1.2" id="mini-notation"><span class="header-section-number">7.1.2</span> Mini Notation</h3>
|
||||
<p>
|
||||
While the transpilation allows JavaScript to express Patterns in a less verbose way, it is still preferable to use
|
||||
the Mini Notation as a more compact way to express rhythm. Strudel aims to provide the same Mini Notation features
|
||||
and syntax as used in Tidal.
|
||||
</p>
|
||||
<p>
|
||||
The Mini Notation parser is implemented using <code>peggy</code>, which allows generating performant parsers for
|
||||
Domain Specific Languages (DSLs) using a concise grammar notation. The generated parser turns the Mini Notation
|
||||
string into an AST which is used to call the respective Strudel functions with the given structure. For example,
|
||||
<code>"c3 [e3 g3]*2"</code> will result in the following calls:
|
||||
</p>
|
||||
<div class="sourceCode" id="cb9">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="fu">seq</span>(</span>
|
||||
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a> <span class="fu">reify</span>(<span class="st">'c3'</span>)<span class="op">.</span><span class="fu">withLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">1</span><span class="op">,</span><span class="dv">1</span>]<span class="op">,</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">4</span><span class="op">,</span><span class="dv">4</span>])<span class="op">,</span></span>
|
||||
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a> <span class="fu">seq</span>(</span>
|
||||
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">reify</span>(<span class="st">'e3'</span>)<span class="op">.</span><span class="fu">withLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">5</span><span class="op">,</span><span class="dv">5</span>]<span class="op">,</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">8</span><span class="op">,</span><span class="dv">8</span>])<span class="op">,</span></span>
|
||||
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">reify</span>(<span class="st">'g3'</span>)<span class="op">.</span><span class="fu">withLocation</span>([<span class="dv">1</span><span class="op">,</span><span class="dv">8</span><span class="op">,</span><span class="dv">8</span>]<span class="op">,</span> [<span class="dv">1</span><span class="op">,</span><span class="dv">10</span><span class="op">,</span><span class="dv">10</span>])<span class="op">,</span></span>
|
||||
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a> )<span class="op">.</span><span class="fu">fast</span>(<span class="dv">2</span>)</span>
|
||||
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a>)</span></code></pre>
|
||||
</div>
|
||||
<h3 data-number="7.1.3" id="highlighting-locations">
|
||||
<span class="header-section-number">7.1.3</span> Highlighting Locations
|
||||
</h3>
|
||||
<p>
|
||||
As seen in the examples above, both the JS and the Mini Notation parser add source code locations using
|
||||
<code>withMiniLocation</code> and <code>withLocation</code> methods. While the JS parser adds locations relative
|
||||
to the user code as a whole, the Mini Notation adds locations relative to the position of the mini notation
|
||||
string. The absolute location of elements within Mini Notation can be calculated by simply adding both locations
|
||||
together. This absolute location can be used to highlight active events in real time.
|
||||
</p>
|
||||
<h2 data-number="7.2" id="scheduling-events"><span class="header-section-number">7.2</span> Scheduling Events</h2>
|
||||
<p>
|
||||
After an instance of <code>Pattern</code> is obtained from the user code, it is used by the scheduler to get
|
||||
queried for events. Once started, the scheduler runs at a fixed interval to query active pattern for events
|
||||
withing the current interval’s time span. A simplified implementation looks like this:
|
||||
</p>
|
||||
<div class="sourceCode" id="cb10">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> pattern <span class="op">=</span> <span class="fu">seq</span>(<span class="st">'c3'</span><span class="op">,</span> [<span class="st">'e3'</span><span class="op">,</span> <span class="st">'g3'</span>])<span class="op">;</span> <span class="co">// pattern from user</span></span>
|
||||
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> interval <span class="op">=</span> <span class="fl">0.5</span><span class="op">;</span> <span class="co">// query interval in seconds</span></span>
|
||||
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> time <span class="op">=</span> <span class="dv">0</span><span class="op">;</span> <span class="co">// beginning of current time span</span></span>
|
||||
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span> minLatency <span class="op">=</span> <span class="op">.</span><span class="dv">1</span><span class="op">;</span> <span class="co">// min time before a hap should trigger</span></span>
|
||||
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a><span class="pp">setInterval</span>(() <span class="kw">=></span> {</span>
|
||||
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> haps <span class="op">=</span> pattern<span class="op">.</span><span class="fu">queryArc</span>(time<span class="op">,</span> time <span class="op">+</span> interval)<span class="op">;</span></span>
|
||||
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a> time <span class="op">+=</span> interval<span class="op">;</span> <span class="co">// increment time</span></span>
|
||||
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a> haps<span class="op">.</span><span class="fu">forEach</span>((hap) <span class="kw">=></span> {</span>
|
||||
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> deadline <span class="op">=</span> hap<span class="op">.</span><span class="at">whole</span><span class="op">.</span><span class="at">begin</span> <span class="op">-</span> time <span class="op">+</span> minLatency<span class="op">;</span></span>
|
||||
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a> <span class="fu">onTrigger</span>(hap<span class="op">,</span> deadline<span class="op">,</span> duration)<span class="op">;</span></span>
|
||||
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a> })<span class="op">;</span></span>
|
||||
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a>}<span class="op">,</span> interval <span class="op">*</span> <span class="dv">1000</span>)<span class="op">;</span> <span class="co">// query each "interval" seconds</span></span></code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Note that the above code is simplified for illustrative purposes. The actual implementation has to work around
|
||||
imprecise callbacks of <code>setInterval</code>. More about the implementation details can be read in
|
||||
<a href="https://loophole-letters.vercel.app/web-audio-scheduling">this blog post</a>.
|
||||
</p>
|
||||
<p>
|
||||
The fact that <code>Pattern.queryArc</code> is a pure function that maps a time span to a set of events allows us
|
||||
to choose any interval we like without changing the resulting output. It also means that when the pattern is
|
||||
changed from outside, the next scheduling callback will work with the new pattern, keeping its clock running.
|
||||
</p>
|
||||
<p>
|
||||
The latency between the time the pattern is evaluated and the change is heard is between
|
||||
<code>minLatency</code> and <code>interval + minLatency</code>, in our example between 100ms and 600ms. In
|
||||
Strudel, the current query interval is 50ms with a minLatency of 100ms, meaning the latency is between 50ms and
|
||||
150ms.
|
||||
</p>
|
||||
<h2 data-number="7.3" id="output"><span class="header-section-number">7.3</span> Output</h2>
|
||||
<p>
|
||||
The last step is to trigger each event in the chosen output. This is where the given time and value of each event
|
||||
is used to generate audio or any other form of time based output. The default output of the Strudel REPL is the
|
||||
WebAudio output. To understand what an output does, we first have to understand what control parameters are.
|
||||
</p>
|
||||
<h3 data-number="7.3.1" id="control-parameters">
|
||||
<span class="header-section-number">7.3.1</span> Control Parameters
|
||||
</h3>
|
||||
<p>
|
||||
To be able to manipulate multiple aspects of sound in parallel, so called control parameters are used to shape the
|
||||
value of each event. Example:
|
||||
</p>
|
||||
<div class="sourceCode" id="cb11">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="fu">note</span>(<span class="st">"c3 e3"</span>)<span class="op">.</span><span class="fu">cutoff</span>(<span class="dv">1000</span>)<span class="op">.</span><span class="fu">s</span>(<span class="st">'sawtooth'</span>)</span>
|
||||
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a> <span class="op">.</span><span class="fu">queryArc</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">1</span>)<span class="op">.</span><span class="fu">map</span>(hap <span class="kw">=></span> hap<span class="op">.</span><span class="at">value</span>)</span>
|
||||
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a><span class="co">/* [</span></span>
|
||||
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a><span class="co"> { note: 'c3', cutoff: 1000, s: 'sawtooth' }</span></span>
|
||||
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a><span class="co"> { note: 'e3', cutoff: 1000, s: 'sawtooth' }</span></span>
|
||||
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a><span class="co">] */</span></span></code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Here, the control parameter functions <code>note</code>, <code>cutoff</code> and <code>s</code> are used, where
|
||||
each controls a different property in the value object. Each control parameter function accepts a primitive value,
|
||||
a list of values to be sequenced into a <code>Pattern</code>, or a <code>Pattern</code>. In the example,
|
||||
<code>note</code> gets a <code>Pattern</code> from a Mini Notation expression (double quoted), while
|
||||
<code>cutoff</code> and <code>s</code> are given a <code>Number</code> and a (single quoted)
|
||||
<code>String</code> respectively.
|
||||
</p>
|
||||
<p>
|
||||
Strudel comes with a large default set of control parameter functions that are based on the ones used by Tidal and
|
||||
SuperDirt, focusing on music and audio terminology. It is however possible to create custom control paramters for
|
||||
any purpose:
|
||||
</p>
|
||||
<div class="sourceCode" id="cb12">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> { x<span class="op">,</span> y } <span class="op">=</span> <span class="fu">createParams</span>(<span class="st">'x'</span><span class="op">,</span> <span class="st">'y'</span>)</span>
|
||||
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a><span class="fu">x</span>(sine<span class="op">.</span><span class="fu">range</span>(<span class="dv">0</span><span class="op">,</span> <span class="dv">200</span>))<span class="op">.</span><span class="fu">y</span>(cosine<span class="op">.</span><span class="fu">range</span>(<span class="dv">0</span><span class="op">,</span><span class="dv">200</span>))</span></code></pre>
|
||||
</div>
|
||||
<p>
|
||||
This example creates the custom control parameters <code>x</code> and <code>y</code> which are then used to form a
|
||||
pattern that descibes the coordinates of a circle.
|
||||
</p>
|
||||
<h3 data-number="7.3.2" id="outputs"><span class="header-section-number">7.3.2</span> Outputs</h3>
|
||||
<p>
|
||||
Now that we know how the value of an event is manipulated using control parameters, we can look at how outputs can
|
||||
use that value to generate anything. The scheduler above was calling the <code>onTrigger</code> function which is
|
||||
used to implement the output. A very simple version of the web audio output could look like this:
|
||||
</p>
|
||||
<div class="sourceCode" id="cb13">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="kw">function</span> <span class="fu">onTrigger</span>(hap<span class="op">,</span> deadline<span class="op">,</span> duration) {</span>
|
||||
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> { note } <span class="op">=</span> hap<span class="op">.</span><span class="at">value</span><span class="op">;</span></span>
|
||||
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> time <span class="op">=</span> <span class="fu">getAudioContext</span>()<span class="op">.</span><span class="at">currentTime</span> <span class="op">+</span> deadline<span class="op">;</span></span>
|
||||
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">const</span> o <span class="op">=</span> <span class="fu">getAudioContext</span>()<span class="op">.</span><span class="fu">createOscillator</span>()<span class="op">;</span></span>
|
||||
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="at">frequency</span><span class="op">.</span><span class="at">value</span> <span class="op">=</span> <span class="fu">getFreq</span>(note)<span class="op">;</span></span>
|
||||
<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="fu">start</span>(time)<span class="op">;</span></span>
|
||||
<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="fu">stop</span>(time <span class="op">+</span> <span class="bu">event</span><span class="op">.</span><span class="at">duration</span>)<span class="op">;</span></span>
|
||||
<span id="cb13-8"><a href="#cb13-8" aria-hidden="true" tabindex="-1"></a> o<span class="op">.</span><span class="fu">connect</span>(<span class="fu">getAudioContext</span>()<span class="op">.</span><span class="at">destination</span>)<span class="op">;</span></span>
|
||||
<span id="cb13-9"><a href="#cb13-9" aria-hidden="true" tabindex="-1"></a>}</span></code></pre>
|
||||
</div>
|
||||
<p>
|
||||
The above example will create an <code>OscillatorNode</code> for each event, where the frequency is controlled by
|
||||
the <code>note</code> param. In essence, this is how the WebAudio API output of Strudel works, only with many more
|
||||
parameters to control synths, samples and effects.
|
||||
</p>
|
||||
<h1 data-number="8" id="pattern-alignment-and-combination">
|
||||
<span class="header-section-number">8</span> Pattern alignment and combination
|
||||
</h1>
|
||||
<p>
|
||||
One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective
|
||||
of their structure. Its declarative approach means a live coder does not have to think about the details of
|
||||
<em>how</em> this is done, only <em>what</em> is to be done.
|
||||
</p>
|
||||
<p>
|
||||
As a simple example, consider two number patterns <code>"0 [1 2] 3"</code>, and <code>"10 20"</code>. The first
|
||||
has three contiguous steps of equal lengths, with the second step broken down into two substeps, giving four
|
||||
events in total. There are a very large number of ways in which the structure of these two patterns could be
|
||||
combined, but the default method in both Strudel and Tidal is to line up the cycles of the two patterns, and then
|
||||
take events from the first pattern and match them with those in the second pattern. Therefore, the following two
|
||||
lines are equivalent:
|
||||
</p>
|
||||
<div class="sourceCode" id="cb14">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="st">"0 [1 2] 3"</span><span class="op">.</span><span class="fu">add</span>(<span class="st">"10 20"</span>)</span>
|
||||
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a><span class="st">"10 [11 22] 23"</span></span></code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Where the events only partially overlap, they are treated as fragments of the event in the first pattern. This is
|
||||
a little difficult to conceptualise, but lets start by comparing the two patterns in the following example:
|
||||
</p>
|
||||
<div class="sourceCode" id="cb15">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="st">"0 1 2"</span><span class="op">.</span><span class="fu">add</span>(<span class="st">"10 20"</span>)</span>
|
||||
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a><span class="st">"10 [11 21] 20"</span></span></code></pre>
|
||||
</div>
|
||||
<p>
|
||||
They are similar to the previous example in that the number <code>1</code> is split in two, with its two halves
|
||||
added to <code>10</code> and <code>20</code> respectively. However, the <code>11</code> ‘remembers’ that it is a
|
||||
fragment of that original <code>1</code> event, and so is treated as having a duration of a third of a cycle,
|
||||
despite only being active for a sixth of a cycle. Likewise, the <code>21</code> is also a fragment of that
|
||||
original <code>1</code> event, but a fragment of its second half. Because the start of its event is missing, it
|
||||
wouldn’t actually trigger a sound (unless it underwent further pattern transformations/combinations).
|
||||
</p>
|
||||
<p>
|
||||
In practice, the effect of this default, implicit method for combining two patterns is that the second pattern is
|
||||
added <em>in</em> to the first one, and indeed this can be made explicit:
|
||||
</p>
|
||||
<div class="sourceCode" id="cb16">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a><span class="st">"0 1 2"</span><span class="op">.</span><span class="at">add</span><span class="op">.</span><span class="fu">in</span>(<span class="st">"10 20"</span>)</span></code></pre>
|
||||
</div>
|
||||
<p>This makes way for other ways to align the pattern, and several are already defined, in particular:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code>in</code> - as explained above, aligns cycles, and applies values from the pattern on the right
|
||||
<em>in</em> to the pattern on the left.
|
||||
</li>
|
||||
<li>
|
||||
<code>out</code> - as with <code>in</code>, but values are applied <em>out</em> of the pattern on the left
|
||||
(i.e. <em>in</em> to the one on the right).
|
||||
</li>
|
||||
<li>
|
||||
<code>mix</code> - structures from both patterns are combined, so that the new events are not fragments but are
|
||||
created at intersections of events from both sides.
|
||||
</li>
|
||||
<li>
|
||||
<code>squeeze</code> - cycles from the pattern on the right are squeezed into events on the left. So that
|
||||
e.g. <code>"0 1 2".add.squeeze("10 20")</code> is equivalent to <code>"[10 20] [11 21] [12 22]"</code>.
|
||||
</li>
|
||||
<li>
|
||||
<code>squeezeout</code> - as with <code>squeeze</code>, but cycles from the left are squeezed into events on the
|
||||
right. So, <code>"0 1 2".add.squeezeout("10 20")</code> is equivalent to <code>[10 11 12] [20 21 22]</code>.
|
||||
</li>
|
||||
<li>
|
||||
<code>trig</code> is similar to <code>squeezeout</code> in that cycles from the right are aligned with events on
|
||||
the left. However those cycles are not ‘squeezed’, rather they are truncated to fit the event. So
|
||||
<code>"0 1 2 3 4 5 6 7".add.trig("10 [20 30]")</code> would be equivalent to
|
||||
<code>10 11 12 13 20 21 30 31</code>. In effect, events on the right ‘trigger’ cycles on the left.
|
||||
</li>
|
||||
<li>
|
||||
<code>trigzero</code> is similar to <code>trig</code>, but the pattern is ‘triggered’ from its very first cycle,
|
||||
rather than from the current cycle. <code>trig</code> and <code>trigzero</code> therefore only give different
|
||||
results where the leftmost pattern differs from one cycle to the next.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
We will save going deeper into the background, design and practicalities of these alignment functions for future
|
||||
publications. However in the next section, we take them as a case study for looking at the different design
|
||||
affordances offered by Haskell to Tidal, and JavaScript to Strudel.
|
||||
</p>
|
||||
<h1 data-number="9" id="comparing-strudel-and-haskell-in-use">
|
||||
<span class="header-section-number">9</span> Comparing Strudel and Haskell in use
|
||||
</h1>
|
||||
<p>
|
||||
Unlike Haskell, JavaScript lacks the ability to define custom infix operators, or change the meaning of existing
|
||||
ones. So the above Strudel example of <code>"0 1 2".add.out("10 20")</code> is equivalent to the Tidal expression
|
||||
<code>"0 1 2" +| "10 20"</code>, where the vertical bar in the operator <code>+|</code> stands for
|
||||
<code>out</code> (where <code>a |+ b</code> would be equivalent of <code>a.add.in(b)</code>).
|
||||
</p>
|
||||
<p>
|
||||
From this we can already see that Tidal tends towards brevity through mixing infix operators with functions, and
|
||||
Strudel tends towards spelling out operations which are joined together with the <code>.</code> operator. This
|
||||
then is the design trade-off of Tidal’s tersity, versus Strudel’s simplicity.
|
||||
</p>
|
||||
<p>To demonstrate this, consider the following Tidal pattern:</p>
|
||||
<pre
|
||||
class="tidal"
|
||||
><code>iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4</code></pre>
|
||||
<p>This can be directly translated to the Strudel equivalent:</p>
|
||||
<div class="sourceCode" id="cb18">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb18-1"><a href="#cb18-1" aria-hidden="true" tabindex="-1"></a><span class="fu">iter</span>(<span class="dv">4</span><span class="op">,</span> <span class="fu">every</span>(<span class="dv">3</span><span class="op">,</span> add<span class="op">.</span><span class="fu">squeeze</span>(<span class="st">"10 20"</span>)<span class="op">,</span> <span class="fu">n</span>(<span class="st">"0 1 3"</span>)<span class="op">.</span><span class="fu">s</span>(<span class="st">"triangle"</span>)<span class="op">.</span><span class="fu">crush</span>(<span class="dv">4</span>)))</span></code></pre>
|
||||
</div>
|
||||
<p>Although for a more canonical Strudel expression, we would reorder it as:</p>
|
||||
<div class="sourceCode" id="cb19">
|
||||
<pre
|
||||
class="sourceCode js"
|
||||
><code class="sourceCode javascript"><span id="cb19-1"><a href="#cb19-1" aria-hidden="true" tabindex="-1"></a><span class="fu">n</span>(<span class="st">"0 1 3"</span>)<span class="op">.</span><span class="fu">every</span>(<span class="dv">3</span><span class="op">,</span> add<span class="op">.</span><span class="fu">squeeze</span>(<span class="st">"10 20"</span>))<span class="op">.</span><span class="fu">iter</span>(<span class="dv">4</span>)<span class="op">.</span><span class="fu">s</span>(<span class="st">"triangle"</span>)<span class="op">.</span><span class="fu">crush</span>(<span class="dv">4</span>)</span></code></pre>
|
||||
</div>
|
||||
<p>
|
||||
The Strudel example uses the <code>.</code> method call operator for all operations and combinations, whereas the
|
||||
Tidal example has <code>#</code> for the default method for combining patterns and uses infix operators for other
|
||||
methods. The lack of parenthesis in the Tidal example is partly due to the way that arguments are applied to
|
||||
Haskell’s functions, and partly due to the use of the <code>$</code> operator as an alternative way to establish
|
||||
precedence and control the order of evaluation.
|
||||
</p>
|
||||
<p>
|
||||
Considering the above, we argue that the Haskell syntax is a little cleaner, but that the Strudel syntax is easier
|
||||
to learn. Our informal observation is that while Haskell’s dollar <code>$</code> operator is very useful in making
|
||||
code easier to work with, it is one of the most difficult aspects of Tidal use for beginners to learn. On the
|
||||
other hand, the deeper levels of parenthesis in Strudel code can be difficult to keep track of, especially while
|
||||
coding under pressure of live musical performance. However this difficulty can be largely be mitigated by
|
||||
reordering expressions, and further mitigated by supporting editor features.
|
||||
</p>
|
||||
<p>
|
||||
With Strudel, we have little choice but to embrace the affordances and constraints offered by JavaScript, and
|
||||
while designing a domain-specific language entirely based on method calls is a challenge, through creative
|
||||
adoption of functional programming techniques like partial application, we are so far very happy with the results.
|
||||
Tidal’s functional reactive approach to pattern-making has in general translated well to JavaScript, and
|
||||
opportunities and constraints have overall traded off to create a very approachable and useable live coding
|
||||
environment.
|
||||
</p>
|
||||
<h2 data-number="9.1" id="the-trade-off-of-flexible-typing">
|
||||
<span class="header-section-number">9.1</span> The trade-off of flexible typing
|
||||
</h2>
|
||||
<p>
|
||||
We have identified one problem with porting Tidal to JavaScript where we have missed Haskell’s strict typing and
|
||||
type inference. In both Tidal and Strudel, time is rational, where any point in time is represented as the ratio
|
||||
of two integers. This allows representation of musical ratios such that are impossible to represent accurately
|
||||
using the more common floating point numbers. However while libraries are available that support rational numbers
|
||||
in JavaScript, the lack of strict typing means that it is easy to implement pattern methods where computationally
|
||||
expensive conversion from floating point to rational numbers are performed late, and therefore often enough to
|
||||
overload the CPUs, due to the large number of iterative calculations required to estimate a ratio for a given
|
||||
floating point number. To mitigate this problem, we might consider moving to TypeScript in the future.
|
||||
</p>
|
||||
<h1 data-number="10" id="future-outlook"><span class="header-section-number">10</span> Future Outlook</h1>
|
||||
<p>
|
||||
The project is still young, with many features on the horizon. As general guiding principles, Strudel aims to be
|
||||
</p>
|
||||
<ol type="1">
|
||||
<li>accessible</li>
|
||||
<li>consistent with Tidal’s approach to pattern</li>
|
||||
<li>modular and extensible</li>
|
||||
</ol>
|
||||
<p>
|
||||
While Haskell’s type system makes it a great language for the ongoing development of Tidal’s inner representation
|
||||
of pattern, JavaScript’s vibrant ecosystem, flexibility and accessibility makes it a great host for more ad-hoc
|
||||
experiments, including interface design. For the future, it is planned to integrate additional alternative sound
|
||||
engines such as Glicol <span class="citation" data-cites="lanChaosprintGlicol2022">(Lan [2020] 2022)</span> and
|
||||
Faust
|
||||
<span class="citation" data-cites="FaustProgrammingLanguage2022"
|
||||
>(<em>Faust - Programming Language for Audio Applications and Plugins</em> [2016] 2022)</span
|
||||
>. Strudel is already approaching feature parity with Tidal, but there are more Tidal functions to be ported, and
|
||||
work to be done to improve compatibility with Tidal’s mininotation. Tidal version 2.0 is under development, which
|
||||
brings a new representation for sequences to its patterns, which will then be brought to Strudel. Besides sound,
|
||||
other ways to render events are being explored, such as graphical, and choreographic output. We are also looking
|
||||
into alternative ways of editing patterns, including multi-user editing for network music, parsing a novel syntax
|
||||
to escape the constraints of javascript, and developing hardware/e-textile interfaces. In summary, there is a lot
|
||||
of fun ahead.
|
||||
</p>
|
||||
<h1 data-number="11" id="links"><span class="header-section-number">11</span> Links</h1>
|
||||
<p>
|
||||
The Strudel REPL is available at <a href="https://strudel.cc" class="uri">https://strudel.cc</a>, including an
|
||||
interactive tutorial. The repository is at
|
||||
<a href="https://github.com/tidalcycles/strudel" class="uri">https://github.com/tidalcycles/strudel</a>, all the
|
||||
code is open source under the AGPL-3.0 License.
|
||||
</p>
|
||||
<h1 data-number="12" id="acknowledgments"><span class="header-section-number">12</span> Acknowledgments</h1>
|
||||
<p>
|
||||
Thanks to the Strudel and wider Tidal, live coding, WebAudio and free/open source software communities for
|
||||
inspiration and support. Alex McLean’s work on this project is supported by a UKRI Future Leaders Fellowship
|
||||
[grant number MR/V025260/1].
|
||||
</p>
|
||||
<h1 class="unnumbered" id="references">References</h1>
|
||||
<div id="refs" class="references csl-bib-body hanging-indent" role="doc-bibliography">
|
||||
<div id="ref-FaustProgrammingLanguage2022" class="csl-entry" role="doc-biblioentry">
|
||||
<em>Faust - Programming Language for Audio Applications and Plugins</em>. (2016) 2022. C++. GRAME.
|
||||
<a href="https://github.com/grame-cncm/faust">https://github.com/grame-cncm/faust</a>.
|
||||
</div>
|
||||
<div id="ref-jackHydra2022" class="csl-entry" role="doc-biblioentry">
|
||||
Jack, Olivia. (2022) 2022. <em>Hydra</em>.
|
||||
<a href="https://github.com/ojack/hydra">https://github.com/ojack/hydra</a>.
|
||||
</div>
|
||||
<div id="ref-lanChaosprintGlicol2022" class="csl-entry" role="doc-biblioentry">
|
||||
Lan, Qichao. (2020) 2022. <em>Chaosprint/Glicol</em>. Rust.
|
||||
<a href="https://github.com/chaosprint/glicol">https://github.com/chaosprint/glicol</a>.
|
||||
</div>
|
||||
<div id="ref-mcleanAlgorithmicPattern2020a" class="csl-entry" role="doc-biblioentry">
|
||||
Mclean, Alex. 2020. <span>“Algorithmic Pattern.”</span> In
|
||||
<em>Proceedings of the International Conference on New Interfaces for Musical Expression</em>, 265--270.
|
||||
Birmingham, UK. <a href="https://zenodo.org/record/4813352">https://zenodo.org/record/4813352</a>.
|
||||
</div>
|
||||
<div id="ref-mcleanFeedforward2020" class="csl-entry" role="doc-biblioentry">
|
||||
McLean, Alex. 2020. <span>“Feedforward.”</span> In
|
||||
<em>Proceedings of New Interfaces for Musical Expression</em>. Birmingham.
|
||||
<a href="https://zenodo.org/record/6353969">https://zenodo.org/record/6353969</a>.
|
||||
</div>
|
||||
<div id="ref-mcleanTidalVortexZero2022" class="csl-entry" role="doc-biblioentry">
|
||||
McLean, Alex, Raphaël Forment, Sylvain Le Beux, and Damián Silvani. 2022. <span>“TidalVortex Zero.”</span> In
|
||||
<em>Proceedings of the 7th International Conference on Live Coding</em>. Limerick, Ireland: Zenodo.
|
||||
<a href="https://doi.org/10.5281/zenodo.6456380">https://doi.org/10.5281/zenodo.6456380</a>.
|
||||
</div>
|
||||
<div id="ref-ogbornDktr0WebDirt2022" class="csl-entry" role="doc-biblioentry">
|
||||
Ogborn, David. (2016) 2022. <em>Dktr0/WebDirt</em>. JavaScript.
|
||||
<a href="https://github.com/dktr0/WebDirt">https://github.com/dktr0/WebDirt</a>.
|
||||
</div>
|
||||
<div id="ref-ogbornEstuaryBrowserbasedCollaborative2017" class="csl-entry" role="doc-biblioentry">
|
||||
Ogborn, David, Jamie Beverley, Luis Navarro del Angel, Eldad Tsabary, and Alex McLean. 2017.
|
||||
<span>“Estuary: Browser-Based Collaborative Projectional Live Coding of Musical Patterns.”</span> In
|
||||
<em>Proceedings of the International Conference on Live Coding</em>, 11. Morelia.
|
||||
</div>
|
||||
<div id="ref-robertsGibberLiveCoding2012" class="csl-entry" role="doc-biblioentry">
|
||||
Roberts, Charles, and Joann Kuchera-morin. 2012. <span>“Gibber: Live Coding Audio in the Browser.”</span> In
|
||||
<em>In Proceedings of the 2012 International Computer Music Conference</em>.
|
||||
</div>
|
||||
<div id="ref-StrudelWAC2022" class="csl-entry" role="doc-biblioentry">
|
||||
Roos, Felix, and Alex McLean. 2022. <span>“Strudel: Algorithmic Patterns for the Web.”</span> In. Zenodo.
|
||||
<a href="https://doi.org/10.5281/zenodo.6768844">https://doi.org/10.5281/zenodo.6768844</a>.
|
||||
</div>
|
||||
<div id="ref-solomonPurescriptocarina2022" class="csl-entry" role="doc-biblioentry">
|
||||
Solomon, Mike. (2021) 2022. <em>Purescript-Ocarina</em>. PureScript.
|
||||
<a href="https://github.com/mikesol/purescript-ocarina">https://github.com/mikesol/purescript-ocarina</a>.
|
||||
</div>
|
||||
<div id="ref-SuperDirt2022" class="csl-entry" role="doc-biblioentry">
|
||||
<em>SuperDirt</em>. (2015) 2022. SuperCollider. musikinformatik.
|
||||
<a href="https://github.com/musikinformatik/SuperDirt">https://github.com/musikinformatik/SuperDirt</a>.
|
||||
</div>
|
||||
<div id="ref-toussaintEuclideanAlgorithmGenerates2005" class="csl-entry" role="doc-biblioentry">
|
||||
Toussaint, Godfried. 2005. <span>“The Euclidean Algorithm Generates Traditional Musical Rhythms.”</span> In
|
||||
<em>In Proceedings of BRIDGES: Mathematical Connections in Art, Music and Science</em>, 47–56.
|
||||
<a href="http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231"
|
||||
>http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.62.231</a
|
||||
>.
|
||||
</div>
|
||||
<div id="ref-CsoundWebAssembly" class="csl-entry" role="doc-biblioentry">
|
||||
Yi, Steven, Victor Lazzarini, and Edward Costello. 2018.
|
||||
<span>“WebAssembly AudioWorklet Csound.”</span> In. Berlin, Germany.
|
||||
<a href="https://mural.maynoothuniversity.ie/16018/">https://mural.maynoothuniversity.ie/16018/</a>.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -451,7 +451,7 @@ While Haskell's type system makes it a great language for the ongoing developmen
|
||||
# Links
|
||||
|
||||
The Strudel REPL is available at <https://strudel.cc>, including an interactive tutorial.
|
||||
The repository is at <https://github.com/tidalcycles/strudel>, all the code is open source under the AGPL-3.0 License.
|
||||
The repository is at <https://codeberg.org/uzu/strudel>, all the code is open source under the AGPL-3.0 License.
|
||||
|
||||
# Acknowledgments
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
@@ -0,0 +1,63 @@
|
||||
$if(false)$ This is a pandoc template and should not be edited. $endif$
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" $if(lang)$ lang="$lang$" xml:lang="$lang$" $endif$>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||
<meta name="generator" content="pandoc" />
|
||||
$for(author-meta)$
|
||||
<meta name="author" content="$author-meta$" />
|
||||
$endfor$ $if(date-meta)$
|
||||
<meta name="date" content="$date-meta$" />
|
||||
$endif$
|
||||
<title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
|
||||
<style type="text/css">
|
||||
code {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
$if(quotes)$
|
||||
<style type="text/css">
|
||||
q {
|
||||
quotes: '“' '”' '‘' '’';
|
||||
}
|
||||
</style>
|
||||
$endif$ $if(highlighting-css)$
|
||||
<style type="text/css">
|
||||
$highlighting-css$
|
||||
</style>
|
||||
$endif$
|
||||
<link rel="stylesheet" href="css/iclc.css" $if(html5)$$else$type="text/css" $endif$ />
|
||||
$for(css)$
|
||||
<link rel="stylesheet" href="$css$" $if(html5)$$else$type="text/css" $endif$ />
|
||||
$endfor$ $if(math)$ $math$ $endif$ $for(header-includes)$ $header-includes$ $endfor$
|
||||
</head>
|
||||
<body>
|
||||
$for(include-before)$ $include-before$ $endfor$ $if(title)$
|
||||
<div id="$idprefix$header">
|
||||
<h1 class="title">$title$</h1>
|
||||
$if(subtitle)$
|
||||
<h1 class="subtitle">$subtitle$</h1>
|
||||
$endif$
|
||||
<ul id="authorlist">
|
||||
$for(author)$
|
||||
<li>$author$</li>
|
||||
$endfor$
|
||||
</ul>
|
||||
$if(date)$
|
||||
<h3 class="date">$date$</h3>
|
||||
$endif$
|
||||
</div>
|
||||
$endif$ $if(toc)$
|
||||
<div id="$idprefix$TOC">$toc$</div>
|
||||
$endif$
|
||||
|
||||
<h2 class="abstract">Abstract</h2>
|
||||
<div id="abstract">
|
||||
$if(abstract)$ $abstract$ $else$ Please provide an abstract in the metadata block at the top of your markdown
|
||||
document. Refer to template.txt for details. $endif$
|
||||
</div>
|
||||
|
||||
$body$ $for(include-after)$ $include-after$ $endfor$
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,8 @@
|
||||
This document introduces you to Strudel in a technical sense. If you just want to *use* Strudel, have a look at the [Tutorial](https://strudel.tidalcycles.org/tutorial/).
|
||||
This document introduces you to Strudel in a technical sense.
|
||||
|
||||
It is rather out of date, but there might still be useful info below.
|
||||
|
||||
If you just want to *use* Strudel, have a look at the [Tutorial](https://strudel.tidalcycles.org/tutorial/).
|
||||
|
||||
## Strudel Packages
|
||||
|
||||
@@ -17,11 +21,11 @@ More info in the [REPL README](https://codeberg.org/uzu/strudel/src/branch/main/
|
||||
|
||||
# High Level Overview
|
||||
|
||||
<img src="https://github.com/tidalcycles/strudel/blob/talk/talk/public/strudelflow.png?raw=true" width="600" />
|
||||
<img src="strudelflow.png" width="600" />
|
||||
|
||||
## 1. End User Code
|
||||
|
||||
The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://codeberg.org/uzu/strudel/src/branch/main/packages/eval#strudelcycleseval) evaluates the user code
|
||||
The End User Code is written in JavaScript with added syntax sugar. The [eval package](https://github.com/tidalcycles/strudel/tree/main/packages/eval#strudelcycleseval) evaluates the user code
|
||||
after a transpilation step, which resolves the syntax sugar. If you don't want the syntax sugar, you can omit the eval package and call the native javascript `eval` instead.
|
||||
|
||||
### 🍭 Syntax Sugar
|
||||
@@ -54,13 +58,13 @@ This is how it works:
|
||||
- The AST is transformed to resolve the syntax sugar
|
||||
- The AST is used to generate code again (shift-codegen)
|
||||
|
||||
Shift will most likely be replaced with acorn in the future, see https://codeberg.org/uzu/strudel/issues/174
|
||||
Shift will most likely be replaced with acorn in the future, see https://github.com/tidalcycles/strudel/issues/174
|
||||
|
||||
### Mini Notation
|
||||
|
||||
Another important part of the user code is the mini notation, which allows to express rhythms in a short manner.
|
||||
|
||||
- the mini notation is [implemented as a PEG grammar](https://codeberg.org/uzu/strudel/src/branch/main/packages/mini/krill.pegjs), living in the [mini package](https://codeberg.org/uzu/strudel/src/branch/main/packages/mini)
|
||||
- the mini notation is [implemented as a PEG grammar](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), living in the [mini package](https://github.com/tidalcycles/strudel/tree/main/packages/mini)
|
||||
- it is based on [krill](https://github.com/Mdashdotdashn/krill) by Mdashdotdashn
|
||||
- the peg grammar is used to generate a parser with [peggyjs](https://peggyjs.org/)
|
||||
- the generated parser takes a mini notation string and outputs an AST
|
||||
@@ -171,7 +175,7 @@ Here is an example Hap value with different properties:
|
||||
```js
|
||||
{ note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 }
|
||||
```
|
||||
<img src="https://github.com/tidalcycles/strudel/blob/talk/talk/public/waa-nodes.png?raw=true" width="600" />
|
||||
<img src="waa-nodes.png" width="600" />
|
||||
|
||||
<div className="text-left">
|
||||
|
||||
@@ -182,12 +186,12 @@ Here is an example Hap value with different properties:
|
||||
|
||||
At the time of writing this doc, the following outputs are supported:
|
||||
|
||||
- Web Audio API `.out()` see [/webaudio](https://codeberg.org/uzu/strudel/src/branch/main/packages/webaudio)
|
||||
- MIDI `.midi()` see [/midi](https://codeberg.org/uzu/strudel/src/branch/main/packages/midi)
|
||||
- OSC `.osc()` see [/osc](https://codeberg.org/uzu/strudel/src/branch/main/packages/osc)
|
||||
- Serial `.serial()` see [/serial](https://codeberg.org/uzu/strudel/src/branch/main/packages/serial)
|
||||
- Tone.js `.tone()` (deprecated?) [/tone](https://codeberg.org/uzu/strudel/src/branch/main/packages/tone)
|
||||
- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://codeberg.org/uzu/strudel/src/branch/main/packages/webdirt)
|
||||
- Speech `.speak()` (experimental) part of [/core](https://codeberg.org/uzu/strudel/src/branch/main/packages/core)
|
||||
- Web Audio API `.out()` see [/webaudio](https://github.com/tidalcycles/strudel/tree/main/packages/webaudio)
|
||||
- MIDI `.midi()` see [/midi](https://github.com/tidalcycles/strudel/tree/main/packages/midi)
|
||||
- OSC `.osc()` see [/osc](https://github.com/tidalcycles/strudel/tree/main/packages/osc)
|
||||
- Serial `.serial()` see [/serial](https://github.com/tidalcycles/strudel/tree/main/packages/serial)
|
||||
- Tone.js `.tone()` (deprecated?) [/tone](https://github.com/tidalcycles/strudel/tree/main/packages/tone)
|
||||
- WebDirt `.webdirt()` (deprecated?) [/webdirt](https://github.com/tidalcycles/strudel/tree/main/packages/webdirt)
|
||||
- Speech `.speak()` (experimental) part of [/core](https://github.com/tidalcycles/strudel/tree/main/packages/core)
|
||||
|
||||
These could change, so make sure to check the [packages folder](https://codeberg.org/uzu/strudel/src/branch/main/packages).
|
||||
These could change, so make sure to check the [packages folder](https://github.com/tidalcycles/strudel/tree/main/packages).
|
||||
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 49 KiB |
@@ -83,4 +83,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/>.
|
||||
*/
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
Each folder represents one of the @strudel/* packages [published to npm](https://www.npmjs.com/org/strudel).
|
||||
|
||||
To understand how those pieces connect, refer to the [Technical Manual](https://codeberg.org/uzu/strudel/src/branch/main/technical-manual.md) or the individual READMEs.
|
||||
To understand how those pieces connect, refer to the [Technical Manual](https://codeberg.org/uzu/strudel/wiki/Technical-Manual) or the individual READMEs.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { history, indentWithTab } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
|
||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||
@@ -37,6 +37,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 +59,8 @@ export const defaultSettings = {
|
||||
isFlashEnabled: true,
|
||||
isTooltipEnabled: false,
|
||||
isLineWrappingEnabled: false,
|
||||
isTabIndentationEnabled: false,
|
||||
isMultiCursorEnabled: false,
|
||||
theme: 'strudelTheme',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 18,
|
||||
@@ -62,7 +72,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,7 +85,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
/* search(),
|
||||
highlightSelectionMatches(), */
|
||||
...initialSettings,
|
||||
javascript(),
|
||||
mondo ? [] : javascript(),
|
||||
sliderPlugin,
|
||||
widgetPlugin,
|
||||
// indentOnInput(), // works without. already brought with javascript extension?
|
||||
@@ -209,6 +219,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) : [];
|
||||
};
|
||||
|
||||
@@ -21,11 +21,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 ? active() : [], keymap.of(historyKeymap)];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/codemirror",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "Codemirror Extensions for Strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
|
||||
@@ -8,6 +8,11 @@ import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs';
|
||||
import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs';
|
||||
import redText, { settings as redTextSettings } from './themes/red-text.mjs';
|
||||
import greenText, { settings as greenTextSettings } from './themes/green-text.mjs';
|
||||
import archBtw, { settings as archBtwSettings } from './themes/archBtw.mjs';
|
||||
import fruitDaw, { settings as fruitDawSettings } from './themes/fruitDaw.mjs';
|
||||
|
||||
import bluescreenlight, { settings as bluescreenlightsettings } from './themes/bluescreenlight.mjs';
|
||||
|
||||
import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
|
||||
import atomone, { settings as atomOneSettings } from './themes/atomone.mjs';
|
||||
import aura, { settings as auraSettings } from './themes/aura.mjs';
|
||||
@@ -39,17 +44,20 @@ import { setTheme } from '@strudel/draw';
|
||||
export const themes = {
|
||||
strudelTheme,
|
||||
algoboy,
|
||||
archBtw,
|
||||
androidstudio,
|
||||
atomone,
|
||||
aura,
|
||||
bbedit,
|
||||
blackscreen,
|
||||
bluescreen,
|
||||
bluescreenlight,
|
||||
CutiePi,
|
||||
darcula,
|
||||
dracula,
|
||||
duotoneDark,
|
||||
eclipse,
|
||||
fruitDaw,
|
||||
githubDark,
|
||||
githubLight,
|
||||
greenText,
|
||||
@@ -78,10 +86,12 @@ export const themes = {
|
||||
export const settings = {
|
||||
strudelTheme: strudelThemeSettings,
|
||||
bluescreen: bluescreenSettings,
|
||||
bluescreenlight: bluescreenlightsettings,
|
||||
blackscreen: blackscreenSettings,
|
||||
whitescreen: whitescreenSettings,
|
||||
teletext: teletextSettings,
|
||||
algoboy: algoboySettings,
|
||||
archBtw: archBtwSettings,
|
||||
androidstudio: androidstudioSettings,
|
||||
atomone: atomOneSettings,
|
||||
aura: auraSettings,
|
||||
@@ -92,9 +102,11 @@ export const settings = {
|
||||
eclipse: eclipseSettings,
|
||||
CutiePi: CutiePiSettings,
|
||||
sonicPink: sonicPinkSettings,
|
||||
fruitDaw: fruitDawSettings,
|
||||
githubLight: githubLightSettings,
|
||||
githubDark: githubDarkSettings,
|
||||
greenText: greenTextSettings,
|
||||
|
||||
gruvboxDark: gruvboxDarkSettings,
|
||||
gruvboxLight: gruvboxLightSettings,
|
||||
materialDark: materialDarkSettings,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Arch Btw
|
||||
* Modern terminal inspired theme
|
||||
* made by Jade
|
||||
*/
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
const hex = ['rgb(0, 0, 0)', 'rgb(82, 208, 250)', 'rgba(113, 208, 250, .4)', 'rgba(113, 208, 250, .15)'];
|
||||
|
||||
export const settings = {
|
||||
background: hex[0],
|
||||
lineBackground: 'transparent',
|
||||
foreground: hex[1],
|
||||
selection: hex[2],
|
||||
selectionMatch: hex[0],
|
||||
gutterBackground: hex[0],
|
||||
gutterForeground: hex[2],
|
||||
gutterBorder: 'transparent',
|
||||
lineHighlight: hex[0],
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{
|
||||
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||
color: hex[1],
|
||||
},
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
|
||||
{ tag: [t.comment, t.brace, t.bracket], color: hex[2] },
|
||||
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[1] },
|
||||
{ tag: [t.attributeName, t.number], color: hex[1] },
|
||||
{ tag: t.keyword, color: hex[1] },
|
||||
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* A lighter blue screen theme
|
||||
* made by Jade
|
||||
*/
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
const hex = ['rgb(75, 130, 247)', 'rgb(47, 108, 246)', 'rgb(255, 255, 255)', 'rgba(255, 255, 255,.3)'];
|
||||
|
||||
export const settings = {
|
||||
background: hex[0],
|
||||
lineBackground: 'transparent',
|
||||
foreground: hex[2],
|
||||
selection: hex[3],
|
||||
selectionMatch: hex[0],
|
||||
gutterBackground: hex[0],
|
||||
gutterForeground: hex[2],
|
||||
gutterBorder: 'transparent',
|
||||
lineHighlight: hex[1],
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{
|
||||
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||
color: hex[2],
|
||||
},
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[2] },
|
||||
{ tag: [t.comment, t.bracket, t.brace, t.compareOperator], color: hex[3] },
|
||||
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
|
||||
{ tag: [t.attributeName, t.number], color: hex[2] },
|
||||
{ tag: t.keyword, color: hex[2] },
|
||||
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Fruit Daw
|
||||
* made by Jade
|
||||
*/
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from './theme-helper.mjs';
|
||||
|
||||
const hex = [
|
||||
'rgb(84, 93, 98)',
|
||||
'rgb(255, 255, 255)',
|
||||
'rgba(255, 255, 255, .25)',
|
||||
'rgb(67, 76, 81)',
|
||||
'rgb(186, 230, 115)',
|
||||
'rgb(252, 184, 67)',
|
||||
'rgb(124, 206, 254)',
|
||||
'rgb(83, 101, 102)',
|
||||
'rgba(46, 62, 72,.5)',
|
||||
'rgb(94, 100, 108)',
|
||||
'rgb(167, 216, 177)',
|
||||
];
|
||||
|
||||
export const settings = {
|
||||
background: hex[0],
|
||||
lineBackground: 'transparent',
|
||||
foreground: hex[10],
|
||||
selection: hex[8],
|
||||
selectionMatch: hex[0],
|
||||
gutterBackground: hex[3],
|
||||
gutterForeground: hex[2],
|
||||
gutterBorder: 'transparent',
|
||||
lineHighlight: hex[3],
|
||||
};
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{
|
||||
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
|
||||
color: hex[1],
|
||||
},
|
||||
{ tag: [t.bool, t.special(t.variableName)], color: hex[1] },
|
||||
{ tag: [t.comment, t.brace, t.bracket], color: hex[2] },
|
||||
{ tag: [t.variableName], color: hex[1] },
|
||||
{ tag: [t.labelName, t.propertyName, t.self, t.atom], color: hex[5] },
|
||||
{ tag: [t.attributeName, t.number], color: hex[6] },
|
||||
{ tag: t.keyword, color: hex[5] },
|
||||
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[4] },
|
||||
],
|
||||
});
|
||||
@@ -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);
|
||||
@@ -242,6 +252,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.
|
||||
*
|
||||
@@ -296,6 +320,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)
|
||||
*
|
||||
@@ -434,6 +459,78 @@ export const { crush } = registerControl('crush');
|
||||
*/
|
||||
export const { coarse } = registerControl('coarse');
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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
|
||||
*
|
||||
@@ -443,6 +540,42 @@ export const { coarse } = registerControl('coarse');
|
||||
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* modulate the amplitude of an orbit to create a "sidechain" like effect
|
||||
*
|
||||
* @name duckorbit
|
||||
* @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)
|
||||
*
|
||||
*/
|
||||
export const { duck } = registerControl('duckorbit', 'duck');
|
||||
|
||||
/**
|
||||
* the amount of ducking applied to target orbit
|
||||
*
|
||||
* @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>"))
|
||||
*
|
||||
*/
|
||||
|
||||
export const { duckdepth } = registerControl('duckdepth');
|
||||
|
||||
/**
|
||||
* the attack time of the duck effect
|
||||
*
|
||||
* @name duckattack
|
||||
* @param {number | Pattern} time
|
||||
* @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 0 0.4>").duckdepth(1))
|
||||
*
|
||||
*/
|
||||
export const { duckattack } = registerControl('duckattack', 'duckatt');
|
||||
|
||||
export const { drive } = registerControl('drive');
|
||||
|
||||
/**
|
||||
@@ -975,15 +1108,31 @@ export const { delayfeedback, delayfb, dfb } = registerControl('delayfeedback',
|
||||
*
|
||||
*/
|
||||
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
|
||||
@@ -1535,18 +1684,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 +1760,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,9 +10,9 @@ 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 { Pattern, 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 from './fraction.mjs';
|
||||
import Fraction, { lcm } from './fraction.mjs';
|
||||
|
||||
const left = function (n, x) {
|
||||
const [ons, offs] = n;
|
||||
@@ -42,29 +42,26 @@ const _bjork = function (n, x) {
|
||||
|
||||
export const bjork = function (ons, steps) {
|
||||
const inverted = ons < 0;
|
||||
ons = Math.abs(ons);
|
||||
const offs = steps - ons;
|
||||
const x = Array(ons).fill([1]);
|
||||
const y = Array(offs).fill([0]);
|
||||
const result = _bjork([ons, offs], [x, y]);
|
||||
const p = flatten(result[1][0]).concat(flatten(result[1][1]));
|
||||
if (inverted) {
|
||||
return p.map((x) => (x === 0 ? 1 : 0));
|
||||
}
|
||||
return p;
|
||||
const absOns = Math.abs(ons);
|
||||
const offs = steps - absOns;
|
||||
const ones = Array(absOns).fill([1]);
|
||||
const zeros = Array(offs).fill([0]);
|
||||
const result = _bjork([absOns, offs], [ones, zeros]);
|
||||
const pattern = flatten(result[1][0]).concat(flatten(result[1][1]));
|
||||
return inverted ? pattern.map((x) => 1 - x) : pattern;
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes the structure of the pattern to form an euclidean rhythm.
|
||||
* Euclidian rhythms are rhythms obtained using the greatest common
|
||||
* Changes the structure of the pattern to form an Euclidean rhythm.
|
||||
* Euclidean rhythms are rhythms obtained using the greatest common
|
||||
* divisor of two numbers. They were described in 2004 by Godfried
|
||||
* Toussaint, a canadian computer scientist. Euclidian rhythms are
|
||||
* Toussaint, a Canadian computer scientist. Euclidean rhythms are
|
||||
* really useful for computer/algorithmic music because they can
|
||||
* describe a large number of rhythms with a couple of numbers.
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name euclid
|
||||
* @param {number} pulses the number of onsets / beats
|
||||
* @param {number} pulses the number of onsets/beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @returns Pattern
|
||||
* @example
|
||||
@@ -76,7 +73,7 @@ export const bjork = function (ons, steps) {
|
||||
* Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence.
|
||||
* @memberof Pattern
|
||||
* @name euclidRot
|
||||
* @param {number} pulses the number of onsets / beats
|
||||
* @param {number} pulses the number of onsets/beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param {number} rotation offset in steps
|
||||
* @returns Pattern
|
||||
@@ -86,13 +83,13 @@ export const bjork = function (ons, steps) {
|
||||
*/
|
||||
|
||||
/**
|
||||
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
|
||||
* @example // A thirteenth-century Persian rhythm called Khafif-e-ramal.
|
||||
* note("c3").euclid(2,5)
|
||||
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
|
||||
* note("c3").euclid(3,4)
|
||||
* @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm.
|
||||
* note("c3").euclidRot(3,5,2)
|
||||
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
|
||||
* @example // A Ruchenitza rhythm used in a Bulgarian folk dance.
|
||||
* note("c3").euclid(3,7)
|
||||
* @example // The Cuban tresillo pattern.
|
||||
* note("c3").euclid(3,8)
|
||||
@@ -142,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));
|
||||
});
|
||||
@@ -151,8 +156,10 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
|
||||
* so there will be no gaps.
|
||||
* @name euclidLegato
|
||||
* @memberof Pattern
|
||||
* @param {number} pulses the number of onsets / beats
|
||||
* @param {number} pulses the number of onsets/beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param rotation offset in steps
|
||||
* @param pat
|
||||
* @example
|
||||
* note("c3").euclidLegato(3,8)
|
||||
*/
|
||||
@@ -161,13 +168,13 @@ const _euclidLegato = function (pulses, steps, rotation, pat) {
|
||||
if (pulses < 1) {
|
||||
return silence;
|
||||
}
|
||||
const bin_pat = _euclidRot(pulses, steps, rotation);
|
||||
const bin_pat = _euclidRot(pulses, steps, 0);
|
||||
const gapless = bin_pat
|
||||
.join('')
|
||||
.split('1')
|
||||
.slice(1)
|
||||
.map((s) => [s.length + 1, true]);
|
||||
return pat.struct(timeCat(...gapless));
|
||||
return pat.struct(timeCat(...gapless)).late(Fraction(rotation).div(steps));
|
||||
};
|
||||
|
||||
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {
|
||||
@@ -180,7 +187,7 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
|
||||
* the resulting sequence
|
||||
* @name euclidLegatoRot
|
||||
* @memberof Pattern
|
||||
* @param {number} pulses the number of onsets / beats
|
||||
* @param {number} pulses the number of onsets/beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param {number} rotation offset in steps
|
||||
* @example
|
||||
@@ -189,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.3",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -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
|
||||
@@ -1260,7 +1305,7 @@ export function sequenceP(pats) {
|
||||
* @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:
|
||||
@@ -1541,7 +1586,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
|
||||
*
|
||||
@@ -1981,6 +2026,7 @@ export const apply = register('apply', function (func, pat) {
|
||||
|
||||
/**
|
||||
* Plays the pattern at the given cycles per minute.
|
||||
* @deprecated
|
||||
* @example
|
||||
* s("<bd sd>,hh*2").cpm(90) // = 90 bpm
|
||||
*/
|
||||
@@ -2490,6 +2536,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',
|
||||
@@ -2505,7 +2582,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
|
||||
@@ -2719,7 +2796,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);
|
||||
@@ -3203,7 +3280,7 @@ export const slice = register(
|
||||
* s("bd!8").onTriggerTime((hap) => {console.info(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);
|
||||
@@ -3340,3 +3417,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))));
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
@@ -85,6 +86,17 @@ export function repl({
|
||||
const pause = () => scheduler.pause();
|
||||
const toggle = () => scheduler.toggle();
|
||||
const setCps = (cps) => scheduler.setCps(cps);
|
||||
|
||||
/**
|
||||
* Changes the global tempo to the given cycles per minute
|
||||
*
|
||||
* @name setcpm
|
||||
* @alias setCpm
|
||||
* @param {number} cpm cycles per minute
|
||||
* @example
|
||||
* setcpm(140/4) // =140 bpm in 4/4
|
||||
* $: s("bd*4,[- sd]*2").bank('tr707')
|
||||
*/
|
||||
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
||||
|
||||
// TODO - not documented as jsdoc examples as the test framework doesn't simulate enough context for `each` and `all`..
|
||||
@@ -102,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.
|
||||
@@ -179,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);
|
||||
@@ -191,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`;
|
||||
@@ -225,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) {
|
||||
@@ -232,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")
|
||||
@@ -266,7 +264,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 +283,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 +296,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 +396,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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
controls.test.mjs - <short description TODO>
|
||||
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/controls.test.mjs>
|
||||
Copyright (C) 2023 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/controls.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
drawLine.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/drawLine.test.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/drawLine.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/>.
|
||||
*/
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { bjork } from '../euclid.mjs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { fastcat } from '../pattern.mjs';
|
||||
|
||||
describe('bjork', () => {
|
||||
it('should apply bjorklund to ons and steps', () => {
|
||||
expect(bjork(3, 8)).toStrictEqual([1, 0, 0, 1, 0, 0, 1, 0]);
|
||||
expect(bjork(-3, 8)).toStrictEqual([0, 1, 1, 0, 1, 1, 0, 1]);
|
||||
expect(bjork(8, 8)).toStrictEqual([1, 1, 1, 1, 1, 1, 1, 1]);
|
||||
expect(bjork(-8, 8)).toStrictEqual([0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
expect(bjork(5, 8)).toStrictEqual([1, 0, 1, 1, 0, 1, 1, 0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('euclid', () => {
|
||||
it('Can create euclid', () => {
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclid(3, 8)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 1/8: a', '3/8 → 1/2: a', '3/4 → 7/8: a']);
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclid(5, 8)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '3/8 → 1/2: a', '5/8 → 3/4: a', '3/4 → 7/8: a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('euclidRot', () => {
|
||||
it('Can create euclidRot', () => {
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclidRot(3, 8, 2)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '5/8 → 3/4: a']);
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclidRot(5, 8, 2)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 1/8: a', '1/4 → 3/8: a', '1/2 → 5/8: a', '5/8 → 3/4: a', '7/8 → 1/1: a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('euclidLegato', () => {
|
||||
it('Can create euclidLegato', () => {
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclidLegato(3, 8)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 3/8: a', '3/8 → 3/4: a', '3/4 → 1/1: a']);
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclidLegato(5, 8)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 1/4: a', '1/4 → 3/8: a', '3/8 → 5/8: a', '5/8 → 3/4: a', '3/4 → 1/1: a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('euclidLegatoRot', () => {
|
||||
it('Can create euclidLegatoRot', () => {
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclidLegatoRot(3, 8, 2)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 1/4: a', '1/4 → 5/8: a', '5/8 → 1/1: a']);
|
||||
expect(
|
||||
fastcat('a')
|
||||
.euclidLegatoRot(5, 8, 2)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.showWhole(true)),
|
||||
).toStrictEqual(['0/1 → 1/4: a', '1/4 → 1/2: a', '1/2 → 5/8: a', '5/8 → 7/8: a', '7/8 → 1/1: a']);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
fraction.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/fraction.test.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/fraction.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/>.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
pattern.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/pattern.test.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/pattern.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 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';
|
||||
@@ -884,7 +887,7 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
it('Doesnt drop haps in the 9th cycle', () => {
|
||||
// fixed with https://github.com/tidalcycles/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
|
||||
// fixed with https://codeberg.org/uzu/strudel/commit/72eeaf446e3d5e186d63cc0d2276f0723cde017a
|
||||
expect(sequence(1, 2, 3).ply(2).early(8).firstCycle().length).toBe(6);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
/*
|
||||
util.test.mjs - Tests for the core 'util' module
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/util.test.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/test/util.test.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { pure } from '../pattern.mjs';
|
||||
import {
|
||||
isNote,
|
||||
tokenizeNote,
|
||||
noteToMidi,
|
||||
midiToFreq,
|
||||
freqToMidi,
|
||||
_mod,
|
||||
compose,
|
||||
flatten,
|
||||
fractionalArgs,
|
||||
freqToMidi,
|
||||
getFrequency,
|
||||
getPlayableNoteValue,
|
||||
parseNumeral,
|
||||
parseFractional,
|
||||
isNote,
|
||||
midiToFreq,
|
||||
noteToMidi,
|
||||
numeralArgs,
|
||||
fractionalArgs,
|
||||
parseFractional,
|
||||
parseNumeral,
|
||||
rotate,
|
||||
splitAt,
|
||||
tokenizeNote,
|
||||
zipWith,
|
||||
} from '../util.mjs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('isNote', () => {
|
||||
it('should recognize notes without accidentals', () => {
|
||||
@@ -233,3 +237,46 @@ describe('fractionalArgs', () => {
|
||||
expect(add('q', 2)).toBe(2.25);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rotate', () => {
|
||||
it('should rotate array to the left', () => {
|
||||
expect(rotate([0, 1, 2, 3], 2)).toStrictEqual([2, 3, 0, 1]);
|
||||
expect(rotate([0, 1, 2, 3], 0)).toStrictEqual([0, 1, 2, 3]);
|
||||
expect(rotate([0, 1, 2, 3], -3)).toStrictEqual([1, 2, 3, 0]);
|
||||
expect(rotate([0, 1, 2, 3], 3)).toStrictEqual([3, 0, 1, 2]);
|
||||
expect(rotate([0], 3)).toStrictEqual([0]);
|
||||
expect(rotate([], 3)).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flatten', () => {
|
||||
it('should flatten array by one level', () => {
|
||||
expect(flatten([0, 1, 2, 3])).toStrictEqual([0, 1, 2, 3]);
|
||||
expect(flatten([0, 1, [2, 3]])).toStrictEqual([0, 1, 2, 3]);
|
||||
expect(flatten([0, [1, [2, 3]]])).toStrictEqual([0, 1, [2, 3]]);
|
||||
expect(flatten([0])).toStrictEqual([0]);
|
||||
expect(flatten([])).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitAt', () => {
|
||||
it('should split array into two', () => {
|
||||
expect(splitAt(2, [0, 1, 2, 3])).toStrictEqual([
|
||||
[0, 1],
|
||||
[2, 3],
|
||||
]);
|
||||
expect(splitAt(0, [0, 1, 2, 3])).toStrictEqual([[], [0, 1, 2, 3]]);
|
||||
expect(splitAt(-3, [0, 1, 2, 3])).toStrictEqual([[0], [1, 2, 3]]);
|
||||
expect(splitAt(3, [0, 1, 2, 3])).toStrictEqual([[0, 1, 2], [3]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('zipWith', () => {
|
||||
it('should use the function to combine the two arrays element-wise', () => {
|
||||
expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2, 3])).toStrictEqual([0, 2, 4, 6]);
|
||||
expect(zipWith((a, b) => a + b, [0, 1, 2, 3], [0, 1, 2])).toStrictEqual([0, 2, 4, NaN]);
|
||||
expect(zipWith((a, b) => a + b, [0, 1, 2], [0, 1, 2, 3])).toStrictEqual([0, 2, 4]);
|
||||
expect(zipWith((a) => a, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([0, 1, 2]);
|
||||
expect(zipWith((a, b) => b, [0, 1, 2], [1, 2, 3, 0])).toStrictEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ export const compose = (...funcs) => pipe(...funcs.reverse());
|
||||
// Removes 'None' values from given list
|
||||
export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
|
||||
|
||||
// flattens by one level
|
||||
export const flatten = (arr) => [].concat(...arr);
|
||||
|
||||
export const id = (a) => a;
|
||||
@@ -237,6 +238,7 @@ export const splitAt = function (index, value) {
|
||||
return [value.slice(0, index), value.slice(index)];
|
||||
};
|
||||
|
||||
// Uses the function f to combine the arrays xs, ys element-wise
|
||||
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||
|
||||
export const pairs = function (xs) {
|
||||
@@ -485,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.4",
|
||||
"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.3",
|
||||
"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",
|
||||
|
||||
@@ -56,14 +56,15 @@ You can use button inputs to control different aspects of your music, such as ga
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`const gp = gamepad(0)
|
||||
// Use button values to control amplitude
|
||||
setcpm(120)
|
||||
// Use button values to control amplitude
|
||||
$: stack(
|
||||
s("[[hh hh] oh hh oh]/2").mask(gp.tglX).bank("RolandTR909"), // X btn for HH
|
||||
s("cr*1").mask(gp.Y).bank("RolandTR909"), // LB btn for CR
|
||||
s("bd").mask(gp.tglA).bank("RolandTR909"), // A btn for BD
|
||||
s("[ht - - mt - - lt - ]/2").mask(gp.tglB).bank("RolandTR909"), // B btn for Toms
|
||||
s("sd*4").mask(gp.RB).bank("RolandTR909"), // RB btn for SD
|
||||
).cpm(120)
|
||||
)
|
||||
`}
|
||||
/>
|
||||
|
||||
@@ -74,14 +75,13 @@ Analog sticks can be used for continuous control, such as pitch shifting or pann
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`const gp = gamepad(0)
|
||||
// Use analog stick for continuous control
|
||||
setcpm(120)
|
||||
// Use analog stick for continuous control
|
||||
$: note("c4 d3 a3 e3").sound("sawtooth")
|
||||
.lpf(gp.x1.range(100,4000))
|
||||
.lpq(gp.y1.range(5,30))
|
||||
.decay(gp.y2.range(0.1,2))
|
||||
.lpenv(gp.x2.range(-5,5))
|
||||
.cpm(120)
|
||||
`}
|
||||
.lpenv(gp.x2.range(-5,5))`}
|
||||
/>
|
||||
|
||||
### Button Sequences
|
||||
@@ -89,6 +89,7 @@ $: note("c4 d3 a3 e3").sound("sawtooth")
|
||||
You can define button sequences to trigger specific actions, like playing a sound when a sequence is detected.
|
||||
|
||||
<MiniRepl client:idle tune={`const gp = gamepad(0)
|
||||
setcpm(120)
|
||||
// Define button sequences
|
||||
const HADOUKEN = [
|
||||
'd', // Down
|
||||
@@ -99,7 +100,7 @@ const KONAMI = 'uuddlrlrba' //Konami Code ↑↑↓↓←→←→BA
|
||||
|
||||
// Check butto-n sequence (returns 1 while detected, 0 when not within last 1 second)
|
||||
$: s("free_hadouken -").slow(2)
|
||||
.mask(gp.btnSequence(HADOUKEN)).room(1).cpm(120)
|
||||
.mask(gp.btnSequence(HADOUKEN)).room(1)
|
||||
|
||||
// hadouken.wav by Syna-Max
|
||||
//https://freesound.org/people/Syna-Max/sounds/67674/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/gamepad",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "Gamepad Inputs for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/hydra",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/midi",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mini",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
mini.test.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/mini/test/mini.test.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/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/>.
|
||||
*/
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
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.1",
|
||||
"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.3",
|
||||
"description": "DeviceMotion API for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/mqtt",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.3",
|
||||
"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.3",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"type": "module",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -17,7 +17,7 @@ You can also pin the version like this:
|
||||
```
|
||||
|
||||
This has the advantage that your code will always work, regardless of potential breaking changes in the strudel codebase.
|
||||
See [releases](https://github.com/tidalcycles/strudel/releases) for the latest versions.
|
||||
See [releases](https://codeberg.org/uzu/strudel/releases) for the latest versions.
|
||||
|
||||
## Use Web Component
|
||||
|
||||
@@ -91,7 +91,7 @@ or
|
||||
</script>
|
||||
```
|
||||
|
||||
The `.editor` property on the `strudel-editor` web component gives you the instance of [StrudelMirror](https://github.com/tidalcycles/strudel/blob/a46bd9b36ea7d31c9f1d3fca484297c7da86893f/packages/codemirror/codemirror.mjs#L124) that runs the REPL.
|
||||
The `.editor` property on the `strudel-editor` web component gives you the instance of [StrudelMirror](https://codeberg.org/uzu/strudel/src/branch/a46bd9b36ea7d31c9f1d3fca484297c7da86893f/packages/codemirror/codemirror.mjs#L124) that runs the REPL.
|
||||
|
||||
For example, you could use `setCode` to change the code from the outside, `start` / `stop` to toggle playback or `evaluate` to evaluate the code.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel/repl",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"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`),
|
||||
]);
|
||||
|
||||