Compare commits

..

162 Commits

Author SHA1 Message Date
Felix Roos 5588b62b40 Publish
- @strudel.cycles/webaudio@0.0.4
2022-04-16 15:24:40 +02:00
Felix Roos 03cb80b75b add core dependeny 2022-04-16 15:24:24 +02:00
Felix Roos 86a3fd6e22 Publish
- @strudel.cycles/webaudio@0.0.3
2022-04-16 15:17:30 +02:00
Felix Roos 27ebe7ba73 stub change 2022-04-16 15:17:02 +02:00
Felix Roos 0ce2dcacef Publish
- @strudel.cycles/core@0.0.5
 - @strudel.cycles/eval@0.0.5
 - @strudel.cycles/midi@0.0.6
 - @strudel.cycles/mini@0.0.6
 - @strudel.cycles/tonal@0.0.5
 - @strudel.cycles/tone@0.0.6
 - @strudel.cycles/xen@0.0.5
2022-04-16 15:14:29 +02:00
Felix Roos 6c8fc6bb21 core publish 2022-04-16 15:12:06 +02:00
Felix Roos 0e26d4e741 Publish
- @strudel.cycles/core@0.0.4
 - @strudel.cycles/eval@0.0.4
 - @strudel.cycles/midi@0.0.5
 - @strudel.cycles/mini@0.0.5
 - @strudel.cycles/osc@0.0.2
 - @strudel.cycles/tonal@0.0.4
 - @strudel.cycles/tone@0.0.5
 - @strudel.cycles/webaudio@0.0.2
 - @strudel.cycles/xen@0.0.4
2022-04-16 15:10:49 +02:00
alex 3d6f01c872 build 2022-04-16 12:39:02 +01:00
alex 4894588284 fix degrade/undegrade 2022-04-16 12:37:47 +01:00
alex 7adea0ac65 build 2022-04-16 12:29:05 +01:00
Alex McLean 138c84cd5f Merge pull request #74 from tidalcycles/more-functions
More random functions
2022-04-16 12:26:57 +01:00
alex d283791f77 Merge branch 'more-functions' of github.com:tidalcycles/strudel into more-functions 2022-04-16 12:24:27 +01:00
alex 911d9d17d6 undegrade, sometimes, sometimesBy, sometimesByPre, often, rarely, almostNever, almostAlways, never, always 2022-04-16 12:19:42 +01:00
alex 8abbaf2e56 undegrade, sometimes, sometimesBy, sometimesByPre, often, rarely, almostNever, almostAlways, never, always 2022-04-16 12:18:44 +01:00
alex dca8b46abf add degrade/degradeBy 2022-04-16 11:40:47 +01:00
Alex McLean 2b968c4387 Merge pull request #73 from tidalcycles/more-functions
Port `perlin` noise, `rangex`, and `palindrome`
2022-04-16 10:13:25 +01:00
alex c88508ace3 tidy 2022-04-16 10:12:01 +01:00
alex b4adbcd6cb Merge branch 'main' into more-functions 2022-04-16 10:10:00 +01:00
alex 031b93f062 fix palindrome 2022-04-16 10:09:47 +01:00
alex f1ac9c4443 perlin noise 2022-04-16 10:03:00 +01:00
alex 544e32e0f8 add 'constant' 2022-04-16 10:02:35 +01:00
Felix Roos f8f806e8eb Merge pull request #26 from tidalcycles/webaudio
webaudio package
2022-04-16 10:26:06 +02:00
Felix Roos 2aed40d99b add example info 2022-04-16 10:24:09 +02:00
Felix Roos d0443a7f41 Merge remote-tracking branch 'origin/HEAD' into webaudio 2022-04-16 10:21:41 +02:00
Felix Roos 1d83ad5282 add randomBells 2022-04-16 00:13:18 +02:00
alex 8fda84c15a palindrome 2022-04-15 22:42:03 +01:00
alex 939bc643be rangex 2022-04-15 22:41:39 +01:00
alex c163039c6e build 2022-04-15 22:06:02 +01:00
Felix Roos 9c6652b19d remove old imports 2022-04-15 21:45:55 +02:00
Felix Roos 2510399017 Merge remote-tracking branch 'origin/HEAD' into webaudio 2022-04-15 20:54:33 +02:00
Felix Roos 9d2cbbaa2c fix notes example 2022-04-15 20:40:58 +02:00
Alex McLean f47d4d0c9d Merge pull request #70 from tidalcycles/more-functions
More randomness, fix `rand`, and add `brand`, `irand` and `choose`
2022-04-15 19:18:06 +01:00
alex f41d89515c Merge branch 'main' into more-functions 2022-04-15 19:15:32 +01:00
alex ea16d6b050 Add choose, fix rand to be unipolar. 2022-04-15 16:05:48 +01:00
alex 19f7dc1657 add brand (boolean rand) and brandBy 2022-04-15 15:05:30 +01:00
alex b674014377 Ignore continuous haps (those without a whole) 2022-04-15 15:04:16 +01:00
Alex McLean 802ed9d481 Merge pull request #69 from tidalcycles/more-functions
First effort at rand()
2022-04-15 10:29:51 +01:00
alex 1f1b20041c Merge branch 'main' of github.com:tidalcycles/strudel into more-functions 2022-04-15 10:15:12 +01:00
alex 3ae0762f62 First effort at rand() 2022-04-15 10:14:51 +01:00
Felix Roos 233cc4bdc4 update task instructions 2022-04-15 09:33:46 +02:00
Felix Roos 8dde34565a add speakerman 2022-04-14 19:06:33 +02:00
Felix Roos 14774ae687 build 2022-04-14 18:26:53 +02:00
Felix Roos ed4156530b add build script 2022-04-14 18:18:53 +02:00
Felix Roos 45e8cd104b package-lock 2022-04-14 17:42:51 +02:00
Felix Roos fc195eae1e Merge pull request #68 from tidalcycles/osc-fixed
use new fixed version of osc-js package
2022-04-14 17:41:18 +02:00
Felix Roos 18d8049f37 use new fixed version of osc-js package 2022-04-14 17:37:44 +02:00
Felix Roos ff32485f67 fix header 2022-04-14 00:35:15 +02:00
Felix Roos d718eb0be2 add readme 2022-04-14 00:32:45 +02:00
Felix Roos 325eec2c6f refactor scheduler constructor + add onEvent 2022-04-14 00:32:41 +02:00
Felix Roos 5629c0ee72 npm i 2022-04-14 00:32:09 +02:00
Felix Roos db20ff93c7 fix lockfile 2022-04-14 00:15:27 +02:00
Felix Roos 192eb28ac7 Merge remote-tracking branch 'origin/main' into webaudio 2022-04-14 00:12:47 +02:00
Felix Roos 02738a0694 Merge pull request #67 from tidalcycles/speak
Speech output
2022-04-14 00:09:17 +02:00
Felix Roos 61892bdf3d fix minirepl import 2022-04-13 23:58:51 +02:00
Felix Roos 02ece1183e export patternify 2022-04-13 23:58:44 +02:00
Felix Roos 38fe310a7c fix import 2022-04-13 23:58:34 +02:00
Felix Roos 70fbbd65e9 Merge remote-tracking branch 'origin/main' into speak 2022-04-13 23:58:24 +02:00
Alex McLean 2973223fb1 Merge pull request #62 from tidalcycles/tidy-modules
Separate out strudel.mjs, make index.mjs aggregate module
2022-04-13 22:55:32 +01:00
Felix Roos 1dd19c0831 add speak output 2022-04-13 23:25:55 +02:00
Felix Roos 16e2c97e0c export patternify helpers 2022-04-13 23:25:18 +02:00
alex 050296d0c2 make separate signal module 2022-04-13 17:41:48 +01:00
alex f75da0e2e3 separate out strudel.mjs, make index.mjs aggregate module 2022-04-13 17:25:02 +01:00
Alex McLean 0396c3f475 Merge pull request #61 from tidalcycles/more-functions
More functions
2022-04-13 16:26:45 +01:00
alex 97044fee16 build 2022-04-13 16:25:58 +01:00
alex ca4bffe5a1 compress 2022-04-13 16:20:34 +01:00
alex 0df67fa920 patternify zoom 2022-04-13 16:04:57 +01:00
alex ec3371c4ff Allow linger to accept non-fractions 2022-04-13 15:59:36 +01:00
alex dcc5f66691 Merge branch 'main' of github.com:tidalcycles/strudel into more-functions 2022-04-13 15:51:36 +01:00
alex ec10f349bd Add _zoom and linger 2022-04-13 15:51:23 +01:00
Alex McLean 3b9e854042 Merge pull request #56 from tidalcycles/more-functions
More functions
2022-04-13 09:04:29 +01:00
alex 9eba883978 move some utilities to utils.mjs 2022-04-13 09:03:31 +01:00
alex fb826bed70 sort classes to top of export list 2022-04-13 08:44:38 +01:00
alex 2797856f50 sort exports 2022-04-13 08:40:04 +01:00
alex ad69fa19b6 Prettify 2022-04-13 08:38:08 +01:00
Felix Roos 2060e86875 add queryArc + update readme 2022-04-13 00:25:53 +02:00
Felix Roos 87605ca25e Merge branch 'main' of https://github.com/tidalcycles/strudel 2022-04-13 00:15:39 +02:00
Felix Roos 3cd10bf4b3 package-lock 2022-04-13 00:15:36 +02:00
Felix Roos 6bdb0fee55 fix server import 2022-04-13 00:15:09 +02:00
Felix Roos 42e2531e8d readme additions 2022-04-13 00:09:04 +02:00
Felix Roos 171f845069 add setup script 2022-04-12 23:54:56 +02:00
Felix Roos f9b043dc7e remove osc-js folder + remove umd stuff 2022-04-12 23:54:13 +02:00
Felix Roos e069e66952 patch osc for client 2022-04-12 23:42:46 +02:00
Felix Roos c9d7a2f313 osc instructions 2022-04-12 22:58:13 +02:00
alex c69af72060 Merge branch 'main' of github.com:tidalcycles/strudel 2022-04-12 21:49:32 +01:00
alex 4111a12cbf hard temporary fork of osc-js 2022-04-12 21:48:48 +01:00
alex 57c1c4e016 more info 2022-04-12 21:48:29 +01:00
alex 324dcd7ef8 use local osc.js 2022-04-12 21:48:11 +01:00
Felix Roos c604f920a0 add root scripts 2022-04-12 20:14:48 +02:00
Felix Roos f8cbd67557 fix: disable highlighting 2022-04-12 20:14:08 +02:00
alex 3c31891774 Add range2 2022-04-12 17:53:40 +01:00
alex 0e5dc4f368 add range 2022-04-12 17:48:09 +01:00
alex e75d8048fe rename range > listRange and remove from exports 2022-04-12 16:54:27 +01:00
alex 7c10d171f6 patternify jux, add juxBy 2022-04-12 16:49:27 +01:00
Alex McLean 5aa01ce91c Merge pull request #27 from tidalcycles/osc
OSC and SuperDirt support
2022-04-12 16:04:04 +01:00
Alex McLean 86bb92d3be Merge pull request #50 from tidalcycles/chop
Implement `chop()`
2022-04-12 12:24:14 +01:00
alex e199564415 Patternify chop() 2022-04-12 12:22:04 +01:00
alex 51db0eaf40 Test and fix chop / squeezebind 2022-04-12 12:16:02 +01:00
alex 83c9200e7d Prettify 2022-04-12 12:01:19 +01:00
alex 529d448383 squeezeBind itself + tidying 2022-04-12 12:00:39 +01:00
alex 8ea59067ce rename inner <> outer bind and join 2022-04-12 11:55:56 +01:00
Alex McLean ac5c844f0b Merge pull request #48 from tidalcycles/squeezejoin
First run at squeezeBind, ref #32
2022-04-12 11:37:32 +01:00
alex b10e0c3a15 Add ply() 2022-04-12 11:31:20 +01:00
alex c2cc6b6dc8 Working squeezeBind 2022-04-12 11:22:53 +01:00
alex 357b0ee467 First run at squeezeBind, ref #32 2022-04-12 10:53:22 +01:00
Alex McLean ac409965d5 Merge pull request #44 from tidalcycles/fix-polymeter
Fix polymeter
2022-04-11 23:03:37 +01:00
alex c596fe33a9 Fix polymeter 2022-04-11 23:01:26 +01:00
Felix Roos 4dbe366978 just a little plumber tune 2022-04-11 23:25:13 +02:00
Felix Roos 7ca8c8d298 fix "every" example 2022-04-11 23:07:53 +02:00
Felix Roos cf7b2b2acf Merge pull request #40 from tidalcycles/compose
Compose
2022-04-11 22:39:43 +02:00
Felix Roos 81598aefce remove ramda dep + update lockfiles 2022-04-11 22:37:28 +02:00
Felix Roos 9effc166bb use own compose 2022-04-11 22:32:53 +02:00
Felix Roos de3c453763 add pipe + compose 2022-04-11 22:31:02 +02:00
Alex McLean 9da114d965 Merge branch 'main' into osc 2022-04-11 21:10:37 +01:00
Felix Roos 3f603eb84f Merge pull request #38 from bwagner/patch-2
Update tutorial.mdx
2022-04-11 21:49:34 +02:00
Bernhard Wagner 3f1df8faa6 Update tutorial.mdx 2022-04-11 18:08:23 +02:00
Felix Roos 2a3af80deb Merge pull request #37 from bwagner/patch-1
Update tutorial.mdx
2022-04-11 08:43:44 +02:00
Bernhard Wagner abfff8bcf1 Update tutorial.mdx
seperating -> separating
2022-04-11 08:34:11 +02:00
Felix Roos c6d7dabada Create CONTRIBUTING.md 2022-04-10 01:01:02 +02:00
Felix Roos 0c61bb0063 Update featurelist.md
tick some boxes
2022-04-09 22:10:27 +02:00
Felix Roos 80393bf765 Publish
- @strudel.cycles/core@0.0.3
 - @strudel.cycles/eval@0.0.3
 - @strudel.cycles/midi@0.0.4
 - @strudel.cycles/mini@0.0.4
 - @strudel.cycles/tonal@0.0.3
 - @strudel.cycles/tone@0.0.4
 - @strudel.cycles/xen@0.0.3
2022-04-05 23:44:40 +02:00
Felix Roos f43ecf1a85 fix readme instructions 2022-04-05 23:41:10 +02:00
Felix Roos a242c0654b dont create defaultSynth on start 2022-04-05 23:40:21 +02:00
Felix Roos c8e9fe519f osc timing now hopefully works everywhere 2022-04-02 22:41:04 +02:00
Felix Roos 0c726d835d fix keybindings on safari 2022-04-02 22:40:35 +02:00
Felix Roos 86b1e992a1 fix absolute time 2022-04-02 21:46:56 +02:00
alex c3b15fffa4 remove duplication 2022-04-02 18:48:30 +01:00
alex 7bbac0ed23 newline 2022-04-02 18:40:27 +01:00
alex 1fb1d15d4c add all the superdirt controls 2022-04-02 18:38:20 +01:00
alex 64ea9b7c8b controls (renamed from packages/osc/superdirt.mjs 2022-04-02 17:32:36 +01:00
Felix Roos 5a03386ad5 package-lock 2022-04-02 18:14:10 +02:00
alex acca363a4e move osc/superdirt to core/controls 2022-04-02 16:06:42 +01:00
alex e28a411408 Make param args a sequence, remove some redundancy 2022-04-02 16:03:50 +01:00
alex 89a7bd4e7d superdirt parameters 2022-04-01 23:17:52 +01:00
alex 46b2e82b43 formatting 2022-04-01 23:15:54 +01:00
alex 723b239fda formatting 2022-04-01 23:15:39 +01:00
Felix Roos bfb757b3b1 remove logs 2022-04-01 21:02:11 +02:00
Felix Roos 7fb9c3bf1e move core repl + add package 2022-04-01 20:54:54 +02:00
alex bf7c8cebc4 tidy 2022-04-01 15:38:06 +01:00
alex ac01a621be workaround for osc-js timestamp bug, reduce latency 2022-04-01 15:37:50 +01:00
Felix Roos 47a046ca1d begin webaudio package 2022-04-01 15:44:17 +02:00
Felix Roos 4ef70d2a15 Merge branch 'osc' of https://github.com/tidalcycles/strudel into osc 2022-03-29 22:17:46 +02:00
Felix Roos ee0d835cd8 add core examples 2022-03-29 22:16:36 +02:00
alex 330263028e it seems the custom handler wasn't being used 2022-03-29 17:04:00 +01:00
alex d0d19778a4 try sending osc in timestamped bundle with added latency 2022-03-29 17:03:30 +01:00
alex 0bfbd87bbf try to send osc as timestamped bundle 2022-03-29 16:39:52 +01:00
alex ba095d8e7f Stop failing on no tests 2022-03-29 15:36:56 +01:00
alex fb52227a92 prettify 2022-03-29 15:29:19 +01:00
alex 0b8919de61 send actual osc parameters 2022-03-29 08:12:34 +01:00
alex 7fca7e6bd9 attempt to send an OSC message 2022-03-28 22:10:20 +01:00
alex 69884c61b4 first (broken) attempt 2022-03-28 21:41:49 +01:00
Felix Roos ed6cf2ecf1 enlarge button 2022-03-28 21:24:44 +02:00
Felix Roos 0be0347f8b basic communication with superdirt 2022-03-28 21:22:24 +02:00
Alex McLean e376f25d92 Merge pull request #25 from tidalcycles/Add-chunk-and-chunkBack
Add chunk, chunkBack and iterBack
2022-03-28 16:27:21 +01:00
alex d2a715af0f Add chunk, chunkBack and iterBack 2022-03-28 14:51:13 +01:00
alex 3c230986e7 Expain style and community a little bit. 2022-03-28 13:54:38 +01:00
alex f529d09227 prettify strudel.mjs, plus add top level echo function 2022-03-28 13:37:13 +01:00
Alex McLean b7988f2158 Merge pull request #24 from tidalcycles/packages
packaging
2022-03-28 10:48:21 +01:00
Felix Roos 2a0d8c3f77 remove codesandbox specific line 2022-03-28 00:31:56 +02:00
Felix Roos 429fcaf05a Publish
- @strudel.cycles/midi@0.0.3
 - @strudel.cycles/mini@0.0.3
 - @strudel.cycles/tone@0.0.3
2022-03-28 00:04:23 +02:00
Felix Roos 4603d16162 add midi note 2022-03-27 23:53:31 +02:00
Felix Roos 6d4a7f96eb add tone example 2022-03-27 23:49:56 +02:00
Felix Roos 4c277afea9 add publishing info 2022-03-27 23:45:46 +02:00
91 changed files with 15339 additions and 4647 deletions
+2 -1
View File
@@ -26,4 +26,5 @@ node_modules/
.DS_Store
repl-parcel
mytunes.ts
doc
doc
.parcel-cache
+16
View File
@@ -0,0 +1,16 @@
{
"printWidth": 120,
"useTabs": false,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "always",
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"endOfLine": "lf"
}
+111
View File
@@ -0,0 +1,111 @@
# 🌀 Contributing to Strudel 🌀
Thanks for wanting to contribute!!! There are many ways you can add value to this project
## Communication Channels
To get in touch with the contributors, either
- open a [github discussion](https://github.com/tidalcycles/strudel/discussions) or
- [join the Tidal Discord Channel](https://discord.gg/remJ6gQA) and go to the #strudel channel
- Find related discussions on the [tidal club forum](https://club.tidalcycles.org/)
## Ask a Question
If you have any questions about strudel, make sure you've read the
[tutorial](https://strudel.tidalcycles.org/tutorial/) to find out if it answers your question.
If not, use one of the Communication Channels above!
Don't be afraid to ask! Your question might be of great value for other people too.
## Give Feedback
No matter if you've used the Strudel REPL or if you are using the strudel packages, we are happy to hear some feedback.
Use one of the Communication Channels listed above and drop us a line or two!
## Share Music
If you made some music with strudel, you can give back some love and share what you've done!
Your creation could also be part of the random selection in the REPL if you want.
Use one of the Communication Channels listed above.
## Improve the Tutorial
If you find some weak spots in the [tutorial](https://strudel.tidalcycles.org/),
you are welcome to improve them by editing [this file](https://github.com/tidalcycles/strudel/blob/main/repl/src/tutorial/tutorial.mdx).
This will even work without setting up a development environment, only a github account is required.
## Propose a Feature
If you want a specific feature that is not part of strudel yet, feel free to use one of the communication channels above.
Maybe you even want to help with the implementation of that feature!
## Report a Bug
If you've found a bug, or some behaviour that does not seem right, you are welcome to file an [issue](https://github.com/tidalcycles/strudel/issues).
Please check that it has not been reported before.
## Fix a Bug
To fix a bug that has been reported,
1. check that nobody else is already fixing it and respond to the issue to let people know you're on it
3. fork the repository
4. make sure you've setup the project (see below)
5. hopefully fix the bug
6. make sure the tests pass
7. send a pull request
## Write Tests
There are still many tests that have not been written yet! Reading and writing tests is a great opportunity to get familiar with the codebase.
You can find the tests in each package in the `test` folder. To run all tests, run `npm test` from the root folder.
## Project Setup
To get the project up and running for development, make sure you have installed:
- git
- node, preferably v16
then, do the following:
```sh
git clone https://github.com/tidalcycles/strudel.git && cd strudel
npm i # install at root to symlink packages
npx lerna bootstrap # install all dependencies in packages
cd repl && npm i # install repl dependencies
npm run start # start repl
```
Those commands might look slightly different for your OS.
Please report any problems you've had with the setup instructions!
## Code Style
To make sure the code changes only where it should, we are using prettier to unify the code style.
If you use VSCode, you can
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
2. open command palette and run "Format Document With..."
3. Choose "Configure Default Formatter..."
4. Select prettier
## Package Workflow
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
When you run `npm i` on the root folder, [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces) will symlink all packages
in the `node_modules` folder. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
which allows developing multiple packages at the same time
## Package Publishing
To publish all packages that have been changed since the last release, run:
```sh
npx lerna publish
```
## Have Fun
Remember to have fun, and that this project is driven by the passion of volunteers!
+34 -10
View File
@@ -4,18 +4,42 @@
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This is unstable software, please tread carefully.
Try it here: https://strudel.tidalcycles.org/
- Try it here: <https://strudel.tidalcycles.org/>
- Tutorial: <https://strudel.tidalcycles.org/tutorial/>
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
Tutorial: https://strudel.tidalcycles.org/tutorial/
## Running Locally
## Local development
Run the REPL locally:
After cloning the project, you can run the REPL locally:
```bash
npm install
npx lerna bootstrap
cd repl
npm install
npm run start
npm run setup
npm run repl
```
## Using Strudel In Your Project
There are multiple npm packages you can use to use strudel, or only parts of it, in your project:
- [`core`](./packages/core/): tidal pattern engine
- [`mini`](./packages/mini): mini notation parser + core binding
- [`eval`](./packages/eval): user code evaluator. syntax sugar + highlighting
- [`tone`](./packages/tone): bindings for Tone.js instruments and effects
- [`osc`](./packages/osc): bindings to communicate via OSC
- [`midi`](./packages/midi): webmidi bindings
- [`tonal`](./packages/tonal): tonal functions
- [`xen`](./packages/xen): microtonal / xenharmonic functions
Click on the package names to find out more about each one.
## Contributing
There are many ways to contribute to this project! See [contribution guide](./CONTRIBUTING.md).
## Community
There is a #strudel channel on the TidalCycles discord: <https://discord.com/invite/HGEdXmRkzT>
You can also ask questions and find related discussions on the tidal club forum: <https://club.tidalcycles.org/>
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
+5 -5
View File
@@ -1,16 +1,16 @@
{
"files": {
"main.css": "/static/css/main.0d689283.css",
"main.js": "/static/js/main.77e38ada.js",
"static/js/787.8f7ec9e0.chunk.js": "/static/js/787.8f7ec9e0.chunk.js",
"main.js": "/static/js/main.f7b8088f.js",
"static/js/787.1c52cb78.chunk.js": "/static/js/787.1c52cb78.chunk.js",
"static/media/logo.svg": "/static/media/logo.ac95051720b3dccfe511e0e02d8e1029.svg",
"index.html": "/index.html",
"main.0d689283.css.map": "/static/css/main.0d689283.css.map",
"main.77e38ada.js.map": "/static/js/main.77e38ada.js.map",
"787.8f7ec9e0.chunk.js.map": "/static/js/787.8f7ec9e0.chunk.js.map"
"main.f7b8088f.js.map": "/static/js/main.f7b8088f.js.map",
"787.1c52cb78.chunk.js.map": "/static/js/787.1c52cb78.chunk.js.map"
},
"entrypoints": [
"static/css/main.0d689283.css",
"static/js/main.77e38ada.js"
"static/js/main.f7b8088f.js"
]
}
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Strudel REPL"/><title>Strudel REPL</title><script defer="defer" src="/static/js/main.77e38ada.js"></script><link href="/static/css/main.0d689283.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Strudel REPL"/><title>Strudel REPL</title><script defer="defer" src="/static/js/main.f7b8088f.js"></script><link href="/static/css/main.0d689283.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
"use strict";(self.webpackChunk_strudel_cycles_repl=self.webpackChunk_strudel_cycles_repl||[]).push([[787],{787:function(e,t,n){n.r(t),n.d(t,{getCLS:function(){return y},getFCP:function(){return g},getFID:function(){return C},getLCP:function(){return P},getTTFB:function(){return D}});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,p=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=p(),d(),s((function(){setTimeout((function(){v=p(),d()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime<i.firstHiddenTime&&(r.value=e.startTime,r.entries.push(e),n(!0)))},o=window.performance&&performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint")[0],f=o?null:c("paint",a);(o||f)&&(n=m(e,r,t),o&&a(o),s((function(i){r=u("FCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,n(!0)}))}))})))},h=!1,T=-1,y=function(e,t){h||(g((function(e){T=e.value})),h=!0);var n,i=function(t){T>-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},p=c("layout-shift",v);p&&(n=m(i,r,t),f((function(){p.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r<a-w){var e={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+r};o.forEach((function(t){t(e)})),o=[]}},b=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),p=function(e){e.startTime<a.firstHiddenTime&&(v.value=e.processingStart-e.startTime,v.entries.push(e),n(!0))},d=c("first-input",p);n=m(e,v,t),d&&f((function(){d.takeRecords().map(p),d.disconnect()}),!0),d&&s((function(){var a;v=u("FID"),n=m(e,v,t),o=[],r=-1,i=null,F(addEventListener),a=p,o.push(a),S()}))},k={},P=function(e,t){var n,i=l(),r=u("LCP"),a=function(e){var t=e.startTime;t<i.firstHiddenTime&&(r.value=t,r.entries.push(e),n())},o=c("largest-contentful-paint",a);if(o){n=m(e,r,t);var v=function(){k[r.id]||(o.takeRecords().map(a),o.disconnect(),k[r.id]=!0,n(!0))};["keydown","click"].forEach((function(e){addEventListener(e,v,{once:!0,capture:!0})})),f(v,!0),s((function(i){r=u("LCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,k[r.id]=!0,n(!0)}))}))}))}},D=function(e){var t,n=u("TTFB");t=function(){try{var t=performance.getEntriesByType("navigation")[0]||function(){var e=performance.timing,t={entryType:"navigation",startTime:0};for(var n in e)"navigationStart"!==n&&"toJSON"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]);
//# sourceMappingURL=787.1c52cb78.chunk.js.map
File diff suppressed because one or more lines are too long
-2
View File
@@ -1,2 +0,0 @@
"use strict";(self.webpackChunk_strudel_repl=self.webpackChunk_strudel_repl||[]).push([[787],{787:function(e,t,n){n.r(t),n.d(t,{getCLS:function(){return y},getFCP:function(){return g},getFID:function(){return C},getLCP:function(){return P},getTTFB:function(){return D}});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,p=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=p(),d(),s((function(){setTimeout((function(){v=p(),d()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime<i.firstHiddenTime&&(r.value=e.startTime,r.entries.push(e),n(!0)))},o=window.performance&&performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint")[0],f=o?null:c("paint",a);(o||f)&&(n=m(e,r,t),o&&a(o),s((function(i){r=u("FCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,n(!0)}))}))})))},h=!1,T=-1,y=function(e,t){h||(g((function(e){T=e.value})),h=!0);var n,i=function(t){T>-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},p=c("layout-shift",v);p&&(n=m(i,r,t),f((function(){p.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r<a-w){var e={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+r};o.forEach((function(t){t(e)})),o=[]}},b=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),p=function(e){e.startTime<a.firstHiddenTime&&(v.value=e.processingStart-e.startTime,v.entries.push(e),n(!0))},d=c("first-input",p);n=m(e,v,t),d&&f((function(){d.takeRecords().map(p),d.disconnect()}),!0),d&&s((function(){var a;v=u("FID"),n=m(e,v,t),o=[],r=-1,i=null,F(addEventListener),a=p,o.push(a),S()}))},k={},P=function(e,t){var n,i=l(),r=u("LCP"),a=function(e){var t=e.startTime;t<i.firstHiddenTime&&(r.value=t,r.entries.push(e),n())},o=c("largest-contentful-paint",a);if(o){n=m(e,r,t);var v=function(){k[r.id]||(o.takeRecords().map(a),o.disconnect(),k[r.id]=!0,n(!0))};["keydown","click"].forEach((function(e){addEventListener(e,v,{once:!0,capture:!0})})),f(v,!0),s((function(i){r=u("LCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,k[r.id]=!0,n(!0)}))}))}))}},D=function(e){var t,n=u("TTFB");t=function(){try{var t=performance.getEntriesByType("navigation")[0]||function(){var e=performance.timing,t={entryType:"navigation",startTime:0};for(var n in e)"navigationStart"!==n&&"toJSON"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]);
//# sourceMappingURL=787.8f7ec9e0.chunk.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><link rel="icon" href="/tutorial/favicon.e3ab9dd9.ico"><link rel="stylesheet" type="text/css" href="/tutorial/index.999678aa.css"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content="Strudel REPL"><title>Strudel Tutorial</title></head><body> <div id="root"></div> <noscript>You need to enable JavaScript to run this app.</noscript> <script src="/tutorial/index.097d520a.js" defer></script> </body></html>
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><link rel="icon" href="/tutorial/favicon.e3ab9dd9.ico"><link rel="stylesheet" type="text/css" href="/tutorial/index.757e9f9d.css"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content="Strudel REPL"><title>Strudel Tutorial</title></head><body> <div id="root"></div> <noscript>You need to enable JavaScript to run this app.</noscript> <script src="/tutorial/index.36c08014.js" defer></script> </body></html>
+12 -9
View File
@@ -6,7 +6,7 @@
- [ ] setcps
- [ ] naming patterns? block based evaluation?
- [ ] once
- [ ] silence
- [x] silence
- [x] hush
- [ ] panic
@@ -30,22 +30,23 @@ https://tidalcycles.org/docs/patternlib/tour/concatenation
- [ ] overlay => like stack? "The overlay function is similar to cat" => wrong?
- [ ] `<>` operator (=== overlay)
- [x] stack
- [x] superimpose => strudel supports multiple args => is this layer?
- [ ] layer => is this like superimpose but with multiple args?
- [x] superimpose
- [x] layer
- [ ] steps ?
- [ ] iter, iter'
- [x] iter
- [x] iter' = iterBack
## Alteration
- [ ] range, rangex
- [ ] quantise
- [ ] ply
- [ ] stutter
- [x] stutter = echo
- [ ] stripe, slowstripe
- [ ] palindrome = every(2, rev)
- [ ] trunc
- [ ] linger
- [ ] chunk, chunk'
- [x] chunk, chunk'
- [ ] shuffle
- [ ] scramble
- [ ] rot
@@ -68,7 +69,8 @@ https://tidalcycles.org/docs/patternlib/tour/concatenation
- [ ] select, selectF
- [ ] pickF
- [ ] squeeze
- [ ] euclid, euclidInv, euclidFull
- [x] euclid, euclidLegato
- [ ] euclidInv, euclidFull
- [ ] ifp
## Time
@@ -83,7 +85,8 @@ https://tidalcycles.org/docs/patternlib/tour/concatenation
- [x] off
- [ ] rotL / rotR
- [x] rev
- [ ] jux / juxBy
- [x] jux
- [ ] juxBy
- [ ] swingBy / swing
- [ ] ghost
- [ ] inside / outside
@@ -114,7 +117,7 @@ https://tidalcycles.org/docs/patternlib/tour/concatenation
- [ ] chop
- [ ] striate / striateBy
- [ ] loopAt
- [ ] segment
- [x] segment
- [ ] discretise
## Randomness
+5271 -229
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -6,7 +6,11 @@
"main": "strudel.mjs",
"scripts": {
"test": "npm run test --workspaces --if-present",
"bootstrap": "lerna bootstrap"
"bootstrap": "lerna bootstrap",
"setup": "npm i && npm run bootstrap && cd repl && npm i",
"repl": "cd repl && npm run start",
"osc": "cd packages/osc && npm run server",
"build": "cd repl && npm run build"
},
"workspaces": [
"packages/*"
+6 -4
View File
@@ -11,18 +11,18 @@ npm i @strudel.cycles/core --save
## Example
```js
import { sequence, State, TimeSpan } from '@strudel.cycles/core';
import { sequence } from '@strudel.cycles/core';
const pattern = sequence('a', ['b', 'c']);
const events = pattern.query(new State(new TimeSpan(0, 2)));
const events = pattern.queryArc(0, 1);
const spans = events.map(
(event) => `${event.value}: ${event.whole.begin.toFraction()} - ${event.whole.end.toFraction()} `,
);
```
spans:
yields:
```log
a: 0 - 1/2
@@ -33,4 +33,6 @@ b: 3/2 - 7/4
c: 7/4 - 2
```
[play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-qmz6qr?file=/src/index.js).
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-qmz6qr?file=/src/index.js).
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/package-examples/packages/core/examples/canvas.html)
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/package-examples/packages/core/examples/metro.html)
+288
View File
@@ -0,0 +1,288 @@
import { Pattern, sequence } from './pattern.mjs';
const controls = {};
const generic_params = [
['s', 's', 'sound'],
//['s', 'toArg', 'for internal sound routing'],
// ["f", "from", "for internal sound routing"),
//['f', 'to', 'for internal sound routing'],
['f', 'accelerate', 'a pattern of numbers that speed up (or slow down) samples while they play.'],
['f', 'amp', 'like @gain@, but linear.'],
[
'f',
'attack',
'a pattern of numbers to specify the attack time (in seconds) of an envelope applied to each sample.',
],
['f', 'bandf', 'a pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
['f', 'bandq', 'a pattern of anumbers from 0 to 1. Sets the q-factor of the band-pass filter.'],
[
'f',
'begin',
'a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.',
],
['f', 'legato', 'controls the amount of overlap between two adjacent sounds'],
// ['f', 'clhatdecay', ''],
[
'f',
'crush',
'bit crushing, a pattern of numbers from 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).',
],
[
'f',
'coarse',
'fake-resampling, a pattern of numbers for lowering the sample rate, i.e. 1 for original 2 for half, 3 for a third and so on.',
],
['i', 'channel', 'choose the channel the pattern is sent to in superdirt'],
[
'i',
'cut',
'In the style of classic drum-machines, `cut` will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.',
],
['f', 'cutoff', 'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the low-pass filter.'],
// ['f', 'cutoffegint', ''],
['f', 'decay', ''],
['f', 'delay', 'a pattern of numbers from 0 to 1. Sets the level of the delay signal.'],
['f', 'delayfeedback', 'a pattern of numbers from 0 to 1. Sets the amount of delay feedback.'],
['f', 'delaytime', 'a pattern of numbers from 0 to 1. Sets the length of the delay.'],
['f', 'detune', ''],
['f', 'djf', 'DJ filter, below 0.5 is low pass filter, above is high pass filter.'],
[
'f',
'dry',
'when set to `1` will disable all reverb for this pattern. See `room` and `size` for more information about reverb.',
],
[
'f',
'end',
'the same as `begin`, but cuts the end off samples, shortening them; e.g. `0.75` to cut off the last quarter of each sample.',
],
[
'f',
'fadeTime',
"Used when using begin/end or chop/striate and friends, to change the fade out time of the 'grain' envelope.",
],
[
'f',
'fadeInTime',
'As with fadeTime, but controls the fade in time of the grain envelope. Not used if the grain begins at position 0 in the sample.',
],
['f', 'freq', ''],
[
'f',
'gain',
'a pattern of numbers that specify volume. Values less than 1 make the sound quieter. Values greater than 1 make the sound louder. For the linear equivalent, see @amp@.',
],
['f', 'gate', ''],
// ['f', 'hatgrain', ''],
[
'f',
'hcutoff',
'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the high-pass filter. Also has alias @hpf@',
],
[
'f',
'hold',
'a pattern of numbers to specify the hold time (in seconds) of an envelope applied to each sample. Only takes effect if `attack` and `release` are also specified.',
],
[
'f',
'hresonance',
'a pattern of numbers from 0 to 1. Applies the resonance of the high-pass filter. Has alias @hpq@',
],
// ['f', 'lagogo', ''],
// ['f', 'lclap', ''],
// ['f', 'lclaves', ''],
// ['f', 'lclhat', ''],
// ['f', 'lcrash', ''],
['f', 'leslie', ''],
['f', 'lrate', ''],
['f', 'lsize', ''],
// ['f', 'lfo', ''],
// ['f', 'lfocutoffint', ''],
// ['f', 'lfodelay', ''],
// ['f', 'lfoint', ''],
// ['f', 'lfopitchint', ''],
// ['f', 'lfoshape', ''],
// ['f', 'lfosync', ''],
// ['f', 'lhitom', ''],
// ['f', 'lkick', ''],
// ['f', 'llotom', ''],
[
'f',
'lock',
'A pattern of numbers. Specifies whether delaytime is calculated relative to cps. When set to 1, delaytime is a direct multiple of a cycle.',
],
['f', 'loop', 'loops the sample (from `begin` to `end`) the specified number of times.'],
// ['f', 'lophat', ''],
// ['f', 'lsnare', ''],
['f', 'n', 'The note or sample number to choose for a synth or sampleset'],
['f', 'note', 'The note or pitch to play a sound or synth with'],
['f', 'degree', ''],
['f', 'mtranspose', ''],
['f', 'ctranspose', ''],
['f', 'harmonic', ''],
['f', 'stepsPerOctave', ''],
['f', 'octaveR', ''],
[
'f',
'nudge',
'Nudges events into the future by the specified number of seconds. Negative numbers work up to a point as well (due to internal latency)',
],
['i', 'octave', ''],
['f', 'offset', ''],
// ['f', 'ophatdecay', ''],
[
'i',
'orbit',
'a pattern of numbers. An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share hardware output bus offset and global effects, e.g. reverb and delay. The maximum number of orbits is specified in the superdirt startup, numbers higher than maximum will wrap around.',
],
['f', 'overgain', ''],
['f', 'overshape', ''],
[
'f',
'pan',
'a pattern of numbers between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)',
],
[
'f',
'panspan',
'a pattern of numbers between -inf and inf, which controls how much multichannel output is fanned out (negative is backwards ordering)',
],
[
'f',
'pansplay',
'a pattern of numbers between 0.0 and 1.0, which controls the multichannel spread range (multichannel only)',
],
[
'f',
'panwidth',
'a pattern of numbers between 0.0 and inf, which controls how much each channel is distributed over neighbours (multichannel only)',
],
[
'f',
'panorient',
'a pattern of numbers between -1.0 and 1.0, which controls the relative position of the centre pan in a pair of adjacent speakers (multichannel only)',
],
// ['f', 'pitch1', ''],
// ['f', 'pitch2', ''],
// ['f', 'pitch3', ''],
// ['f', 'portamento', ''],
['f', 'rate', "used in SuperDirt softsynths as a control rate or 'speed'"],
[
'f',
'release',
'a pattern of numbers to specify the release time (in seconds) of an envelope applied to each sample.',
],
['f', 'resonance', 'a pattern of numbers from 0 to 1. Specifies the resonance of the low-pass filter.'],
['f', 'room', 'a pattern of numbers from 0 to 1. Sets the level of reverb.'],
// ['f', 'sagogo', ''],
// ['f', 'sclap', ''],
// ['f', 'sclaves', ''],
// ['f', 'scrash', ''],
['f', 'semitone', ''],
[
'f',
'shape',
'wave shaping distortion, a pattern of numbers from 0 for no distortion up to 1 for loads of distortion.',
],
[
'f',
'size',
'a pattern of numbers from 0 to 1. Sets the perceptual size (reverb time) of the `room` to be used in reverb.',
],
['f', 'slide', ''],
[
'f',
'speed',
'a pattern of numbers which changes the speed of sample playback, i.e. a cheap way of changing pitch. Negative values will play the sample backwards!',
],
['f', 'squiz', ''],
['f', 'stutterdepth', ''],
['f', 'stuttertime', ''],
['f', 'sustain', ''],
['f', 'timescale', ''],
['f', 'timescalewin', ''],
// ['f', 'tomdecay', ''],
[
's',
'unit',
'used in conjunction with `speed`, accepts values of "r" (rate, default behavior), "c" (cycles), or "s" (seconds). Using `unit "c"` means `speed` will be interpreted in units of cycles, e.g. `speed "1"` means samples will be stretched to fill a cycle. Using `unit "s"` means the playback speed will be adjusted so that the duration is the number of seconds specified by `speed`.',
],
['f', 'velocity', ''],
// ['f', 'vcfegint', ''],
// ['f', 'vcoegint', ''],
['f', 'voice', ''],
[
's',
'vowel',
'formant filter to make things sound like vowels, a pattern of either `a`, `e`, `i`, `o` or `u`. Use a rest (`~`) for no effect.',
],
['f', 'waveloss', ''],
['f', 'dur', ''],
// ['f', 'modwheel', ''],
['f', 'expression', ''],
['f', 'sustainpedal', ''],
['f', 'tremolodepth', "Tremolo Audio DSP effect | params are 'tremolorate' and 'tremolodepth'"],
['f', 'tremolorate', "Tremolo Audio DSP effect | params are 'tremolorate' and 'tremolodepth'"],
['f', 'phaserdepth', "Phaser Audio DSP effect | params are 'phaserrate' and 'phaserdepth'"],
['f', 'phaserrate', "Phaser Audio DSP effect | params are 'phaserrate' and 'phaserdepth'"],
['f', 'fshift', 'frequency shifter'],
['f', 'fshiftnote', 'frequency shifter'],
['f', 'fshiftphase', 'frequency shifter'],
['f', 'triode', 'tube distortion'],
['f', 'krush', 'shape/bass enhancer'],
['f', 'kcutoff', ''],
['f', 'octer', 'octaver effect'],
['f', 'octersub', 'octaver effect'],
['f', 'octersubsub', 'octaver effect'],
['f', 'ring', 'ring modulation'],
['f', 'ringf', 'ring modulation'],
['f', 'ringdf', 'ring modulation'],
['f', 'distort', 'noisy fuzzy distortion'],
['f', 'freeze', 'Spectral freeze'],
['f', 'xsdelay', ''],
['f', 'tsdelay', ''],
['f', 'real', 'Spectral conform'],
['f', 'imag', ''],
['f', 'enhance', 'Spectral enhance'],
['f', 'partials', ''],
['f', 'comb', 'Spectral comb'],
['f', 'smear', 'Spectral smear'],
['f', 'scram', 'Spectral scramble'],
['f', 'binshift', 'Spectral binshift'],
['f', 'hbrick', 'High pass sort of spectral filter'],
['f', 'lbrick', 'Low pass sort of spectral filter'],
['f', 'midichan', ''],
['f', 'control', ''],
['f', 'ccn', ''],
['f', 'ccv', ''],
['f', 'polyTouch', ''],
['f', 'midibend', ''],
['f', 'miditouch', ''],
['f', 'ctlNum', ''],
['f', 'frameRate', ''],
['f', 'frames', ''],
['f', 'hours', ''],
['s', 'midicmd', ''],
['f', 'minutes', ''],
['f', 'progNum', ''],
['f', 'seconds', ''],
['f', 'songPtr', ''],
['f', 'uid', ''],
['f', 'val', ''],
['f', 'cps', ''],
];
const _name = (name, ...pats) => sequence(...pats).withValue((x) => ({ [name]: x }));
const _unionise = (func) =>
function (...pats) {
return this.union(func(...pats));
};
generic_params.forEach(([type, name, description]) => {
controls[name] = (...pats) => _name(name, ...pats);
Pattern.prototype[name] = _unionise(controls[name]);
});
export default controls;
+1 -1
View File
@@ -1,4 +1,4 @@
import { Pattern, timeCat } from './strudel.mjs';
import { Pattern, timeCat } from './pattern.mjs';
import bjork from 'bjork';
import { rotate } from './util.mjs';
import Fraction from './fraction.mjs';
+26
View File
@@ -0,0 +1,26 @@
<input
type="text"
id="text"
value="cat('a', 'b')"
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
spellcheck="false"
/>
<div id="output"></div>
<script type="module">
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.0.2');
Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
const input = document.getElementById('text');
const getEvents = () => {
const code = document.getElementById('text').value;
const pattern = eval(code);
const events = pattern.firstCycle();
console.log(code, '->', events);
document.getElementById('output').innerHTML = events.map((e) => e.show()).join('<br/>');
};
getEvents();
input.addEventListener('input', () => getEvents());
</script>
<p>
This page shows how skypack can be used to import strudel core directly into a simple html page. <br />
No server, no bundler and no build setup is needed to run this!
</p>
+34
View File
@@ -0,0 +1,34 @@
<body style="margin: 0">
<input
type="text"
id="text"
value="cat('orange', 'indigo')"
style="width: 100%; font-size: 2em; background: black; color: white; outline: none; position: absolute; top: 0"
spellcheck="false"
/>
<canvas id="canvas"></canvas>
<script type="module">
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.0.2');
// this adds all strudel functions to the global scope, to be used by eval
Object.assign(window, strudel);
// setup elements
const input = document.getElementById('text');
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');
input.focus(); // autofocus
input.setSelectionRange(input.value.length, input.value.length); // move cursor to end
paint(input.value); // initial paint
input.addEventListener('input', (e) => paint(e.target.value)); // repaint on input
function paint(code) {
const pattern = eval(code); // run code
const events = pattern.firstCycle(); // query first cycle
events.forEach((event) => {
ctx.fillStyle = event.value;
ctx.fillRect(event.whole.begin * canvas.width, 0, event.duration * canvas.width, canvas.height);
});
}
</script>
</body>
+6 -1
View File
@@ -1,5 +1,5 @@
import Fraction from 'fraction.js';
import { TimeSpan } from './strudel.mjs';
import { TimeSpan } from './timespan.mjs';
// Returns the start of the cycle.
Fraction.prototype.sam = function () {
@@ -16,6 +16,11 @@ Fraction.prototype.wholeCycle = function () {
return new TimeSpan(this.sam(), this.nextSam());
};
// The position of a time value relative to the start of its cycle.
Fraction.prototype.cyclePos = function () {
return this.sub(this.sam());
};
Fraction.prototype.lt = function (other) {
return this.compare(other) < 0;
};
+90
View File
@@ -0,0 +1,90 @@
export class Hap {
/*
Event class, representing a value active during the timespan
'part'. This might be a fragment of an event, in which case the
timespan will be smaller than the 'whole' timespan, otherwise the
two timespans will be the same. The 'part' must never extend outside of the
'whole'. If the event represents a continuously changing value
then the whole will be returned as None, in which case the given
value will have been sampled from the point halfway between the
start and end of the 'part' timespan.
The context is to store a list of source code locations causing the event
*/
constructor(whole, part, value, context = {}, stateful = false) {
this.whole = whole;
this.part = part;
this.value = value;
this.context = context;
this.stateful = stateful;
if (stateful) {
console.assert(typeof this.value === 'function', 'Stateful values must be functions');
}
}
get duration() {
return this.whole.end.sub(this.whole.begin).valueOf();
}
wholeOrPart() {
return this.whole ? this.whole : this.part;
}
withSpan(func) {
// Returns a new event with the function f applies to the event timespan.
const whole = this.whole ? func(this.whole) : undefined;
return new Hap(whole, func(this.part), this.value, this.context);
}
withValue(func) {
// Returns a new event with the function f applies to the event value.
return new Hap(this.whole, this.part, func(this.value), this.context);
}
hasOnset() {
// Test whether the event contains the onset, i.e that
// the beginning of the part is the same as that of the whole timespan."""
return this.whole != undefined && this.whole.begin.equals(this.part.begin);
}
resolveState(state) {
if (this.stateful && this.hasOnset()) {
console.log('stateful');
const func = this.value;
const [newState, newValue] = func(state);
return [newState, new Hap(this.whole, this.part, newValue, this.context, false)];
}
return [state, this];
}
spanEquals(other) {
return (this.whole == undefined && other.whole == undefined) || this.whole.equals(other.whole);
}
equals(other) {
return (
this.spanEquals(other) &&
this.part.equals(other.part) &&
// TODO would == be better ??
this.value === other.value
);
}
show() {
return (
'(' + (this.whole == undefined ? '~' : this.whole.show()) + ', ' + this.part.show() + ', ' + this.value + ')'
);
}
combineContext(b) {
const a = this;
return { ...a.context, ...b.context, locations: (a.context.locations || []).concat(b.context.locations || []) };
}
setContext(context) {
return new Hap(this.whole, this.part, this.value, context);
}
}
export default Hap;
+11
View File
@@ -0,0 +1,11 @@
export * from './controls.mjs';
export * from './euclid.mjs';
import Fraction from './fraction.mjs';
export {Fraction};
export * from './hap.mjs';
export * from './pattern.mjs';
export * from './signal.mjs';
export * from './state.mjs';
export * from './timespan.mjs';
export * from './util.mjs';
// export * from './value.mjs';
+3 -18
View File
@@ -1,17 +1,16 @@
{
"name": "@strudel.cycles/core",
"version": "0.0.2",
"version": "0.0.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel.cycles/core",
"version": "0.0.1",
"version": "0.0.3",
"license": "GPL-3.0-or-later",
"dependencies": {
"bjork": "^0.0.1",
"fraction.js": "^4.2.0",
"ramda": "^0.28.0"
"fraction.js": "^4.2.0"
},
"devDependencies": {
"mocha": "^9.2.2"
@@ -733,15 +732,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/ramda": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz",
"integrity": "sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ramda"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -1485,11 +1475,6 @@
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true
},
"ramda": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz",
"integrity": "sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA=="
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+5 -5
View File
@@ -1,8 +1,8 @@
{
"name": "@strudel.cycles/core",
"version": "0.0.2",
"version": "0.0.5",
"description": "Port of Tidal Cycles to JavaScript",
"main": "strudel.mjs",
"main": "index.mjs",
"type": "module",
"scripts": {
"test": "mocha --colors"
@@ -26,10 +26,10 @@
"homepage": "https://strudel.tidalcycles.org",
"dependencies": {
"bjork": "^0.0.1",
"fraction.js": "^4.2.0",
"ramda": "^0.28.0"
"fraction.js": "^4.2.0"
},
"devDependencies": {
"mocha": "^9.2.2"
}
},
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2"
}
File diff suppressed because it is too large Load Diff
+169
View File
@@ -0,0 +1,169 @@
import { Hap } from './hap.mjs';
import { Pattern, fastcat, reify, silence, stack } from './pattern.mjs';
import Fraction from './fraction.mjs';
import { id } from './util.mjs';
export function steady(value) {
// A continuous value
return new Pattern((span) => Hap(undefined, span, value));
}
export const signal = (func) => {
const query = (state) => [new Hap(undefined, state.span, func(state.span.midpoint()))];
return new Pattern(query);
};
export const isaw = signal((t) => 1 - (t % 1));
export const isaw2 = isaw._toBipolar();
export const saw = signal((t) => t % 1);
export const saw2 = saw._toBipolar();
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
export const sine = sine2._fromBipolar();
export const cosine = sine._early(Fraction(1).div(4));
export const cosine2 = sine2._early(Fraction(1).div(4));
export const square = signal((t) => Math.floor((t * 2) % 2));
export const square2 = square._toBipolar();
export const tri = fastcat(isaw, saw);
export const tri2 = fastcat(isaw2, saw2);
export const time = signal(id);
// random signals
const xorwise = (x) => {
const a = (x << 13) ^ x;
const b = (a >> 17) ^ a;
return (b << 5) ^ b;
};
// stretch 300 cycles over the range of [0,2**29 == 536870912) then apply the xorshift algorithm
const _frac = (x) => x - Math.trunc(x);
const timeToIntSeed = (x) => xorwise(Math.trunc(_frac(x / 300) * 536870912));
const intSeedToRand = (x) => (x % 536870912) / 536870912;
const timeToRand = (x) => intSeedToRand(timeToIntSeed(x));
const timeToRandsPrime = (seed, n) => {
const result = [];
for (let i = 0; i < n; ++n) {
result.push(intSeedToRand(seed));
seed = xorwise(seed);
}
return result;
};
const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
export const rand2 = signal(timeToRand);
export const rand = rand2.fmap(Math.abs);
export const _brandBy = (p) => rand.fmap((x) => x < p);
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
export const brand = _brandBy(0.5);
export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
export const chooseWith = (pat, xs) => {
if (xs.length == 0) {
return silence;
}
return pat.range(0, xs.length).fmap((i) => xs[Math.floor(i)]);
};
export const choose = (...xs) => chooseWith(rand, xs);
export const perlinWith = (pat) => {
const pata = pat.fmap(Math.floor);
const patb = pat.fmap((t) => Math.floor(t) + 1);
const smootherStep = (x) => 6.0 * x ** 5 - 15.0 * x ** 4 + 10.0 * x ** 3;
const interp = (x) => (a) => (b) => a + smootherStep(x) * (b - a);
return pat.sub(pata).fmap(interp).appBoth(pata.fmap(timeToRand)).appBoth(patb.fmap(timeToRand));
};
export const perlin = perlinWith(time);
Pattern.prototype._degradeByWith = function (withPat, x) {
return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x));
};
Pattern.prototype._degradeBy = function (x) {
return this._degradeByWith(rand, x);
};
Pattern.prototype.degrade = function () {
return this._degradeBy(0.5);
};
Pattern.prototype._undegradeBy = function (x) {
return this._degradeByWith(
rand.fmap((r) => 1 - r),
x,
);
};
Pattern.prototype.undegrade = function () {
return this._undegradeBy(0.5);
};
Pattern.prototype._sometimesBy = function (x, func) {
return stack(this._degradeBy(x), func(this._undegradeBy(1 - x)));
};
Pattern.prototype.sometimesBy = function (patx, func) {
const pat = this;
return reify(patx)
.fmap((x) => pat._sometimesBy(x, func))
.innerJoin();
};
Pattern.prototype._sometimesByPre = function (x, func) {
return stack(this._degradeBy(x), func(this).undegradeBy(1 - x));
};
Pattern.prototype.sometimesByPre = function (patx, func) {
const pat = this;
return reify(patx)
.fmap((x) => pat._sometimesByPre(x, func))
.innerJoin();
};
Pattern.prototype.sometimes = function (func) {
return this._sometimesBy(0.5, func);
};
Pattern.prototype.sometimesPre = function (func) {
return this._sometimesByPre(0.5, func);
};
Pattern.prototype.often = function (func) {
return this.sometimesBy(0.75, func);
};
Pattern.prototype.rarely = function (func) {
return this.sometimesBy(0.25, func);
};
Pattern.prototype.almostNever = function (func) {
return this.sometimesBy(0.1, func);
};
Pattern.prototype.almostAlways = function (func) {
return this.sometimesBy(0.9, func);
};
Pattern.prototype.never = function (func) {
return this;
};
Pattern.prototype.always = function (func) {
return func(this);
};
Pattern.prototype.patternified.push('degradeBy', 'undegradeBy');
+33
View File
@@ -0,0 +1,33 @@
import { Pattern, patternify2 } from './index.mjs';
const synth = window?.speechSynthesis;
let allVoices = synth?.getVoices();
// console.log('voices', allVoices);
function speak(words, lang, voice) {
synth.cancel();
const utterance = new SpeechSynthesisUtterance(words);
utterance.lang = lang;
allVoices = synth.getVoices();
const voices = allVoices.filter((v) => v.lang.includes(lang));
if (typeof voice === 'number') {
utterance.voice = voices[voice % voices.length];
} else if (typeof voice === 'string') {
utterance.voice = voices.find((voice) => voice.name === voice);
}
// console.log(utterance.voice?.name, utterance.voice?.lang);
speechSynthesis.speak(utterance);
}
Pattern.prototype._speak = function (lang, voice) {
return this._withEvent((event) => {
const onTrigger = (time, event) => {
speak(event.value, lang, voice);
};
return event.setContext({ ...event.context, onTrigger });
});
};
Pattern.prototype.speak = function (lang, voice) {
return patternify2(Pattern.prototype._speak)(reify(lang), reify(voice), this);
};
+22
View File
@@ -0,0 +1,22 @@
export class State {
constructor(span, controls = {}) {
this.span = span;
this.controls = controls;
}
// Returns new State with different span
setSpan(span) {
return new State(span, this.controls);
}
withSpan(func) {
return this.setSpan(func(this.span));
}
// Returns new State with different controls
setControls(controls) {
return new State(this.span, controls);
}
}
export default State;
File diff suppressed because it is too large Load Diff
+530 -338
View File
@@ -1,6 +1,6 @@
import Fraction from 'fraction.js'
import Fraction from 'fraction.js';
import { strict as assert } from 'assert';
import { deepStrictEqual, strict as assert } from 'assert';
import {
TimeSpan,
@@ -13,6 +13,8 @@ import {
slowcat,
cat,
sequence,
polymeter,
polymeterSteps,
polyrhythm,
silence,
fast,
@@ -31,194 +33,210 @@ import {
square2,
tri,
tri2,
} from '../strudel.mjs';
id,
ply,
} from '../index.mjs';
//import { Time } from 'tone';
import pkg from 'tone';
const { Time } = pkg;
const st = (begin, end) => new State(ts(begin, end))
const st = (begin, end) => new State(ts(begin, end));
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
const hap = (whole, part, value, context={}) => new Hap(whole, part, value, context)
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
const third = Fraction(1,3)
const twothirds = Fraction(2,3)
const third = Fraction(1, 3);
const twothirds = Fraction(2, 3);
describe('TimeSpan', function() {
describe('equals()', function() {
it('Should be equal to the same value', function() {
assert.equal((new TimeSpan(0,4)).equals(new TimeSpan(0,4)), true);
describe('TimeSpan', function () {
describe('equals()', function () {
it('Should be equal to the same value', function () {
assert.equal(new TimeSpan(0, 4).equals(new TimeSpan(0, 4)), true);
});
});
describe('splitCycles', function () {
it('Should split two cycles into two', function () {
assert.equal(new TimeSpan(Fraction(0), Fraction(2)).spanCycles.length, 2);
});
});
describe('splitCycles', function() {
it('Should split two cycles into two', function() {
assert.equal(new TimeSpan(Fraction(0),Fraction(2)).spanCycles.length, 2)
})
})
describe('intersection_e', function () {
var a = new TimeSpan(Fraction(0), Fraction(2))
var b = new TimeSpan(Fraction(1), Fraction(3))
var c = new TimeSpan(Fraction(1), Fraction(2))
var d = new TimeSpan(Fraction(1), Fraction(2))
it('Should create an intersection', function () {
assert.equal(a.intersection_e(b).equals(c), true)
})
})
var a = new TimeSpan(Fraction(0), Fraction(2));
var b = new TimeSpan(Fraction(1), Fraction(3));
var c = new TimeSpan(Fraction(1), Fraction(2));
var d = new TimeSpan(Fraction(1), Fraction(2));
it('Should create an intersection', function () {
assert.equal(a.intersection_e(b).equals(c), true);
});
});
});
describe('Hap', function() {
describe('hasOnset()', function() {
it('True if part includes onset from whole', function() {
assert.equal(new Hap(new TimeSpan(0,1), new TimeSpan(0,1), "thing").hasOnset(), true);
});
describe('Hap', function () {
describe('hasOnset()', function () {
it('True if part includes onset from whole', function () {
assert.equal(new Hap(new TimeSpan(0, 1), new TimeSpan(0, 1), 'thing').hasOnset(), true);
});
var a = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), "a")
var b = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), "b")
var c = new Hap(new TimeSpan(Fraction(0), Fraction(0.25)), new TimeSpan(Fraction(0), Fraction(0.5)), "c")
var d = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), "d")
var e = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), "e")
describe('spanEquals', function() {
it('True if two haps have the same whole and part', function() {
assert.equal(a.spanEquals(b), true)
})
it('False if two haps don\'t the same whole and part', function() {
assert.equal(a.spanEquals(c), false)
})
it('True if two haps have the same part and undefined wholes', function() {
assert.equal(d.spanEquals(e), true)
})
})
describe('resolveState()', () => {
it('Can increment some state', function() {
const stateful_value = state => {
const newValue = state["incrementme"]
// TODO Does the state *need* duplicating here?
const newState = {...state}
newState["incrementme"]++
return [newState, newValue]
}
const state = {incrementme: 10}
const ev1 = new Hap(ts(0,1), ts(0,1), stateful_value, {}, true)
const [state2, ev2] = ev1.resolveState(state)
const [state3, ev3] = ev1.resolveState(state2)
assert.deepStrictEqual(
ev3, new Hap(ts(0,1),ts(0,1),11,{},false)
)
assert.deepStrictEqual(
state3, {incrementme: 12}
)
})
})
});
describe('Pattern', function() {
var a = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), 'a');
var b = new Hap(new TimeSpan(Fraction(0), Fraction(0.5)), new TimeSpan(Fraction(0), Fraction(0.5)), 'b');
var c = new Hap(new TimeSpan(Fraction(0), Fraction(0.25)), new TimeSpan(Fraction(0), Fraction(0.5)), 'c');
var d = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), 'd');
var e = new Hap(undefined, new TimeSpan(Fraction(0), Fraction(0.5)), 'e');
describe('spanEquals', function () {
it('True if two haps have the same whole and part', function () {
assert.equal(a.spanEquals(b), true);
});
it("False if two haps don't the same whole and part", function () {
assert.equal(a.spanEquals(c), false);
});
it('True if two haps have the same part and undefined wholes', function () {
assert.equal(d.spanEquals(e), true);
});
});
describe('resolveState()', () => {
it('Can increment some state', function () {
const stateful_value = (state) => {
const newValue = state['incrementme'];
// TODO Does the state *need* duplicating here?
const newState = { ...state };
newState['incrementme']++;
return [newState, newValue];
};
const state = { incrementme: 10 };
const ev1 = new Hap(ts(0, 1), ts(0, 1), stateful_value, {}, true);
const [state2, ev2] = ev1.resolveState(state);
const [state3, ev3] = ev1.resolveState(state2);
assert.deepStrictEqual(ev3, new Hap(ts(0, 1), ts(0, 1), 11, {}, false));
assert.deepStrictEqual(state3, { incrementme: 12 });
});
});
});
describe('Pattern', function () {
describe('pure', function () {
it('Can make a pattern', function() {
assert.equal(pure("hello").query(st(0.5, 2.5)).length, 3)
})
})
it('Can make a pattern', function () {
assert.equal(pure('hello').query(st(0.5, 2.5)).length, 3);
});
});
describe('fmap()', function () {
it('Can add things', function () {
assert.equal(pure(3).fmap(x => x + 4).firstCycle()[0].value, 7)
})
})
assert.equal(
pure(3)
.fmap((x) => x + 4)
.firstCycle()[0].value,
7,
);
});
});
describe('add()', function () {
it('Can add things', function() {
assert.equal(pure(3).add(pure(4)).query(st(0,1))[0].value, 7)
})
})
it('Can add things', function () {
assert.equal(pure(3).add(pure(4)).query(st(0, 1))[0].value, 7);
});
});
describe('sub()', function () {
it('Can subtract things', function() {
assert.equal(pure(3).sub(pure(4)).query(st(0,1))[0].value, -1)
})
})
it('Can subtract things', function () {
assert.equal(pure(3).sub(pure(4)).query(st(0, 1))[0].value, -1);
});
});
describe('mul()', function () {
it('Can multiply things', function() {
assert.equal(pure(3).mul(pure(2)).firstCycle()[0].value, 6)
})
})
it('Can multiply things', function () {
assert.equal(pure(3).mul(pure(2)).firstCycle()[0].value, 6);
});
});
describe('div()', function () {
it('Can divide things', function() {
assert.equal(pure(3).div(pure(2)).firstCycle()[0].value, 1.5)
})
})
it('Can divide things', function () {
assert.equal(pure(3).div(pure(2)).firstCycle()[0].value, 1.5);
});
});
describe('union()', function () {
it('Can union things', function () {
assert.deepStrictEqual(pure({a: 4, b: 6}).union(pure({c: 7})).firstCycle()[0].value, {a: 4, b: 6, c: 7})
})
})
assert.deepStrictEqual(
pure({ a: 4, b: 6 })
.union(pure({ c: 7 }))
.firstCycle()[0].value,
{ a: 4, b: 6, c: 7 },
);
});
});
describe('stack()', function () {
it('Can stack things', function () {
assert.deepStrictEqual(stack(pure("a"), pure("b"), pure("c")).firstCycle().map(h => h.value), ["a", "b", "c"])
})
})
assert.deepStrictEqual(
stack(pure('a'), pure('b'), pure('c'))
.firstCycle()
.map((h) => h.value),
['a', 'b', 'c'],
);
});
});
describe('_fast()', function () {
it('Makes things faster', function () {
assert.equal(pure("a")._fast(2).firstCycle().length, 2)
})
})
assert.equal(pure('a')._fast(2).firstCycle().length, 2);
});
});
describe('_fastGap()', function () {
it('Makes things faster, with a gap', function () {
assert.deepStrictEqual(
sequence("a", "b", "c")._fastGap(2).firstCycle(),
sequence(["a","b","c"], silence).firstCycle()
)
sequence('a', 'b', 'c')._fastGap(2).firstCycle(),
sequence(['a', 'b', 'c'], silence).firstCycle(),
);
assert.deepStrictEqual(
sequence("a", "b", "c")._fastGap(3).firstCycle(),
sequence(["a","b","c"], silence, silence).firstCycle()
)
})
sequence('a', 'b', 'c')._fastGap(3).firstCycle(),
sequence(['a', 'b', 'c'], silence, silence).firstCycle(),
);
});
it('Makes things faster, with a gap, when speeded up further', function () {
assert.deepStrictEqual(
sequence("a", "b", "c")._fastGap(2).fast(2).firstCycle(),
sequence(["a","b","c"], silence, ["a","b","c"], silence).firstCycle()
)
})
})
sequence('a', 'b', 'c')._fastGap(2).fast(2).firstCycle(),
sequence(['a', 'b', 'c'], silence, ['a', 'b', 'c'], silence).firstCycle(),
);
});
});
describe('_compressSpan()', function () {
it('Can squash cycles of a pattern into a given timespan', function () {
assert.deepStrictEqual(
pure("a")._compressSpan(new TimeSpan(0.25, 0.5)).firstCycle(),
sequence(silence, "a", silence, silence).firstCycle()
)
})
})
pure('a')._compressSpan(new TimeSpan(0.25, 0.5)).firstCycle(),
sequence(silence, 'a', silence, silence).firstCycle(),
);
});
});
describe('fast()', function () {
it('Makes things faster', function () {
assert.equal(pure("a").fast(2).firstCycle().length, 2)
})
assert.equal(pure('a').fast(2).firstCycle().length, 2);
});
it('Makes things faster, with a pattern of factors', function () {
assert.equal(pure("a").fast(sequence(1,4)).firstCycle().length, 3)
assert.equal(pure('a').fast(sequence(1, 4)).firstCycle().length, 3);
// .fast(sequence(1,silence) is a quick hack to cut an event in two..
assert.deepStrictEqual(pure("a").fast(sequence(1,4)).firstCycle(), stack(pure("a").fast(sequence(1,silence)), sequence(silence, ["a","a"])).firstCycle())
})
assert.deepStrictEqual(
pure('a').fast(sequence(1, 4)).firstCycle(),
stack(pure('a').fast(sequence(1, silence)), sequence(silence, ['a', 'a'])).firstCycle(),
);
});
it('defaults to accepting sequences', function () {
assert.deepStrictEqual(
sequence(1,2,3).fast(sequence(1.5,2)).firstCycle(),
sequence(1,2,3).fast(1.5,2).firstCycle()
)
})
it("works as a static function", function () {
sequence(1, 2, 3).fast(sequence(1.5, 2)).firstCycle(),
sequence(1, 2, 3).fast(1.5, 2).firstCycle(),
);
});
it('works as a static function', function () {
assert.deepStrictEqual(
sequence(1,2,3).fast(1,2).firstCycle(),
fast(sequence(1,2), sequence(1,2,3)).firstCycle()
)
})
it("works as a curried static function", function () {
sequence(1, 2, 3).fast(1, 2).firstCycle(),
fast(sequence(1, 2), sequence(1, 2, 3)).firstCycle(),
);
});
it('works as a curried static function', function () {
assert.deepStrictEqual(
sequence(1,2,3).fast(1,2).firstCycle(),
fast(sequence(1,2))(sequence(1,2,3)).firstCycle()
)
})
})
sequence(1, 2, 3).fast(1, 2).firstCycle(),
fast(sequence(1, 2))(sequence(1, 2, 3)).firstCycle(),
);
});
});
describe('_slow()', function () {
it('Makes things slower', function () {
assert.deepStrictEqual(pure("a")._slow(2).firstCycle()[0], new Hap(new TimeSpan(Fraction(0),Fraction(2)), new TimeSpan(Fraction(0), Fraction(1)), "a"))
assert.deepStrictEqual(
pure('a')._slow(2).firstCycle()[0],
new Hap(new TimeSpan(Fraction(0), Fraction(2)), new TimeSpan(Fraction(0), Fraction(1)), 'a'),
);
const pat = sequence(pure('c3'), pure('eb3')._slow(2)); // => try mini('c3 eb3/2') in repl
assert.deepStrictEqual(
pat.query(st(0,1))[1],
hap(ts(0.5,1.5), ts(1/2,1), "eb3")
)
assert.deepStrictEqual(pat.query(st(0, 1))[1], hap(ts(0.5, 1.5), ts(1 / 2, 1), 'eb3'));
// the following test fails
/* assert.deepStrictEqual(
pat.query(ts(1,2))[1], undefined
@@ -228,248 +246,422 @@ describe('Pattern', function() {
// notable examples:
// mini('[c3 g3]/2 eb3') always plays [c3 eb3]
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
})
})
});
});
describe('_filterValues()', function () {
it('Filters true', function () {
assert.equal(pure(true)._filterValues(x => x).firstCycle().length, 1)
})
})
assert.equal(
pure(true)
._filterValues((x) => x)
.firstCycle().length,
1,
);
});
});
describe('when()', function () {
it('Always faster', function () {
assert.equal(pure("a").when(pure(true), x => x._fast(2)).firstCycle().length, 2)
})
assert.equal(
pure('a')
.when(pure(true), (x) => x._fast(2))
.firstCycle().length,
2,
);
});
it('Never faster', function () {
assert.equal(pure("a").when(pure(false), x => x._fast(2)).firstCycle().length, 1)
})
assert.equal(
pure('a')
.when(pure(false), (x) => x._fast(2))
.firstCycle().length,
1,
);
});
it('Can alternate', function () {
assert.deepStrictEqual(
pure(10).when(slowcat(true,false),add(3)).fast(4)._sortEventsByPart().firstCycle(),
fastcat(13,10,13,10).firstCycle()
)
})
})
pure(10).when(slowcat(true, false), add(3)).fast(4)._sortEventsByPart().firstCycle(),
fastcat(13, 10, 13, 10).firstCycle(),
);
});
});
describe('fastcat()', function () {
it('Can concatenate two things', function () {
assert.deepStrictEqual(fastcat(pure("a"), pure("b")).firstCycle().map(x => x.value), ["a", "b"])
})
})
assert.deepStrictEqual(
fastcat(pure('a'), pure('b'))
.firstCycle()
.map((x) => x.value),
['a', 'b'],
);
});
});
describe('slowcat()', function () {
it('Can concatenate things slowly', function () {
assert.deepStrictEqual(slowcat("a", "b").firstCycle().map(x => x.value), ["a"])
assert.deepStrictEqual(slowcat("a", "b")._early(1).firstCycle().map(x => x.value), ["b"])
assert.deepStrictEqual(slowcat("a", slowcat("b", "c"))._early(1).firstCycle().map(x => x.value), ["b"])
assert.deepStrictEqual(slowcat("a", slowcat("b", "c"))._early(3).firstCycle().map(x => x.value), ["c"])
})
})
assert.deepStrictEqual(
slowcat('a', 'b')
.firstCycle()
.map((x) => x.value),
['a'],
);
assert.deepStrictEqual(
slowcat('a', 'b')
._early(1)
.firstCycle()
.map((x) => x.value),
['b'],
);
assert.deepStrictEqual(
slowcat('a', slowcat('b', 'c'))
._early(1)
.firstCycle()
.map((x) => x.value),
['b'],
);
assert.deepStrictEqual(
slowcat('a', slowcat('b', 'c'))
._early(3)
.firstCycle()
.map((x) => x.value),
['c'],
);
});
});
describe('rev()', function () {
it('Can reverse things', function () {
assert.deepStrictEqual(fastcat("a","b","c").rev().firstCycle().sort((a,b) => a.part.begin.sub(b.part.begin)).map(a => a.value), ["c", "b","a"])
})
})
assert.deepStrictEqual(
fastcat('a', 'b', 'c')
.rev()
.firstCycle()
.sort((a, b) => a.part.begin.sub(b.part.begin))
.map((a) => a.value),
['c', 'b', 'a'],
);
});
});
describe('sequence()', () => {
it('Can work like fastcat', () => {
assert.deepStrictEqual(sequence(1,2,3).firstCycle(), fastcat(1,2,3).firstCycle())
})
})
assert.deepStrictEqual(sequence(1, 2, 3).firstCycle(), fastcat(1, 2, 3).firstCycle());
});
});
describe('polyrhythm()', () => {
it('Can layer up cycles', () => {
assert.deepStrictEqual(
polyrhythm(["a","b"],["c"]).firstCycle(),
stack(fastcat(pure("a"),pure("b")),pure("c")).firstCycle()
)
})
})
polyrhythm(['a', 'b'], ['c']).firstCycle(),
stack(fastcat(pure('a'), pure('b')), pure('c')).firstCycle(),
);
});
});
describe('polymeter()', () => {
it('Can layer up cycles, stepwise', () => {
assert.deepStrictEqual(
polymeterSteps(3, ['d', 'e']).firstCycle(),
fastcat(pure('d'), pure('e'), pure('d')).firstCycle(),
);
assert.deepStrictEqual(
polymeter(['a', 'b', 'c'], ['d', 'e']).fast(2).firstCycle(),
stack(sequence('a', 'b', 'c', 'a', 'b', 'c'), sequence('d', 'e', 'd', 'e', 'd', 'e')).firstCycle(),
);
});
});
describe('every()', () => {
it('Can apply a function every 3rd time', () => {
assert.deepStrictEqual(
pure("a").every(3, x => x._fast(2))._fast(3).firstCycle(),
sequence(sequence("a", "a"), "a", "a").firstCycle()
)
})
it("works with currying", () => {
pure('a')
.every(3, (x) => x._fast(2))
._fast(3)
.firstCycle(),
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
);
});
it('works with currying', () => {
assert.deepStrictEqual(
pure("a").every(3, fast(2))._fast(3).firstCycle(),
sequence(sequence("a", "a"), "a", "a").firstCycle()
)
pure('a').every(3, fast(2))._fast(3).firstCycle(),
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
);
assert.deepStrictEqual(
sequence(3,4,5).every(3, add(3)).fast(5).firstCycle(),
sequence(6,7,8,3,4,5,3,4,5,6,7,8,3,4,5).firstCycle()
)
sequence(3, 4, 5).every(3, add(3)).fast(5).firstCycle(),
sequence(6, 7, 8, 3, 4, 5, 3, 4, 5, 6, 7, 8, 3, 4, 5).firstCycle(),
);
assert.deepStrictEqual(
sequence(3,4,5).every(2, sub(1)).fast(5).firstCycle(),
sequence(2,3,4,3,4,5,2,3,4,3,4,5,2,3,4).firstCycle()
)
sequence(3, 4, 5).every(2, sub(1)).fast(5).firstCycle(),
sequence(2, 3, 4, 3, 4, 5, 2, 3, 4, 3, 4, 5, 2, 3, 4).firstCycle(),
);
assert.deepStrictEqual(
sequence(3,4,5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle(),
sequence(5,6,7,3,4,5).firstCycle()
)
})
})
describe('timeCat()', function() {
it('Can concatenate patterns with different relative durations', function() {
sequence(3, 4, 5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle(),
sequence(5, 6, 7, 3, 4, 5).firstCycle(),
);
});
});
describe('timeCat()', function () {
it('Can concatenate patterns with different relative durations', function () {
assert.deepStrictEqual(
sequence("a", ["a", "a"]).firstCycle(),
timeCat([1,"a"], [0.5, "a"], [0.5, "a"]).firstCycle()
)
})
})
describe('struct()', function() {
it('Can restructure a pattern', function() {
sequence('a', ['a', 'a']).firstCycle(),
timeCat([1, 'a'], [0.5, 'a'], [0.5, 'a']).firstCycle(),
);
});
});
describe('struct()', function () {
it('Can restructure a pattern', function () {
assert.deepStrictEqual(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle(), [
hap(ts(0, third), ts(0, third), 'a'),
hap(ts(third, twothirds), ts(third, 0.5), 'a'),
hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
hap(ts(twothirds, 1), ts(twothirds, 1), 'b'),
]);
assert.deepStrictEqual(
sequence("a", "b").struct(sequence(true, true, true)).firstCycle(),
[hap(ts(0,third), ts(0,third), "a"),
hap(ts(third, twothirds), ts(third, 0.5), "a"),
hap(ts(third, twothirds), ts(0.5, twothirds), "b"),
hap(ts(twothirds, 1), ts(twothirds, 1), "b")
]
)
pure('a')
.struct(sequence(true, [true, false], true))
.firstCycle(),
sequence('a', ['a', silence], 'a').firstCycle(),
);
assert.deepStrictEqual(
pure("a").struct(sequence(true, [true,false], true)).firstCycle(),
sequence("a", ["a", silence], "a").firstCycle(),
)
pure('a')
.struct(sequence(true, [true, false], true).invert())
.firstCycle(),
sequence(silence, [silence, 'a'], silence).firstCycle(),
);
assert.deepStrictEqual(
pure("a").struct(sequence(true, [true,false], true).invert()).firstCycle(),
sequence(silence, [silence, "a"], silence).firstCycle(),
)
pure('a')
.struct(sequence(true, [true, silence], true))
.firstCycle(),
sequence('a', ['a', silence], 'a').firstCycle(),
);
});
});
describe('mask()', function () {
it('Can fragment a pattern', function () {
assert.deepStrictEqual(sequence('a', 'b').mask(sequence(true, true, true)).firstCycle(), [
hap(ts(0, 0.5), ts(0, third), 'a'),
hap(ts(0, 0.5), ts(third, 0.5), 'a'),
hap(ts(0.5, 1), ts(0.5, twothirds), 'b'),
hap(ts(0.5, 1), ts(twothirds, 1), 'b'),
]);
});
it('Can mask off parts of a pattern', function () {
assert.deepStrictEqual(
pure("a").struct(sequence(true, [true,silence], true)).firstCycle(),
sequence("a", ["a", silence], "a").firstCycle(),
)
})
})
describe('mask()', function() {
it('Can fragment a pattern', function() {
assert.deepStrictEqual(
sequence("a", "b").mask(sequence(true, true, true)).firstCycle(),
[hap(ts(0, 0.5), ts(0,third), "a"),
hap(ts(0, 0.5), ts(third, 0.5), "a"),
hap(ts(0.5, 1), ts(0.5, twothirds), "b"),
hap(ts(0.5, 1), ts(twothirds, 1), "b")
]
)
})
it('Can mask off parts of a pattern', function() {
assert.deepStrictEqual(
sequence(["a", "b"], "c").mask(sequence(true, false)).firstCycle(),
sequence(["a","b"], silence).firstCycle()
)
assert.deepStrictEqual(
sequence("a").mask(sequence(true, false)).firstCycle(),
[hap(ts(0,1),ts(0,0.5), "a")]
)
})
})
describe('invert()', function() {
it('Can invert a binary pattern', function() {
sequence(['a', 'b'], 'c').mask(sequence(true, false)).firstCycle(),
sequence(['a', 'b'], silence).firstCycle(),
);
assert.deepStrictEqual(sequence('a').mask(sequence(true, false)).firstCycle(), [hap(ts(0, 1), ts(0, 0.5), 'a')]);
});
});
describe('invert()', function () {
it('Can invert a binary pattern', function () {
assert.deepStrictEqual(
sequence(true, false, [true, false]).invert().firstCycle(),
sequence(false, true, [false, true]).firstCycle()
)
})
})
describe('signal()', function() {
it('Can make saw/saw2', function() {
sequence(false, true, [false, true]).firstCycle(),
);
});
});
describe('signal()', function () {
it('Can make saw/saw2', function () {
assert.deepStrictEqual(
saw.struct(true,true,true,true).firstCycle(),
sequence(1/8,3/8,5/8,7/8).firstCycle()
)
saw.struct(true, true, true, true).firstCycle(),
sequence(1 / 8, 3 / 8, 5 / 8, 7 / 8).firstCycle(),
);
assert.deepStrictEqual(
saw2.struct(true,true,true,true).firstCycle(),
sequence(-3/4,-1/4,1/4,3/4).firstCycle()
)
})
it('Can make isaw/isaw2', function() {
saw2.struct(true, true, true, true).firstCycle(),
sequence(-3 / 4, -1 / 4, 1 / 4, 3 / 4).firstCycle(),
);
});
it('Can make isaw/isaw2', function () {
assert.deepStrictEqual(
isaw.struct(true,true,true,true).firstCycle(),
sequence(7/8,5/8,3/8,1/8).firstCycle()
)
isaw.struct(true, true, true, true).firstCycle(),
sequence(7 / 8, 5 / 8, 3 / 8, 1 / 8).firstCycle(),
);
assert.deepStrictEqual(
isaw2.struct(true,true,true,true).firstCycle(),
sequence(3/4,1/4,-1/4,-3/4).firstCycle()
)
})
})
isaw2.struct(true, true, true, true).firstCycle(),
sequence(3 / 4, 1 / 4, -1 / 4, -3 / 4).firstCycle(),
);
});
});
describe('_setContext()', () => {
it('Can set the event context', () => {
assert.deepStrictEqual(
pure("a")._setContext([[[0,1],[1,2]]]).firstCycle(true),
[hap(ts(0,1),
ts(0,1),
"a",
[[[0,1],[1,2]]]
)
]
)
})
})
pure('a')
._setContext([
[
[0, 1],
[1, 2],
],
])
.firstCycle(true),
[
hap(ts(0, 1), ts(0, 1), 'a', [
[
[0, 1],
[1, 2],
],
]),
],
);
});
});
describe('_withContext()', () => {
it('Can update the event context', () => {
assert.deepStrictEqual(
pure("a")._setContext([[[0,1],[1,2]]])._withContext(c => [...c,[[3,4],[3,4]]]).firstCycle(true),
[hap(ts(0,1),
ts(0,1),
"a",
[[[0,1],[1,2]],[[3,4],[3,4]]]
)
]
)
})
})
describe("apply", () => {
pure('a')
._setContext([
[
[0, 1],
[1, 2],
],
])
._withContext((c) => [
...c,
[
[3, 4],
[3, 4],
],
])
.firstCycle(true),
[
hap(ts(0, 1), ts(0, 1), 'a', [
[
[0, 1],
[1, 2],
],
[
[3, 4],
[3, 4],
],
]),
],
);
});
});
describe('apply', () => {
it('Can apply a function', () => {
assert.deepStrictEqual(
sequence("a", "b")._apply(fast(2)).firstCycle(),
sequence("a", "b").fast(2).firstCycle()
)
assert.deepStrictEqual(sequence('a', 'b')._apply(fast(2)).firstCycle(), sequence('a', 'b').fast(2).firstCycle());
}),
it('Can apply a pattern of functions', () => {
assert.deepStrictEqual(
sequence("a", "b").apply(fast(2)).firstCycle(),
sequence("a", "b").fast(2).firstCycle()
)
assert.deepStrictEqual(
sequence("a", "b").apply(fast(2),fast(3)).firstCycle(),
sequence("a", "b").fast(2,3).firstCycle()
)
})
})
describe("layer", () => {
it('Can apply a pattern of functions', () => {
assert.deepStrictEqual(sequence('a', 'b').apply(fast(2)).firstCycle(), sequence('a', 'b').fast(2).firstCycle());
assert.deepStrictEqual(
sequence('a', 'b').apply(fast(2), fast(3)).firstCycle(),
sequence('a', 'b').fast(2, 3).firstCycle(),
);
});
});
describe('layer', () => {
it('Can layer up multiple functions', () => {
assert.deepStrictEqual(
sequence(1,2,3).layer(fast(2), pat => pat.add(3,4)).firstCycle(),
stack(sequence(1,2,3).fast(2), sequence(1,2,3).add(3,4)).firstCycle()
)
})
})
describe("early", () => {
it("Can shift an event earlier", () => {
assert.deepStrictEqual(
pure(30)._late(0.25).query(st(1,2)),
[hap(ts(1/4,5/4), ts(1,5/4), 30), hap(ts(5/4,9/4), ts(5/4,2), 30)]
)
})
it("Can shift an event earlier, into negative time", () => {
assert.deepStrictEqual(
pure(30)._late(0.25).query(st(0,1)),
[hap(ts(-3/4,1/4), ts(0,1/4), 30), hap(ts(1/4,5/4), ts(1/4,1), 30)]
)
})
})
describe("off", () => {
it("Can offset a transformed pattern from the original", () => {
sequence(1, 2, 3)
.layer(fast(2), (pat) => pat.add(3, 4))
.firstCycle(),
stack(sequence(1, 2, 3).fast(2), sequence(1, 2, 3).add(3, 4)).firstCycle(),
);
});
});
describe('early', () => {
it('Can shift an event earlier', () => {
assert.deepStrictEqual(pure(30)._late(0.25).query(st(1, 2)), [
hap(ts(1 / 4, 5 / 4), ts(1, 5 / 4), 30),
hap(ts(5 / 4, 9 / 4), ts(5 / 4, 2), 30),
]);
});
it('Can shift an event earlier, into negative time', () => {
assert.deepStrictEqual(pure(30)._late(0.25).query(st(0, 1)), [
hap(ts(-3 / 4, 1 / 4), ts(0, 1 / 4), 30),
hap(ts(1 / 4, 5 / 4), ts(1 / 4, 1), 30),
]);
});
});
describe('off', () => {
it('Can offset a transformed pattern from the original', () => {
assert.deepStrictEqual(
pure(30).off(0.25, add(2)).firstCycle(),
stack(pure(30), pure(30).late(0.25).add(2)).firstCycle()
)
})
})
describe("jux", () => {
it("Can juxtapose", () => {
stack(pure(30), pure(30).late(0.25).add(2)).firstCycle(),
);
});
});
describe('jux', () => {
it('Can juxtapose', () => {
assert.deepStrictEqual(
pure({a: 1}).jux(fast(2))._sortEventsByPart().firstCycle(),
stack(pure({a:1, pan: 0}), pure({a:1, pan: 1}).fast(2))._sortEventsByPart().firstCycle()
)
})
})
})
pure({ a: 1 }).jux(fast(2))._sortEventsByPart().firstCycle(),
stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
._sortEventsByPart()
.firstCycle(),
);
});
});
describe('juxBy', () => {
it('Can juxtapose by half', () => {
assert.deepStrictEqual(
pure({ a: 1 }).juxBy(0.5, fast(2))._sortEventsByPart().firstCycle(),
stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
._sortEventsByPart()
.firstCycle(),
);
});
});
describe('_squeezeJoin', () => {
it('Can squeeze', () => {
assert.deepStrictEqual(
sequence('a', ['a', 'a'])
.fmap((a) => fastcat('b', 'c'))
._squeezeJoin()
.firstCycle(),
sequence(
['b', 'c'],
[
['b', 'c'],
['b', 'c'],
],
).firstCycle(),
);
});
});
describe('ply', () => {
it('Can ply(3)', () => {
assert.deepStrictEqual(
sequence('a', ['b', 'c']).ply(3).firstCycle(),
sequence(pure('a').fast(3), [pure('b').fast(3), pure('c').fast(3)]).firstCycle(),
);
});
});
describe('chop', () => {
it('Can _chop(2)', () => {
assert.deepStrictEqual(
sequence({ sound: 'a' }, { sound: 'b' })._chop(2).firstCycle(),
sequence(
{ sound: 'a', begin: 0, end: 0.5 },
{ sound: 'a', begin: 0.5, end: 1 },
{ sound: 'b', begin: 0, end: 0.5 },
{ sound: 'b', begin: 0.5, end: 1 },
).firstCycle(),
);
});
it('Can chop(2,3)', () => {
assert.deepStrictEqual(
pure({ sound: 'a' }).fast(2).chop(2, 3)._sortEventsByPart().firstCycle(),
sequence(
[
{ sound: 'a', begin: 0, end: 0.5 },
{ sound: 'a', begin: 0.5, end: 1 },
],
[
{ sound: 'a', begin: 0, end: 1 / 3 },
{ sound: 'a', begin: 1 / 3, end: 2 / 3 },
{ sound: 'a', begin: 2 / 3, end: 1 },
],
)
._sortEventsByPart()
.firstCycle(),
);
});
});
describe('range2', () => {
it('Can change the range of a bipolar pattern', () => {
assert.deepStrictEqual(
sequence(-1, -0.5, 0, 0.5).range2(1000, 1100).firstCycle(),
sequence(1000, 1025, 1050, 1075).firstCycle(),
);
});
});
describe('linger', () => {
it('Can linger on the first quarter of a cycle', () => {
assert.deepStrictEqual(
sequence(0, 1, 2, 3, 4, 5, 6, 7).linger(0.25).firstCycle(),
sequence(0, 1, 0, 1, 0, 1, 0, 1).firstCycle(),
);
});
});
});
+14 -1
View File
@@ -1,5 +1,5 @@
import { strict as assert } from 'assert';
import { isNote, tokenizeNote, toMidi, mod } from '../util.mjs';
import { isNote, tokenizeNote, toMidi, mod, compose } from '../util.mjs';
describe('isNote', () => {
it('should recognize notes without accidentals', () => {
@@ -83,3 +83,16 @@ describe('mod', () => {
assert.equal(mod(-3, 2), 1);
});
});
describe('compose', () => {
const add1 = (a) => a + 1;
it('should compose', () => {
assert.equal(compose(add1, add1)(0), 2);
assert.equal(compose(add1)(0), 1);
});
const addS = (s) => (a) => a + s;
it('should compose left to right', () => {
assert.equal(compose(addS('a'), addS('b'))(''), 'ab');
assert.equal(compose(addS('a'), addS('b'))('x'), 'xab');
});
});
+103
View File
@@ -0,0 +1,103 @@
import Fraction from './fraction.mjs';
export class TimeSpan {
constructor(begin, end) {
this.begin = Fraction(begin);
this.end = Fraction(end);
}
get spanCycles() {
const spans = [];
var begin = this.begin;
const end = this.end;
const end_sam = end.sam();
while (end.gt(begin)) {
// If begin and end are in the same cycle, we're done.
if (begin.sam().equals(end_sam)) {
spans.push(new TimeSpan(begin, this.end));
break;
}
// add a timespan up to the next sam
const next_begin = begin.nextSam();
spans.push(new TimeSpan(begin, next_begin));
// continue with the next cycle
begin = next_begin;
}
return spans;
}
cycleArc() {
// Shifts a timespan to one of equal duration that starts within cycle zero.
// (Note that the output timespan probably does not start *at* Time 0 --
// that only happens when the input Arc starts at an integral Time.)
const b = this.begin.cyclePos();
const e = b + (this.end - this.begin);
return new TimeSpan(b, e);
}
withTime(func_time) {
// Applies given function to both the begin and end time of the timespan"""
return new TimeSpan(func_time(this.begin), func_time(this.end));
}
withEnd(func_time) {
// Applies given function to the end time of the timespan"""
return new TimeSpan(this.begin, func_time(this.end));
}
withCycle(func_time) {
// Like withTime, but time is relative to relative to the cycle (i.e. the
// sam of the start of the timespan)
const sam = this.begin.sam();
const b = sam.add(func_time(this.begin.sub(sam)));
const e = sam.add(func_time(this.end.sub(sam)));
return new TimeSpan(b, e);
}
intersection(other) {
// Intersection of two timespans, returns None if they don't intersect.
const intersect_begin = this.begin.max(other.begin);
const intersect_end = this.end.min(other.end);
if (intersect_begin.gt(intersect_end)) {
return undefined;
}
if (intersect_begin.equals(intersect_end)) {
// Zero-width (point) intersection - doesn't intersect if it's at the end of a
// non-zero-width timespan.
if (intersect_begin.equals(this.end) && this.begin.lt(this.end)) {
return undefined;
}
if (intersect_begin.equals(other.end) && other.begin.lt(other.end)) {
return undefined;
}
}
return new TimeSpan(intersect_begin, intersect_end);
}
intersection_e(other) {
// Like 'sect', but raises an exception if the timespans don't intersect.
const result = this.intersection(other);
if (result == undefined) {
// TODO - raise exception
// raise ValueError(f'TimeSpan {self} and TimeSpan {other} do not intersect')
}
return result;
}
midpoint() {
return this.begin.add(this.end.sub(this.begin).div(Fraction(2)));
}
equals(other) {
return this.begin.equals(other.begin) && this.end.equals(other.end);
}
show() {
return this.begin.show() + ' -> ' + this.end.show();
}
}
export default TimeSpan;
+42
View File
@@ -42,3 +42,45 @@ export const getPlayableNoteValue = (event) => {
// rotate array by n steps (to the left)
export const rotate = (arr, n) => arr.slice(n).concat(arr.slice(0, n));
export const pipe = (...funcs) => {
return funcs.reduce(
(f, g) =>
(...args) =>
f(g(...args)),
(x) => x,
);
};
export const compose = (...funcs) => pipe(...funcs.reverse());
// Removes 'None' values from given list
export const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
export const flatten = (arr) => [].concat(...arr);
export const id = (a) => a;
export const constant = (a, b) => a;
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
export function curry(func, overload) {
const fn = function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
const partial = function (...args2) {
return curried.apply(this, args.concat(args2));
};
if (overload) {
overload(partial, args);
}
return partial;
}
};
if (overload) {
// overload function without args... needed for chordBass.transpose(2)
overload(fn, []);
}
return fn;
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { curry } from 'ramda';
import { curry } from './util.mjs';
function unionWithObj(a, b, func) {
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@strudel.cycles/eval",
"version": "0.0.2",
"version": "0.0.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel.cycles/eval",
"version": "0.0.1",
"version": "0.0.3",
"license": "GPL-3.0-or-later",
"dependencies": {
"estraverse": "^5.3.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/eval",
"version": "0.0.2",
"version": "0.0.5",
"description": "Code evaluator for strudel",
"main": "evaluate.mjs",
"directories": {
@@ -28,7 +28,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.2",
"@strudel.cycles/core": "^0.0.5",
"estraverse": "^5.3.0",
"shift-ast": "^6.1.0",
"shift-codegen": "^7.0.3",
+1 -1
View File
@@ -15,7 +15,7 @@ import {
import shiftCodegen from 'shift-codegen';
const codegen = shiftCodegen.default || shiftCodegen; // parcel module resolution fuckup
import * as strudel from '@strudel.cycles/core/strudel.mjs';
import * as strudel from '@strudel.cycles/core';
const { Pattern } = strudel;
+4
View File
@@ -7,3 +7,7 @@ This package adds midi functionality to strudel Patterns.
```sh
npm i @strudel.cycles/midi --save
```
## Dev Notes
- is this package really necessary? currently, /tone also depends on webmidi through @tonejs/piano. Either move piano out of /tone or merge /midi into /tone...
+1 -1
View File
@@ -1,6 +1,6 @@
import { isNote } from 'tone';
import _WebMidi from 'webmidi';
import { Pattern, isPattern } from '@strudel.cycles/core/strudel.mjs';
import { Pattern, isPattern } from '@strudel.cycles/core';
import { Tone } from '@strudel.cycles/tone';
// if you use WebMidi from outside of this package, make sure to import that instance:
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@strudel.cycles/midi",
"version": "0.0.2",
"version": "0.0.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel.cycles/midi",
"version": "0.0.1",
"version": "0.0.4",
"license": "GPL-3.0-or-later",
"dependencies": {
"tone": "^14.7.77",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/midi",
"version": "0.0.2",
"version": "0.0.6",
"description": "Midi API for strudel",
"main": "midi.mjs",
"repository": {
@@ -21,7 +21,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/tone": "^0.0.2",
"@strudel.cycles/tone": "^0.0.6",
"tone": "^14.7.77",
"webmidi": "^2.5.2"
}
-2
View File
@@ -17,8 +17,6 @@ const pattern = mini('a [b c*2]');
const events = pattern.firstCycle().map((e) => e.show());
console.log(events);
document.getElementById('app').innerHTML = events.join('<br/>');
```
yields:
+1 -1
View File
@@ -1,5 +1,5 @@
import * as krill from './krill-parser.js';
import * as strudel from '@strudel.cycles/core/strudel.mjs';
import * as strudel from '@strudel.cycles/core';
import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs';
const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel;
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/mini",
"version": "0.0.2",
"version": "0.0.6",
"description": "Mini notation for strudel",
"main": "mini.mjs",
"type": "module",
@@ -25,7 +25,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/eval": "^0.0.2",
"@strudel.cycles/tone": "^0.0.2"
"@strudel.cycles/eval": "^0.0.5",
"@strudel.cycles/tone": "^0.0.6"
}
}
+37
View File
@@ -0,0 +1,37 @@
# @strudel.cycles/osc
OSC output for strudel patterns! Currently only tested with super collider / super dirt.
## Usage
OSC will only work if you run the REPL locally + the OSC server besides it:
From the project root:
```js
npm run repl
```
and in a seperate shell:
```js
npm run osc
```
This should give you
```log
osc client running on port 57120
osc server running on port 57121
websocket server running on port 8080
```
Now open Supercollider (with the super dirt startup file)
Now open the REPL and type:
```js
s("<bd sd> hh").osc()
```
or just [click here](http://localhost:3000/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
+22
View File
@@ -0,0 +1,22 @@
<button id="send" style="font-size: 2em">Thank you @ojack</button>
<script type="text/javascript" src="./node_modules/osc-js/lib/osc.min.js"></script>
<script type="text/javascript">
const comm = new OSC();
comm.open(); // connect by default to ws://localhost:8080
document.getElementById('send').addEventListener('click', function () {
// this will send a message via websocket to the node server
// the great question: how do i know which time super collider is at????????
const message = new OSC.Message(
'/dirt/play',
...['_id_', '1', 'cps', 0.5625, 'cycle', 412.3333435058594, 'delta', 0.592592716217041, 'orbit', 0, 's', 'hh'],
);
comm.send(message);
console.log('sent:', message.address, message.args);
});
/*
comm.on('*', (m) => {
console.log('received:', m.address, m.args);
}); */
</script>
+23
View File
@@ -0,0 +1,23 @@
import OSC from 'osc-js';
import { Pattern } from '@strudel.cycles/core';
const comm = new OSC();
comm.open();
const latency = 0.1;
Pattern.prototype.osc = function () {
return this._withEvent((event) => {
const onTrigger = (time, event, currentTime) => {
// time should be audio time of onset
// currentTime should be current time of audio context (slightly before time)
const keyvals = Object.entries(event.value).flat();
const offset = (time - currentTime + latency) * 1000;
const ts = Math.floor(Date.now() + offset);
const message = new OSC.Message('/dirt/play', ...keyvals);
const bundle = new OSC.Bundle([message], ts);
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
comm.send(bundle);
};
return event.setContext({ ...event.context, onTrigger });
});
};
+60
View File
@@ -0,0 +1,60 @@
{
"name": "@strudel.cycles/osc",
"version": "0.0.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel.cycles/osc",
"version": "0.0.1",
"license": "GPL-3.0-or-later",
"dependencies": {
"osc-js": "^2.3.2"
}
},
"node_modules/osc-js": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.2.tgz",
"integrity": "sha512-9i7J4u1hH+glooGMh+ki1ni0JGqKmylT8r0nXKugHbRK63rR+kl4O+5tGW6+/EszjbCju3KV+eXQQzFDdGrmhg==",
"dependencies": {
"ws": "^8.5.0"
}
},
"node_modules/ws": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
},
"dependencies": {
"osc-js": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.2.tgz",
"integrity": "sha512-9i7J4u1hH+glooGMh+ki1ni0JGqKmylT8r0nXKugHbRK63rR+kl4O+5tGW6+/EszjbCju3KV+eXQQzFDdGrmhg==",
"requires": {
"ws": "^8.5.0"
}
},
"ws": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz",
"integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==",
"requires": {}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
{
"name": "@strudel.cycles/osc",
"version": "0.0.2",
"description": "OSC messaging for strudel",
"main": "osc.mjs",
"scripts": {
"test": "echo \"No tests present.\" && exit 0",
"server": "node server.js",
"tidal-sniffer": "node tidal-sniffer.js",
"client": "npx serve -p 4321"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"osc-js": "^2.3.2"
}
}
+30
View File
@@ -0,0 +1,30 @@
const OSC = require('osc-js');
const config = {
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
udpServer: {
host: 'localhost', // @param {string} Hostname of udp server to bind to
port: 57121, // @param {number} Port of udp client for messaging
// enabling the following line will receive tidal messages:
// port: 57120, // @param {number} Port of udp client for messaging
exclusive: false, // @param {boolean} Exclusive flag
},
udpClient: {
host: 'localhost', // @param {string} Hostname of udp client for messaging
port: 57120, // @param {number} Port of udp client for messaging
},
wsServer: {
host: 'localhost', // @param {string} Hostname of WebSocket server
port: 8080, // @param {number} Port of WebSocket server
},
};
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
osc.open(); // start a WebSocket server on port 8080
console.log('osc client running on port', config.udpClient.port);
console.log('osc server running on port', config.udpServer.port);
console.log('websocket server running on port', config.wsServer.port);
+125
View File
@@ -0,0 +1,125 @@
const OSC = require('osc-js');
const config = {
receiver: 'ws', // @param {string} Where messages sent via 'send' method will be delivered to, 'ws' for Websocket clients, 'udp' for udp client
udpServer: {
host: 'localhost', // @param {string} Hostname of udp server to bind to
port: 57120, // @param {number} Port of udp client for messaging
exclusive: false, // @param {boolean} Exclusive flag
},
udpClient: {
host: 'localhost', // @param {string} Hostname of udp client for messaging
// port: 57120, // @param {number} Port of udp client for messaging
port: 41235, // @param {number} Port of udp client for messaging
},
wsServer: {
host: 'localhost', // @param {string} Hostname of WebSocket server
port: 8080, // @param {number} Port of WebSocket server
},
};
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
osc.open(); // start a WebSocket server on port 8080
console.log('osc client running on port', config.udpClient.port);
console.log('osc server running on port', config.udpServer.port);
console.log('websocket server running on port', config.wsServer.port);
// listen for messages from the client
osc.on('*', (m) => {
console.log('received:', m.address, m.args);
});
/*
example tidal messages:
/*
received: /dirt/play [
'_id_',
'1',
'cps',
0.5625,
'cycle',
503.5,
'delta',
0.8888888359069824,
'orbit',
0,
's',
'bd'
]
received: /dirt/play [
'_id_', '1',
'cps', 0.5625,
'cycle', 503.6666564941406,
'delta', 0.592592716217041,
'orbit', 0,
's', 'hh'
]
received: /dirt/play [
'_id_',
'1',
'cps',
0.5625,
'cycle',
504,
'delta',
0.8888888359069824,
'orbit',
0,
's',
'bd'
]
received: /dirt/play [
'_id_',
'1',
'cps',
0.5625,
'cycle',
504,
'delta',
0.592592716217041,
'orbit',
0,
's',
'hh'
]
received: /dirt/play [
'_id_',
'1',
'cps',
0.5625,
'cycle',
504.3333435058594,
'delta',
0.5925922393798828,
'orbit',
0,
's',
'hh'
]
received: /dirt/play [
'_id_',
'1',
'cps',
0.5625,
'cycle',
504.5,
'delta',
0.8888888359069824,
'orbit',
0,
's',
'bd'
]
received: /dirt/play [
'_id_', '1',
'cps', 0.5625,
'cycle', 504.6666564941406,
'delta', 0.592592716217041,
'orbit', 0,
's', 'hh'
]
*/
+2692 -2754
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tonal",
"version": "0.0.2",
"version": "0.0.5",
"description": "Tonal functions for strudel",
"main": "tonal.mjs",
"type": "module",
@@ -25,7 +25,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.2",
"@strudel.cycles/core": "^0.0.5",
"@tonaljs/tonal": "^4.6.5",
"webmidi": "^3.0.15"
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { strict as assert } from 'assert';
import '../tonal.mjs'; // need to import this to add prototypes
import { pure } from '@strudel.cycles/core/strudel.mjs';
import { pure } from '@strudel.cycles/core';
describe('tonal', () => {
it('Should run tonal functions ', () => {
+1 -2
View File
@@ -1,6 +1,5 @@
import { Note, Interval, Scale } from '@tonaljs/tonal';
import { Pattern } from '@strudel.cycles/core';
import { mod } from '@strudel.cycles/core/util.mjs';
import { Pattern, mod } from '@strudel.cycles/core';
// transpose note inside scale by offset steps
// function scaleTranspose(scale: string, offset: number, note: string) {
+1 -1
View File
@@ -1,4 +1,4 @@
import { Pattern as _Pattern, stack, Hap, reify } from '@strudel.cycles/core/strudel.mjs';
import { Pattern as _Pattern, stack, Hap, reify } from '@strudel.cycles/core';
import _voicings from 'chord-voicings';
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
+27
View File
@@ -8,6 +8,33 @@ This package adds Tone.js functions to strudel Patterns.
npm i @strudel.cycles/tone --save
```
## Example
The following example will create a pattern and play it back with tone.js:
```js
import { sequence, stack, State, TimeSpan } from '@strudel.cycles/core';
import { Tone, polysynth, osc, out } from '@strudel.cycles/tone';
const pattern = sequence('c3', ['eb3', stack('g3', 'bb3')]).tone(polysynth().set(osc('sawtooth4')).chain(out()));
document.getElementById('play').addEventListener('click', async () => {
await Tone.start();
Tone.getTransport().stop();
const events = pattern.query(new State(new TimeSpan(0, 4))).filter((e) => e.whole.begin.equals(e.part.begin));
events.forEach((event) =>
Tone.getTransport().schedule((time) => event.context.onTrigger(time, event), event.whole.begin.valueOf()),
);
Tone.getTransport().start('+0.1');
});
```
[open in codesandbox](https://codesandbox.io/s/strudel-tone-example-5ph2te?file=/src/index.js:0-708)
## API
See "Tone API" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
## Dev Notes
- `@tonejs/piano` has peer dependency on `webmidi@^2.5.1`! A newer version of webmidi will break.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@strudel.cycles/tone",
"version": "0.0.2",
"version": "0.0.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@strudel.cycles/tone",
"version": "0.0.1",
"version": "0.0.4",
"license": "GPL-3.0-or-later",
"dependencies": {
"@tonejs/piano": "^0.2.1",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/tone",
"version": "0.0.2",
"version": "0.0.6",
"description": "Tone.js API for strudel",
"main": "tone.mjs",
"type": "module",
@@ -22,7 +22,7 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.2",
"@strudel.cycles/core": "^0.0.5",
"@tonejs/piano": "^0.2.1",
"chord-voicings": "^0.0.1",
"tone": "^14.7.77"
+1 -1
View File
@@ -1,6 +1,6 @@
import { strict as assert } from 'assert';
import '../tone.mjs';
import { pure } from '@strudel.cycles/core/strudel.mjs';
import { pure } from '@strudel.cycles/core';
import Tone from 'tone';
describe('tone', () => {
+10 -7
View File
@@ -30,13 +30,16 @@ import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
// "balanced" | "interactive" | "playback";
// Tone.setContext(new Tone.Context({ latencyHint: 'playback', lookAhead: 1 }));
export const defaultSynth = new PolySynth().chain(new Gain(0.5), getDestination());
defaultSynth.set({
oscillator: { type: 'triangle' },
envelope: {
release: 0.01,
},
});
export const getDefaultSynth = () => {
const s = new PolySynth().chain(new Gain(0.5), getDestination());
s.set({
oscillator: { type: 'triangle' },
envelope: {
release: 0.01,
},
});
return s;
};
// what about
// https://www.charlie-roberts.com/gibberish/playground/
+30
View File
@@ -0,0 +1,30 @@
# @strudel.cycles/webaudio
This package contains a scheduler + a clockworker and synths based on the Web Audio API.
It's an alternative to `@strudel.cycles/tone`, with better performance, but less features.
## Install
```sh
npm i @strudel.cycles/webaudio --save
```
## Example
```js
import { Scheduler, getAudioContext } from '@strudel.cycles/webaudio';
const scheduler = new Scheduler({
audioContext: getAudioContext(),
interval: 0.1,
onEvent: (e) => e.context?.createAudioNode?.(e),
});
const pattern = sequence([55, 99], 110).osc('sawtooth').out()
scheduler.setPattern(pattern);
scheduler.start()
//scheduler.stop()
```
A more sophisticated example can be found in [examples/repl.html](./examples/repl.html).
You can run it inside this directory with `npm run example`.
+69
View File
@@ -0,0 +1,69 @@
// helpers to create a worker dynamically without needing a server / extra file
const stringifyFunction = (func) => '(' + func + ')();';
const urlifyFunction = (func) => URL.createObjectURL(new Blob([stringifyFunction(func)], { type: 'text/javascript' }));
const createWorker = (func) => new Worker(urlifyFunction(func));
// this class is basically the tale of two clocks
class ClockWorker {
worker;
audioContext;
interval = 0.2; // query span
lastEnd = 0;
constructor(audioContext, callback, interval = this.interval) {
this.audioContext = audioContext;
this.interval = interval;
this.worker = createWorker(() => {
// we cannot use closures here!
let interval;
let timerID = null; // this is clock #1 (the sloppy js clock)
const clear = () => {
if (timerID) {
clearInterval(timerID);
timerID = null;
}
};
const start = () => {
clear();
if (!interval) {
throw new Error('no interval set! call worker.postMessage({interval}) before starting.');
}
timerID = setInterval(() => postMessage('tick'), interval * 1000);
};
self.onmessage = function (e) {
if (e.data == 'start') {
start();
} else if (e.data.interval) {
interval = e.data.interval;
if (timerID) {
start();
}
} else if (e.data == 'stop') {
clear();
}
};
});
this.worker.postMessage({ interval });
// const round = (n, d) => Math.round(n * d) / d;
const precision = 100;
this.worker.onmessage = (e) => {
if (e.data === 'tick') {
const begin = this.lastEnd || this.audioContext.currentTime;
const end = this.audioContext.currentTime + this.interval; // DONT reference begin here!
this.lastEnd = end;
// callback with query span, using clock #2 (the audio clock)
callback(begin, end);
}
};
}
start() {
console.log('start...');
this.audioContext.resume();
this.worker.postMessage('start');
}
stop() {
console.log('stop...');
this.worker.postMessage('stop');
}
}
export default ClockWorker;
+61
View File
@@ -0,0 +1,61 @@
<div style="position: absolute; top: 0; right: 0; padding: 4px">
<button id="start" style="margin-bottom: 4px; font-size: 2em">start</button><br />
<button id="stop" style="font-size: 2em">stop</button>
</div>
<textarea
style="font-size: 2em; background: #e8d565; color: #323230; height: 100%; width: 100%; outline: none; border: 0"
id="text"
spellcheck="false"
>
Loading...</textarea
>
<script type="module">
document.body.style = 'margin: 0';
import * as strudel from '@strudel.cycles/core';
import '@strudel.cycles/core/euclid.mjs';
import { Scheduler, getAudioContext } from '@strudel.cycles/webaudio';
const { cat, State, TimeSpan } = strudel;
Object.assign(window, strudel); // add strudel to eval scope
const scheduler = new Scheduler({
audioContext: getAudioContext(),
interval: 0.1,
onEvent: (e) => {
e.context?.createAudioNode?.(e);
},
});
let initialCode = `sequence(1,2).mul(55/2) // frequencies
.mul(slowcat(1,2))
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
.fast(3)
.velocity(.5)
.osc(cat('sawtooth','square').fast(2))
.adsr(0.01,.02,.5,0.1)
.filter('lowshelf',800,25)
.out()`;
try {
const base64 = decodeURIComponent(window.location.href.split('#')[1]);
initialCode = atob(base64);
} catch (err) {
console.warn('failed to decode', err);
}
const input = document.getElementById('text');
input.value = initialCode;
const evaluate = () => {
try {
const pattern = eval(input.value);
scheduler.setPattern(pattern);
window.location.hash = '#' + encodeURIComponent(btoa(input.value)); // update url hash
} catch (err) {
console.warn(err);
}
};
evaluate();
input.addEventListener('input', () => evaluate());
document.getElementById('start').addEventListener('click', () => scheduler.start());
document.getElementById('stop').addEventListener('click', () => scheduler.stop());
</script>
+3
View File
@@ -0,0 +1,3 @@
export { default as ClockWorker } from './clockworker.mjs';
export { default as Scheduler } from './scheduler.mjs';
export * from './webaudio.mjs';
+3851
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
{
"name": "@strudel.cycles/webaudio",
"version": "0.0.4",
"description": "Web Audio helpers for Strudel",
"main": "index.mjs",
"directories": {
"example": "examples"
},
"scripts": {
"example": "parcel examples/repl.html"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tidalcycles/strudel.git"
},
"keywords": [
"tidalcycles",
"strudel",
"pattern",
"livecoding",
"algorave"
],
"author": "Felix Roos <flix91@gmail.com>",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/tidalcycles/strudel/issues"
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.5"
},
"devDependencies": {
"parcel": "^2.4.1"
}
}
+38
View File
@@ -0,0 +1,38 @@
import ClockWorker from './clockworker.mjs';
class Scheduler {
worker;
pattern;
constructor({ audioContext, interval = 0.2, onEvent }) {
this.worker = new ClockWorker(
audioContext,
(begin, end) => {
this.pattern.query(new State(new TimeSpan(begin, end))).forEach((e) => {
if (!e.part.begin.equals(e.whole.begin)) {
return;
}
if (onEvent) {
onEvent?.(e);
} else {
console.warn('unplayable event: no audio node nor onEvent callback', e);
}
});
},
interval,
);
}
start() {
if (!this.pattern) {
throw new Error('Scheduler: no pattern set! call .setPattern first.');
}
this.worker.start();
}
stop() {
this.worker.stop();
}
setPattern(pat) {
this.pattern = pat;
}
}
export default Scheduler;
+74
View File
@@ -0,0 +1,74 @@
import { Pattern } from '@strudel.cycles/core';
let audioContext;
export const getAudioContext = () => {
if (!audioContext) {
audioContext = new AudioContext();
}
return audioContext;
};
const lookahead = 0.2;
const adsr = (attack, decay, sustain, release, velocity, begin, end) => {
const gainNode = getAudioContext().createGain();
gainNode.gain.setValueAtTime(0, begin);
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
// for some reason, using exponential ramping creates little cracklings
return gainNode;
};
Pattern.prototype.withAudioNode = function (createAudioNode) {
return this._withEvent((event) => {
return event.setContext({
...event.context,
createAudioNode: (e) => createAudioNode(e, event.context.createAudioNode?.(event)),
});
});
};
Pattern.prototype._osc = function (type) {
return this.withAudioNode((e) => {
const osc = getAudioContext().createOscillator();
osc.type = type;
osc.frequency.value = e.value; // expects frequency..
osc.start(e.whole.begin.valueOf() + lookahead);
osc.stop(e.whole.end.valueOf() + lookahead); // release?
return osc;
});
};
Pattern.prototype.adsr = function (a = 0.01, d = 0.05, s = 1, r = 0.01) {
return this.withAudioNode((e, node) => {
const velocity = e.context?.velocity || 1;
const envelope = adsr(a, d, s, r, velocity, e.whole.begin.valueOf() + lookahead, e.whole.end.valueOf() + lookahead);
node?.connect(envelope);
return envelope;
});
};
Pattern.prototype.filter = function (type = 'lowshelf', frequency = 1000, gain = 25) {
return this.withAudioNode((e, node) => {
const filter = getAudioContext().createBiquadFilter();
filter.type = type;
filter.frequency.value = frequency;
filter.gain.value = gain;
node?.connect(filter);
return filter;
});
};
Pattern.prototype.out = function () {
const master = getAudioContext().createGain();
master.gain.value = 0.1;
master.connect(getAudioContext().destination);
return this.withAudioNode((e, node) => {
if (!node) {
console.warn('out: no source! call .osc() first');
}
node?.connect(master);
});
};
Pattern.prototype.define('osc', (type, pat) => pat.osc(type), { patternified: true });
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@strudel.cycles/xen",
"version": "0.0.2",
"version": "0.0.5",
"description": "Xenharmonic API for strudel",
"main": "xen.mjs",
"scripts": {
@@ -24,6 +24,6 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
"@strudel.cycles/core": "^0.0.2"
"@strudel.cycles/core": "^0.0.5"
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
import Tune from './tunejs.js';
import { Pattern } from '@strudel.cycles/core/strudel.mjs';
import { Pattern } from '@strudel.cycles/core';
Pattern.prototype._tune = function (scale, tonic = 220) {
const tune = new Tune();
+1 -2
View File
@@ -1,5 +1,4 @@
import { Pattern } from '@strudel.cycles/core/strudel.mjs';
import { mod } from '@strudel.cycles/core/util.mjs';
import { Pattern, mod } from '@strudel.cycles/core';
export function edo(name) {
if (!/^[1-9]+[0-9]*edo$/.test(name)) {
+2 -9
View File
@@ -13,11 +13,8 @@ The REPL is deployed at [strudel.tidalcycles.org](https://strudel.tidalcycles.or
```bash
# from project root
npm install
npx lerna bootstrap
cd repl
npm install
npm run start
npm run setup
npm run repl
```
## Build REPL
@@ -27,7 +24,3 @@ cd repl
npm run build # <- builds repl + tutorial to ../docs
npm run static # <- test static build
```
## Dev Notes
~~Always run `npm i --legacy-peer-deps`, otherwise `@tonejs/piano` will break.~~
+23 -7
View File
@@ -3,14 +3,14 @@ import CodeMirror, { markEvent, markParens } from './CodeMirror';
import cx from './cx';
import logo from './logo.svg';
import playStatic from './static.mjs';
import { defaultSynth } from '@strudel.cycles/tone';
import { getDefaultSynth } from '@strudel.cycles/tone';
import * as tunes from './tunes.mjs';
import useRepl from './useRepl.mjs';
import { useWebMidi } from './useWebMidi';
import './App.css';
// eval stuff start
import { evaluate, extend } from '@strudel.cycles/eval';
import * as strudel from '@strudel.cycles/core/strudel.mjs';
import * as strudel from '@strudel.cycles/core';
import gist from '@strudel.cycles/core/gist.js';
import { mini } from '@strudel.cycles/mini/mini.mjs';
import { Tone } from '@strudel.cycles/tone';
@@ -26,15 +26,28 @@ import '@strudel.cycles/tonal/tonal.mjs';
import '@strudel.cycles/xen/xen.mjs';
import '@strudel.cycles/xen/tune.mjs';
import '@strudel.cycles/core/euclid.mjs';
import '@strudel.cycles/core/speak.mjs';
import '@strudel.cycles/tone/pianoroll.mjs';
import '@strudel.cycles/tone/draw.mjs';
import '@strudel.cycles/osc/osc.mjs';
import controls from '@strudel.cycles/core/controls.mjs';
extend(Tone, strudel, strudel.Pattern.prototype.bootstrap(), toneHelpers, voicingHelpers, drawHelpers, uiHelpers, {
gist,
euclid,
mini,
extend(
Tone,
});
strudel,
strudel.Pattern.prototype.bootstrap(),
controls,
toneHelpers,
voicingHelpers,
drawHelpers,
uiHelpers,
{
gist,
euclid,
mini,
Tone,
},
);
// eval stuff end
const codeParam = window.location.href.split('#')[1];
@@ -52,6 +65,7 @@ function getRandomTune() {
}
const randomTune = getRandomTune();
const defaultSynth = getDefaultSynth();
function App() {
const [editor, setEditor] = useState();
@@ -75,8 +89,10 @@ function App() {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
await activateCode();
e.preventDefault();
} else if (e.code === 'Period') {
cycle.stop();
e.preventDefault();
}
}
};
+3 -1
View File
@@ -2,7 +2,9 @@ import { Tone } from '@strudel.cycles/tone';
import { State, TimeSpan } from '@strudel.cycles/core';
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
import { evaluate } from '@strudel.cycles/eval';
import { defaultSynth } from '@strudel.cycles/tone';
import { getDefaultSynth } from '@strudel.cycles/tone';
const defaultSynth = getDefaultSynth();
// this is a test to play back events with as less runtime code as possible..
// the code asks for the number of seconds to prequery
+74
View File
@@ -668,3 +668,77 @@ export const echoPiano = `"<0 2 [4 6](3,4,1) 3*2>"
.echo(4, 1/8, .5)
.tone((await piano()).chain(out()))
.pianoroll()`;
export const sml1 = `
stack(
// melody
\`<
[e5 ~] [[d5@2 c5] [~@2 e5]] ~ [~ [c5@2 d5]] [e5 e5] [d5 c5] [e5 f5] [g5 a5]
[~ c5] [c5 d5] [e5 [c5@2 c5]] [~ c5] [f5 e5] [c5 d5] [~ g6] [g6 ~]
[e5 ~] [[d5@2 c5] [~@2 e5]] ~ [~ [c5@2 d5]] [e5 e5] [d5 c5] [a5 g5] [c6 [e5@2 d5]]
[~ c5] [c5 d5] [e5 [c5@2 c5]] [~ c5] [f5 e5] [c5 d5] [~ [g6@2 ~] ~@2] [g5 ~]
[~ a5] [b5 c6] [b5@2 ~@2 g5] ~
[f5 ~] [[g5@2 f5] ~] [[e5 ~] [f5 ~]] [[f#5 ~] [g5 ~]]
[~ a5] [b5 c6] [b5@2 ~@2 g5] ~
[eb6 d6] [~ c6] ~!2
>\`
.legato(.95),
// sub melody
\`<
[~ g4]!2 [~ ab4]!2 [~ a4]!2 [~ bb4]!2
[~ a4]!2 [~ g4]!2 [d4 e4] [f4 gb4] ~!2
[~ g4]!2 [~ ab4]!2 [~ a4]!2 [~ bb4]!2
[~ a4]!2 [~ g4]!2 [d4 e4] [f4 gb4] ~!2
[~ c5]!4 [~ a4]!2 [[c4 ~] [d4 ~]] [[eb4 ~] [e4 ~]]
[~ c5]!4 [~ eb5]!2 [g4*2 [f4 ~]] [[e4 ~] [d4 ~]]
>\`,
// bass
\`<
c3!7 a3 f3!2
e3!2 ~!4
c3!7 a3 f3!2
e3!2 ~!4
f3!2 e3!2 d3!2 ~!2
f3!2 e3!2 ab3!2 ~!2
>\`
.legato(.5)
).fast(2) //.tone((await piano()).chain(out()))`;
export const speakerman = `backgroundImage('https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.ytimg.com%2Fvi%2FXR0rKqW3VwY%2Fmaxresdefault.jpg&f=1&nofb=1',
{ className:'darken', style:'background-size:cover'})
stack(
"[g3,bb3,d4] [f3,a3,c4] [c3,e3,g3]@2".slow(2).late(.1),
slowcat(
'Baker man',
'is baking bread',
'Baker man',
'is baking bread',
'Sagabona',
'kunjani wena',
'Sagabona',
'kunjani wena',
'The night train, is coming',
'got to keep on running',
'The night train, is coming',
'got to keep on running',
).speak("en zu en".slow(12), "<0 2 3 4 5 6>".slow(2)),
).slow(4)`;
export const randomBells = `const delay = new FeedbackDelay(1/3, .8).chain(vol(.2), out());
let bell = await sampler({
C6: 'https://freesound.org/data/previews/411/411089_5121236-lq.mp3'
})
const bass = await sampler({
d2: 'https://freesound.org/data/previews/608/608286_13074022-lq.mp3'
});
bell = bell.chain(vol(0.6).connect(delay),out());
"0".euclidLegato(3,8)
.add(rand.range(0,12))
.echo(3, 1/16, (x,n)=>x.add((n+1)*2).velocity(1/(n+1)))
.velocity(rand.range(.5,1))
.legato(rand.range(.4,3))
.scale(slowcat('D minor pentatonic')).tone(bell)
.stack("<D2 A2 G2 F2>".euclidLegato(6,8,1).tone(bass.toDestination()))
.slow(6)
.pianoroll({minMidi:20,maxMidi:120,background:'transparent'})`;
+1 -1
View File
@@ -6,7 +6,7 @@ import cx from '../cx';
// eval stuff start
import { extend } from '@strudel.cycles/eval';
import * as strudel from '@strudel.cycles/core/strudel.mjs';
import * as strudel from '@strudel.cycles/core';
import gist from '@strudel.cycles/core/gist.js';
import { mini } from '@strudel.cycles/mini/mini.mjs';
import { Tone } from '@strudel.cycles/tone';
+6 -6
View File
@@ -99,7 +99,7 @@ You can also press "play" on the next player without needing to stop the last on
## Sequences
We can play more notes by seperating them with spaces:
We can play more notes by separating them with spaces:
<MiniRepl tune={`"e5 b4 d5 c5"`} />
@@ -195,14 +195,14 @@ Internally, the mini notation will expand to use the actual functional JavaScrip
Notes are automatically available as variables:
<MiniRepl tune={`e4`} />
<MiniRepl tune={`sequence(d4, fs4, a4)`} />
An important difference to the mini notation:
For sharp notes, the letter "s" is used instead of "#", because JavaScript does not support "#" in a variable name.
The above is the same as:
<MiniRepl tune={`"e4"`} />
<MiniRepl tune={`sequence('d4', 'f#4', 'a4')`} />
Using strings, you can also use "#".
@@ -331,13 +331,13 @@ Will reverse the pattern:
Will apply the given function every n cycles:
<MiniRepl tune={`cat(e5, pure(b4).every(4, late(0.5)))`} />
<MiniRepl tune={`cat(e5, "b4".every(4, late(0.5)))`} />
<!-- TODO: should be able to do b4.every => like already possible with fast slow etc.. -->
Note that late is called directly. This is a shortcut for:
<MiniRepl tune={`cat(e5, pure(b4).every(4, x => x.late(0.5)))`} />
<MiniRepl tune={`cat(e5, "b4".every(4, x => x.late(0.5)))`} />
<!-- TODO: should the function really run the first cycle? -->
@@ -640,7 +640,7 @@ Patternified autofilter:
<MiniRepl
tune={`"c2 c3"
.synth('sawtooth16').autofilter("<1 4 8>"")`}
.synth('sawtooth16').autofilter("<1 4 8>")`}
/>
## Tonal API
+2 -2
View File
@@ -39,10 +39,10 @@ function useCycle(props) {
// schedule events for next cycle
events
?.filter((event) => event.part.begin.equals(event.whole.begin))
?.filter((event) => event.part.begin.equals(event.whole?.begin))
.forEach((event) => {
Tone.getTransport().schedule((time) => {
onEvent(time, event);
onEvent(time, event, Tone.getContext().currentTime);
Tone.Draw.schedule(() => {
// do drawing or DOM manipulation here
onDraw?.(time, event);
+9 -9
View File
@@ -23,11 +23,18 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }) {
const dirty = useMemo(() => code !== activeCode || error, [code, activeCode, error]);
const pushLog = useCallback((message) => setLog((log) => log + `${log ? '\n\n' : ''}${message}`), []);
// below block allows disabling the highlighting by including "strudel disable-highlighting" in the code (as comment)
onDraw = useMemo(() => {
if (activeCode && !activeCode.includes('strudel disable-highlighting')) {
return onDraw;
}
}, [activeCode, onDraw]);
// cycle hook to control scheduling
const cycle = useCycle({
onDraw,
onEvent: useCallback(
(time, event) => {
(time, event, currentTime) => {
try {
onEvent?.(event);
const { onTrigger, velocity } = event.context;
@@ -41,7 +48,7 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }) {
/* console.warn('no instrument chosen', event);
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
} else {
onTrigger(time, event);
onTrigger(time, event, currentTime);
}
} catch (err) {
console.warn(err);
@@ -111,13 +118,6 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw }) {
}
};
// below block allows disabling the highlighting by including "strudel disable-highlighting" in the code (as comment)
onDraw = useMemo(() => {
if (activeCode && !activeCode.includes('strudel disable-highlighting')) {
return onDraw;
}
}, [activeCode, onDraw]);
const togglePlay = () => {
if (!cycle.started) {
activateCode();