mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 21:58:37 +00:00
Merge remote-tracking branch 'origin/main' into webaudio
This commit is contained in:
commit
192eb28ac7
111
CONTRIBUTING.md
Normal file
111
CONTRIBUTING.md
Normal 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!
|
||||
44
README.md
44
README.md
@ -4,40 +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
|
||||
```
|
||||
|
||||
## Publish Packages
|
||||
## Using Strudel In Your Project
|
||||
|
||||
To publish, just run:
|
||||
There are multiple npm packages you can use to use strudel, or only parts of it, in your project:
|
||||
|
||||
```sh
|
||||
npx lerna version
|
||||
```
|
||||
- [`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
|
||||
|
||||
This will publish all packages that changed since the last version.
|
||||
Click on the package names to find out more about each one.
|
||||
|
||||
## Style
|
||||
## Contributing
|
||||
|
||||
For now, please try to copy the style of surrounding code. VS Code users can install the 'prettier' add-on which will use the .prettierrc configuration file for automatic formatting.
|
||||
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
|
||||
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/
|
||||
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.
|
||||
The discord and forum is shared with the haskell (tidal) and python (vortex) siblings of this project.
|
||||
|
||||
@ -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.194ee673.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.194ee673.js.map": "/static/js/main.194ee673.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.194ee673.js"
|
||||
]
|
||||
}
|
||||
@ -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.194ee673.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>
|
||||
2
docs/static/css/main.0d689283.css.map
vendored
2
docs/static/css/main.0d689283.css.map
vendored
File diff suppressed because one or more lines are too long
2
docs/static/js/787.1c52cb78.chunk.js
vendored
Normal file
2
docs/static/js/787.1c52cb78.chunk.js
vendored
Normal 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
docs/static/js/787.8f7ec9e0.chunk.js
vendored
2
docs/static/js/787.8f7ec9e0.chunk.js
vendored
@ -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
|
||||
3
docs/static/js/main.194ee673.js
vendored
Normal file
3
docs/static/js/main.194ee673.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
docs/static/js/main.194ee673.js.map
vendored
Normal file
1
docs/static/js/main.194ee673.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
docs/static/js/main.77e38ada.js
vendored
3
docs/static/js/main.77e38ada.js
vendored
File diff suppressed because one or more lines are too long
1
docs/static/js/main.77e38ada.js.map
vendored
1
docs/static/js/main.77e38ada.js.map
vendored
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
75
docs/tutorial/index.3b5f65fb.js
Normal file
75
docs/tutorial/index.3b5f65fb.js
Normal file
File diff suppressed because one or more lines are too long
1
docs/tutorial/index.3b5f65fb.js.map
Normal file
1
docs/tutorial/index.3b5f65fb.js.map
Normal file
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 @@
|
||||
<!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.3b5f65fb.js" defer></script> </body></html>
|
||||
@ -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
|
||||
|
||||
5102
package-lock.json
generated
5102
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,10 @@
|
||||
"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"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
||||
@ -15,14 +15,14 @@ import { sequence, State, TimeSpan } 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
packages/core/controls.mjs
Normal file
288
packages/core/controls.mjs
Normal 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,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
packages/core/examples/basic.html
Normal file
26
packages/core/examples/basic.html
Normal 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
packages/core/examples/canvas.html
Normal file
34
packages/core/examples/canvas.html
Normal 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>
|
||||
@ -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
packages/core/hap.mjs
Normal file
90
packages/core/hap.mjs
Normal 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;
|
||||
@ -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';
|
||||
21
packages/core/package-lock.json
generated
21
packages/core/package-lock.json
generated
@ -1,17 +1,16 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"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",
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "strudel.mjs",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "mocha --colors"
|
||||
@ -26,8 +26,7 @@
|
||||
"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"
|
||||
|
||||
@ -1,224 +1,20 @@
|
||||
import TimeSpan from './timespan.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
import { compose } from 'ramda'; // will remove this as soon as compose is implemented here
|
||||
import { isNote, toMidi } from './util.mjs';
|
||||
import Hap from './hap.mjs';
|
||||
import State from './state.mjs';
|
||||
|
||||
// Removes 'None' values from given list
|
||||
const removeUndefineds = (xs) => xs.filter((x) => x != undefined);
|
||||
import { isNote, toMidi, compose, removeUndefineds, flatten, id, listRange, curry } from './util.mjs';
|
||||
|
||||
const flatten = (arr) => [].concat(...arr);
|
||||
|
||||
const id = (a) => a;
|
||||
|
||||
const range = (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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
withTime(func_time) {
|
||||
// Applies given function to both the begin and end time value of the timespan"""
|
||||
return new TimeSpan(func_time(this.begin), func_time(this.end));
|
||||
}
|
||||
withEnd(func_time) {
|
||||
// Applies given function to both the begin and end time value of the timespan"""
|
||||
return new TimeSpan(this.begin, func_time(this.end));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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 + ')'
|
||||
);
|
||||
}
|
||||
|
||||
setContext(context) {
|
||||
return new Hap(this.whole, this.part, this.value, context);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
class Pattern {
|
||||
export class Pattern {
|
||||
// the following functions will get patternFactories as nested functions:
|
||||
constructor(query) {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
queryArc(begin, end) {
|
||||
return this.query(new State(new TimeSpan(begin, end)));
|
||||
}
|
||||
|
||||
_splitQueries() {
|
||||
// Splits queries at cycle boundaries. This makes some calculations
|
||||
// easier to express, as all events are then constrained to happen within
|
||||
@ -352,12 +148,11 @@ class Pattern {
|
||||
if (s == undefined) {
|
||||
return undefined;
|
||||
}
|
||||
// TODO: is it right to add event_val.context here?
|
||||
return new Hap(
|
||||
whole_func(event_func.whole, event_val.whole),
|
||||
s,
|
||||
event_func.value(event_val.value),
|
||||
event_val.context,
|
||||
event_val.combineContext(event_func),
|
||||
);
|
||||
};
|
||||
return flatten(
|
||||
@ -389,11 +184,8 @@ class Pattern {
|
||||
const new_whole = hap_func.whole;
|
||||
const new_part = hap_func.part.intersection_e(hap_val.part);
|
||||
const new_value = hap_func.value(hap_val.value);
|
||||
const hap = new Hap(new_whole, new_part, new_value, {
|
||||
...hap_val.context,
|
||||
...hap_func.context,
|
||||
locations: (hap_val.context.locations || []).concat(hap_func.context.locations || []),
|
||||
});
|
||||
const new_context = hap_val.combineContext(hap_func);
|
||||
const hap = new Hap(new_whole, new_part, new_value, new_context);
|
||||
haps.push(hap);
|
||||
}
|
||||
}
|
||||
@ -413,11 +205,8 @@ class Pattern {
|
||||
const new_whole = hap_val.whole;
|
||||
const new_part = hap_func.part.intersection_e(hap_val.part);
|
||||
const new_value = hap_func.value(hap_val.value);
|
||||
const hap = new Hap(new_whole, new_part, new_value, {
|
||||
...hap_func.context,
|
||||
...hap_val.context,
|
||||
locations: (hap_val.context.locations || []).concat(hap_func.context.locations || []),
|
||||
});
|
||||
const new_context = hap_val.combineContext(hap_func);
|
||||
const hap = new Hap(new_whole, new_part, new_value, new_context);
|
||||
haps.push(hap);
|
||||
}
|
||||
}
|
||||
@ -510,6 +299,23 @@ class Pattern {
|
||||
return this._asNumber().fmap((v) => Math.ceil(v));
|
||||
}
|
||||
|
||||
_toBipolar() {
|
||||
return this.fmap((x) => x * 2 - 1);
|
||||
}
|
||||
_fromBipolar() {
|
||||
return this.fmap((x) => (x + 1) / 2);
|
||||
}
|
||||
|
||||
// Assumes source pattern of numbers in range 0..1
|
||||
range(min, max) {
|
||||
return this.mul(max - min).add(min);
|
||||
}
|
||||
|
||||
// Assumes source pattern of numbers in range -1..1
|
||||
range2(min, max) {
|
||||
return this._fromBipolar().range(min, max);
|
||||
}
|
||||
|
||||
union(other) {
|
||||
return this._opleft(other, (a) => (b) => Object.assign({}, a, b));
|
||||
}
|
||||
@ -553,24 +359,61 @@ class Pattern {
|
||||
return this.bind(id);
|
||||
}
|
||||
|
||||
innerBind(func) {
|
||||
outerBind(func) {
|
||||
return this._bindWhole((a, _) => a, func);
|
||||
}
|
||||
|
||||
outerJoin() {
|
||||
// Flattens a pattern of patterns into a pattern, where wholes are
|
||||
// taken from inner events.
|
||||
return this.outerBind(id);
|
||||
}
|
||||
|
||||
innerBind(func) {
|
||||
return this._bindWhole((_, b) => b, func);
|
||||
}
|
||||
|
||||
innerJoin() {
|
||||
// Flattens a pattern of patterns into a pattern, where wholes are
|
||||
// taken from inner events.
|
||||
return this.innerBind(id);
|
||||
}
|
||||
|
||||
outerBind(func) {
|
||||
return this._bindWhole((_, b) => b, func);
|
||||
_squeezeJoin() {
|
||||
const pat_of_pats = this;
|
||||
function query(state) {
|
||||
const haps = pat_of_pats.query(state);
|
||||
function flatHap(outerHap) {
|
||||
const pat = outerHap.value._compressSpan(outerHap.wholeOrPart().cycleArc());
|
||||
const innerHaps = pat.query(state.setSpan(outerHap.part));
|
||||
function munge(outer, inner) {
|
||||
let whole = undefined;
|
||||
if (inner.whole && outer.whole) {
|
||||
whole = inner.whole.intersection(outer.whole);
|
||||
if (!whole) {
|
||||
// The wholes are present, but don't intersect
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
const part = inner.part.intersection(outer.part);
|
||||
if (!part) {
|
||||
// The parts don't intersect
|
||||
return undefined;
|
||||
}
|
||||
const context = inner.combineContext(outer);
|
||||
return new Hap(whole, part, inner.value, context);
|
||||
}
|
||||
return innerHaps.map((innerHap) => munge(outerHap, innerHap));
|
||||
}
|
||||
const result = flatten(haps.map(flatHap));
|
||||
// remove undefineds
|
||||
return result.filter((x) => x);
|
||||
}
|
||||
return new Pattern(query);
|
||||
}
|
||||
|
||||
outerJoin() {
|
||||
// Flattens a pattern of patterns into a pattern, where wholes are
|
||||
// taken from inner events.
|
||||
return this.outerBind(id);
|
||||
_squeezeBind(func) {
|
||||
return this.fmap(func)._squeezeJoin();
|
||||
}
|
||||
|
||||
_apply(func) {
|
||||
@ -590,7 +433,7 @@ class Pattern {
|
||||
args = args.map((arg) => (isPattern(arg) ? arg.fmap((value) => value.value || value) : arg));
|
||||
const pat_arg = sequence(...args);
|
||||
// arg.locations has to go somewhere..
|
||||
return pat_arg.fmap((arg) => func.call(pat, arg)).outerJoin();
|
||||
return pat_arg.fmap((arg) => func.call(pat, arg)).innerJoin();
|
||||
};
|
||||
return patterned;
|
||||
}
|
||||
@ -616,15 +459,17 @@ class Pattern {
|
||||
return this.withQuerySpan(qf).withEventSpan(ef)._splitQueries();
|
||||
}
|
||||
|
||||
_compressSpan(span) {
|
||||
const b = span.begin;
|
||||
const e = span.end;
|
||||
_compress(b, e) {
|
||||
if (b > e || b > 1 || e > 1 || b < 0 || e < 0) {
|
||||
return silence;
|
||||
}
|
||||
return this._fastGap(Fraction(1).div(e.sub(b)))._late(b);
|
||||
}
|
||||
|
||||
_compressSpan(span) {
|
||||
return this._compress(span.begin, span.end);
|
||||
}
|
||||
|
||||
_fast(factor) {
|
||||
const fastQuery = this.withQueryTime((t) => t.mul(factor));
|
||||
return fastQuery.withEventTime((t) => t.div(factor));
|
||||
@ -634,6 +479,19 @@ class Pattern {
|
||||
return this._fast(Fraction(1).div(factor));
|
||||
}
|
||||
|
||||
_ply(factor) {
|
||||
return this.fmap((x) => pure(x)._fast(factor))._squeezeJoin();
|
||||
}
|
||||
|
||||
_chop(n) {
|
||||
const slices = Array.from({ length: n }, (x, i) => i);
|
||||
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
|
||||
const func = function (o) {
|
||||
return sequence(slice_objects.map((slice_o) => Object.assign({}, o, slice_o)));
|
||||
};
|
||||
return this._squeezeBind(func);
|
||||
}
|
||||
|
||||
// cpm = cycles per minute
|
||||
_cpm(cpm) {
|
||||
return this._fast(cpm / 60);
|
||||
@ -651,6 +509,28 @@ class Pattern {
|
||||
return this._early(Fraction(0).sub(offset));
|
||||
}
|
||||
|
||||
_zoom(s, e) {
|
||||
e = Fraction(e);
|
||||
s = Fraction(s);
|
||||
const d = e.sub(s);
|
||||
return this.withQuerySpan((span) => span.withCycle((t) => t.mul(d).add(s)))
|
||||
.withEventSpan((span) => span.withCycle((t) => t.sub(s).div(d)))
|
||||
._splitQueries();
|
||||
}
|
||||
|
||||
_zoomArc(a) {
|
||||
return this.zoom(a.begin, a.end);
|
||||
}
|
||||
|
||||
_linger(t) {
|
||||
if (t == 0) {
|
||||
return silence;
|
||||
} else if (t < 0) {
|
||||
return this._zoom(t.add(1), 1)._slow(t);
|
||||
}
|
||||
return this._zoom(0, t)._slow(t);
|
||||
}
|
||||
|
||||
struct(...binary_pats) {
|
||||
// Re structure the pattern according to a binary pattern (false values are dropped)
|
||||
const binary_pat = sequence(binary_pats);
|
||||
@ -731,7 +611,7 @@ class Pattern {
|
||||
return new Pattern(query)._splitQueries();
|
||||
}
|
||||
|
||||
jux(func, by = 1) {
|
||||
juxBy(by, func) {
|
||||
by /= 2;
|
||||
const elem_or = function (dict, key, dflt) {
|
||||
if (key in dict) {
|
||||
@ -745,6 +625,10 @@ class Pattern {
|
||||
return stack(left, func(right));
|
||||
}
|
||||
|
||||
_jux(func) {
|
||||
return this.juxBy(1, func);
|
||||
}
|
||||
|
||||
// is there a different name for those in tidal?
|
||||
stack(...pats) {
|
||||
return stack(this, ...pats);
|
||||
@ -758,7 +642,7 @@ class Pattern {
|
||||
}
|
||||
|
||||
stutWith(times, time, func) {
|
||||
return stack(...range(0, times - 1).map((i) => func(this.late(i * time), i)));
|
||||
return stack(...listRange(0, times - 1).map((i) => func(this.late(i * time), i)));
|
||||
}
|
||||
|
||||
stut(times, feedback, time) {
|
||||
@ -767,7 +651,7 @@ class Pattern {
|
||||
|
||||
// these might change with: https://github.com/tidalcycles/Tidal/issues/902
|
||||
_echoWith(times, time, func) {
|
||||
return stack(...range(0, times - 1).map((i) => func(this.late(i * time), i)));
|
||||
return stack(...listRange(0, times - 1).map((i) => func(this.late(i * time), i)));
|
||||
}
|
||||
|
||||
_echo(times, time, feedback) {
|
||||
@ -775,7 +659,7 @@ class Pattern {
|
||||
}
|
||||
|
||||
iter(times, back = false) {
|
||||
return slowcat(...range(0, times - 1).map((i) => (back ? this.late(i / times) : this.early(i / times))));
|
||||
return slowcat(...listRange(0, times - 1).map((i) => (back ? this.late(i / times) : this.early(i / times))));
|
||||
}
|
||||
|
||||
// known as iter' in tidalcycles
|
||||
@ -828,16 +712,20 @@ class Pattern {
|
||||
// methods of Pattern that get callable factories
|
||||
Pattern.prototype.patternified = [
|
||||
'apply',
|
||||
'fast',
|
||||
'slow',
|
||||
'cpm',
|
||||
'early',
|
||||
'late',
|
||||
'duration',
|
||||
'legato',
|
||||
'velocity',
|
||||
'segment',
|
||||
'chop',
|
||||
'color',
|
||||
'cpm',
|
||||
'duration',
|
||||
'early',
|
||||
'fast',
|
||||
'jux',
|
||||
'late',
|
||||
'legato',
|
||||
'linger',
|
||||
'ply',
|
||||
'segment',
|
||||
'slow',
|
||||
'velocity',
|
||||
];
|
||||
// methods that create patterns, which are added to patternified Pattern methods
|
||||
Pattern.prototype.factories = { pure, stack, slowcat, fastcat, cat, timeCat, sequence, polymeter, pm, polyrhythm, pr };
|
||||
@ -846,9 +734,9 @@ Pattern.prototype.factories = { pure, stack, slowcat, fastcat, cat, timeCat, seq
|
||||
// Elemental patterns
|
||||
|
||||
// Nothing
|
||||
const silence = new Pattern((_) => []);
|
||||
export const silence = new Pattern((_) => []);
|
||||
|
||||
function pure(value) {
|
||||
export function pure(value) {
|
||||
// A discrete value that repeats once per cycle
|
||||
function query(state) {
|
||||
return state.span.spanCycles.map((subspan) => new Hap(Fraction(subspan.begin).wholeCycle(), subspan, value));
|
||||
@ -856,43 +744,12 @@ function pure(value) {
|
||||
return new Pattern(query);
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const _toBipolar = (pat) => pat.fmap((x) => x * 2 - 1);
|
||||
const _fromBipolar = (pat) => pat.fmap((x) => (x + 1) / 2);
|
||||
|
||||
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||
export const sine = _fromBipolar(sine2);
|
||||
|
||||
export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||
export const cosine = sine._early(Fraction(1).div(4));
|
||||
|
||||
export const saw = signal((t) => t % 1);
|
||||
export const saw2 = _toBipolar(saw);
|
||||
|
||||
export const isaw = signal((t) => 1 - (t % 1));
|
||||
export const isaw2 = _toBipolar(isaw);
|
||||
|
||||
export const tri2 = fastcat(isaw2, saw2);
|
||||
export const tri = fastcat(isaw, saw);
|
||||
|
||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
||||
export const square2 = _toBipolar(square);
|
||||
|
||||
export function isPattern(thing) {
|
||||
// thing?.constructor?.name !== 'Pattern' // <- this will fail when code is mangled
|
||||
return thing instanceof Pattern;
|
||||
}
|
||||
|
||||
function reify(thing) {
|
||||
export function reify(thing) {
|
||||
// Turns something into a pattern, unless it's already a pattern
|
||||
if (isPattern(thing)) {
|
||||
return thing;
|
||||
@ -901,13 +758,13 @@ function reify(thing) {
|
||||
}
|
||||
// Basic functions for combining patterns
|
||||
|
||||
function stack(...pats) {
|
||||
export function stack(...pats) {
|
||||
const reified = pats.map((pat) => reify(pat));
|
||||
const query = (state) => flatten(reified.map((pat) => pat.query(state)));
|
||||
return new Pattern(query);
|
||||
}
|
||||
|
||||
function slowcat(...pats) {
|
||||
export function slowcat(...pats) {
|
||||
// Concatenation: combines a list of patterns, switching between them
|
||||
// successively, one per cycle.
|
||||
pats = pats.map(reify);
|
||||
@ -928,7 +785,7 @@ function slowcat(...pats) {
|
||||
return new Pattern(query)._splitQueries();
|
||||
}
|
||||
|
||||
function slowcatPrime(...pats) {
|
||||
export function slowcatPrime(...pats) {
|
||||
// Concatenation: combines a list of patterns, switching between them
|
||||
// successively, one per cycle. Unlike slowcat, this version will skip cycles.
|
||||
pats = pats.map(reify);
|
||||
@ -940,24 +797,24 @@ function slowcatPrime(...pats) {
|
||||
return new Pattern(query)._splitQueries();
|
||||
}
|
||||
|
||||
function fastcat(...pats) {
|
||||
export function fastcat(...pats) {
|
||||
// Concatenation: as with slowcat, but squashes a cycle from each
|
||||
// pattern into one cycle
|
||||
return slowcat(...pats)._fast(pats.length);
|
||||
}
|
||||
|
||||
function cat(...pats) {
|
||||
export function cat(...pats) {
|
||||
return fastcat(...pats);
|
||||
}
|
||||
|
||||
function timeCat(...timepats) {
|
||||
export function timeCat(...timepats) {
|
||||
// Like cat, but where each step has a temporal 'weight'
|
||||
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
|
||||
let begin = Fraction(0);
|
||||
const pats = [];
|
||||
for (const [time, pat] of timepats) {
|
||||
const end = begin.add(time);
|
||||
pats.push(reify(pat)._compressSpan(new TimeSpan(begin.div(total), end.div(total))));
|
||||
pats.push(reify(pat)._compress(begin.div(total), end.div(total)));
|
||||
begin = end;
|
||||
}
|
||||
return stack(...pats);
|
||||
@ -976,11 +833,11 @@ function _sequenceCount(x) {
|
||||
return [reify(x), 1];
|
||||
}
|
||||
|
||||
function sequence(...xs) {
|
||||
export function sequence(...xs) {
|
||||
return _sequenceCount(xs)[0];
|
||||
}
|
||||
|
||||
function polymeter(steps = 0, ...args) {
|
||||
export function polymeterSteps(steps, ...args) {
|
||||
const seqs = args.map((a) => _sequenceCount(a));
|
||||
if (seqs.length == 0) {
|
||||
return silence;
|
||||
@ -999,15 +856,19 @@ function polymeter(steps = 0, ...args) {
|
||||
pats.push(seq[0]._fast(Fraction(steps).div(Fraction(seq[1]))));
|
||||
}
|
||||
}
|
||||
return stack(pats);
|
||||
return stack(...pats);
|
||||
}
|
||||
|
||||
export function polymeter(...args) {
|
||||
return polymeterSteps(0, ...args);
|
||||
}
|
||||
|
||||
// alias
|
||||
function pm(args) {
|
||||
polymeter(args);
|
||||
export function pm(...args) {
|
||||
polymeter(...args);
|
||||
}
|
||||
|
||||
function polyrhythm(...xs) {
|
||||
export function polyrhythm(...xs) {
|
||||
const seqs = xs.map((a) => sequence(a));
|
||||
|
||||
if (seqs.length == 0) {
|
||||
@ -1017,35 +878,40 @@ function polyrhythm(...xs) {
|
||||
}
|
||||
|
||||
// alias
|
||||
function pr(args) {
|
||||
export function pr(args) {
|
||||
polyrhythm(args);
|
||||
}
|
||||
|
||||
const fast = curry((a, pat) => pat.fast(a));
|
||||
const slow = curry((a, pat) => pat.slow(a));
|
||||
const early = curry((a, pat) => pat.early(a));
|
||||
const late = curry((a, pat) => pat.late(a));
|
||||
const rev = (pat) => pat.rev();
|
||||
const add = curry((a, pat) => pat.add(a));
|
||||
const sub = curry((a, pat) => pat.sub(a));
|
||||
const mul = curry((a, pat) => pat.mul(a));
|
||||
const div = curry((a, pat) => pat.div(a));
|
||||
const union = curry((a, pat) => pat.union(a));
|
||||
const every = curry((i, f, pat) => pat.every(i, f));
|
||||
const when = curry((binary, f, pat) => pat.when(binary, f));
|
||||
const off = curry((t, f, pat) => pat.off(t, f));
|
||||
const jux = curry((f, pat) => pat.jux(f));
|
||||
const append = curry((a, pat) => pat.append(a));
|
||||
const superimpose = curry((array, pat) => pat.superimpose(...array));
|
||||
const struct = curry((a, pat) => pat.struct(a));
|
||||
const mask = curry((a, pat) => pat.mask(a));
|
||||
const echo = curry((a, b, c, pat) => pat.echo(a, b, c));
|
||||
const invert = (pat) => pat.invert();
|
||||
const inv = (pat) => pat.inv();
|
||||
const iter = curry((a, pat) => pat.iter(a));
|
||||
const iterBack = curry((a, pat) => pat.iter(a));
|
||||
const chunk = curry((a, pat) => pat.chunk(a));
|
||||
const chunkBack = curry((a, pat) => pat.chunkBack(a));
|
||||
export const add = curry((a, pat) => pat.add(a));
|
||||
export const append = curry((a, pat) => pat.append(a));
|
||||
export const chunk = curry((a, pat) => pat.chunk(a));
|
||||
export const chunkBack = curry((a, pat) => pat.chunkBack(a));
|
||||
export const div = curry((a, pat) => pat.div(a));
|
||||
export const early = curry((a, pat) => pat.early(a));
|
||||
export const echo = curry((a, b, c, pat) => pat.echo(a, b, c));
|
||||
export const every = curry((i, f, pat) => pat.every(i, f));
|
||||
export const fast = curry((a, pat) => pat.fast(a));
|
||||
export const inv = (pat) => pat.inv();
|
||||
export const invert = (pat) => pat.invert();
|
||||
export const iter = curry((a, pat) => pat.iter(a));
|
||||
export const iterBack = curry((a, pat) => pat.iter(a));
|
||||
export const jux = curry((f, pat) => pat.jux(f));
|
||||
export const juxBy = curry((by, f, pat) => pat.juxBy(by, f));
|
||||
export const late = curry((a, pat) => pat.late(a));
|
||||
export const linger = curry((a, pat) => pat.linger(a));
|
||||
export const mask = curry((a, pat) => pat.mask(a));
|
||||
export const mul = curry((a, pat) => pat.mul(a));
|
||||
export const off = curry((t, f, pat) => pat.off(t, f));
|
||||
export const ply = curry((a, pat) => pat.ply(a));
|
||||
export const range = curry((a, b, pat) => pat.range(a, b));
|
||||
export const range2 = curry((a, b, pat) => pat.range2(a, b));
|
||||
export const rev = (pat) => pat.rev();
|
||||
export const slow = curry((a, pat) => pat.slow(a));
|
||||
export const struct = curry((a, pat) => pat.struct(a));
|
||||
export const sub = curry((a, pat) => pat.sub(a));
|
||||
export const superimpose = curry((array, pat) => pat.superimpose(...array));
|
||||
export const union = curry((a, pat) => pat.union(a));
|
||||
export const when = curry((binary, f, pat) => pat.when(binary, f));
|
||||
|
||||
// problem: curried functions with spread arguments must have pat at the beginning
|
||||
// with this, we cannot keep the pattern open at the end.. solution for now: use array to keep using pat as last arg
|
||||
@ -1069,24 +935,24 @@ export function makeComposable(func) {
|
||||
return func;
|
||||
}
|
||||
|
||||
const patternify2 = (f) => (pata, patb, pat) =>
|
||||
export const patternify2 = (f) => (pata, patb, pat) =>
|
||||
pata
|
||||
.fmap((a) => (b) => f.call(pat, a, b))
|
||||
.appLeft(patb)
|
||||
.outerJoin();
|
||||
const patternify3 = (f) => (pata, patb, patc, pat) =>
|
||||
.innerJoin();
|
||||
export const patternify3 = (f) => (pata, patb, patc, pat) =>
|
||||
pata
|
||||
.fmap((a) => (b) => (c) => f.call(pat, a, b, c))
|
||||
.appLeft(patb)
|
||||
.appLeft(patc)
|
||||
.outerJoin();
|
||||
const patternify4 = (f) => (pata, patb, patc, patd, pat) =>
|
||||
.innerJoin();
|
||||
export const patternify4 = (f) => (pata, patb, patc, patd, pat) =>
|
||||
pata
|
||||
.fmap((a) => (b) => (c) => (d) => f.call(pat, a, b, c, d))
|
||||
.appLeft(patb)
|
||||
.appLeft(patc)
|
||||
.appLeft(patd)
|
||||
.outerJoin();
|
||||
.innerJoin();
|
||||
|
||||
Pattern.prototype.echo = function (...args) {
|
||||
args = args.map(reify);
|
||||
@ -1104,6 +970,14 @@ Pattern.prototype.chunkBack = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._chunkBack)(...args, this);
|
||||
};
|
||||
Pattern.prototype.zoom = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._zoom)(...args, this);
|
||||
};
|
||||
Pattern.prototype.compress = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._compress)(...args, this);
|
||||
};
|
||||
|
||||
// call this after all Patter.prototype.define calls have been executed! (right before evaluate)
|
||||
Pattern.prototype.bootstrap = function () {
|
||||
@ -1156,50 +1030,3 @@ Pattern.prototype.define = (name, func, options = {}) => {
|
||||
// Pattern.prototype.define('early', (a, pat) => pat.early(a), { patternified: true, composable: true });
|
||||
Pattern.prototype.define('hush', (pat) => pat.hush(), { patternified: false, composable: true });
|
||||
Pattern.prototype.define('bypass', (pat) => pat.bypass(on), { patternified: true, composable: true });
|
||||
|
||||
export {
|
||||
Fraction,
|
||||
TimeSpan,
|
||||
Hap,
|
||||
Pattern,
|
||||
pure,
|
||||
stack,
|
||||
slowcat,
|
||||
fastcat,
|
||||
cat,
|
||||
timeCat,
|
||||
sequence,
|
||||
polymeter,
|
||||
pm,
|
||||
polyrhythm,
|
||||
pr,
|
||||
reify,
|
||||
silence,
|
||||
fast,
|
||||
slow,
|
||||
early,
|
||||
late,
|
||||
rev,
|
||||
add,
|
||||
sub,
|
||||
mul,
|
||||
div,
|
||||
union,
|
||||
every,
|
||||
when,
|
||||
off,
|
||||
jux,
|
||||
append,
|
||||
superimpose,
|
||||
struct,
|
||||
mask,
|
||||
invert,
|
||||
inv,
|
||||
id,
|
||||
range,
|
||||
echo,
|
||||
iter,
|
||||
iterBack,
|
||||
chunk,
|
||||
chunkBack,
|
||||
};
|
||||
30
packages/core/signal.mjs
Normal file
30
packages/core/signal.mjs
Normal file
@ -0,0 +1,30 @@
|
||||
import { Hap } from './hap.mjs';
|
||||
import { Pattern, fastcat } from './pattern.mjs';
|
||||
import Fraction from './fraction.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);
|
||||
33
packages/core/speak.mjs
Normal file
33
packages/core/speak.mjs
Normal 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
packages/core/state.mjs
Normal file
22
packages/core/state.mjs
Normal 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;
|
||||
@ -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(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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
packages/core/timespan.mjs
Normal file
103
packages/core/timespan.mjs
Normal 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,3 +42,44 @@ 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 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,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));
|
||||
|
||||
4
packages/eval/package-lock.json
generated
4
packages/eval/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"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",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"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.3",
|
||||
"estraverse": "^5.3.0",
|
||||
"shift-ast": "^6.1.0",
|
||||
"shift-codegen": "^7.0.3",
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
4
packages/midi/package-lock.json
generated
4
packages/midi/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"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",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"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.3",
|
||||
"@strudel.cycles/tone": "^0.0.4",
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^2.5.2"
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"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.3"
|
||||
"@strudel.cycles/eval": "^0.0.3",
|
||||
"@strudel.cycles/tone": "^0.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
37
packages/osc/README.md
Normal file
37
packages/osc/README.md
Normal 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
packages/osc/index.html
Normal file
22
packages/osc/index.html
Normal 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>
|
||||
1726
packages/osc/osc.js
Normal file
1726
packages/osc/osc.js
Normal file
File diff suppressed because it is too large
Load Diff
23
packages/osc/osc.mjs
Normal file
23
packages/osc/osc.mjs
Normal 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 });
|
||||
});
|
||||
};
|
||||
76
packages/osc/package-lock.json
generated
Normal file
76
packages/osc/package-lock.json
generated
Normal file
@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.0.1",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"osc-js": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic-ws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
|
||||
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
|
||||
"peerDependencies": {
|
||||
"ws": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/osc-js": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.0.tgz",
|
||||
"integrity": "sha512-P2Oy9tf8Z9lQw8JZeR62HNqbKdxj7Kqbsag+ImiJvyxPDReGMVt5LtZbMh/7Ve/wbYEGODkQdFAaLHFVkIlHPw==",
|
||||
"dependencies": {
|
||||
"isomorphic-ws": "4.0.1",
|
||||
"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": {
|
||||
"isomorphic-ws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
|
||||
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
|
||||
"requires": {}
|
||||
},
|
||||
"osc-js": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/osc-js/-/osc-js-2.3.0.tgz",
|
||||
"integrity": "sha512-P2Oy9tf8Z9lQw8JZeR62HNqbKdxj7Kqbsag+ImiJvyxPDReGMVt5LtZbMh/7Ve/wbYEGODkQdFAaLHFVkIlHPw==",
|
||||
"requires": {
|
||||
"isomorphic-ws": "4.0.1",
|
||||
"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
packages/osc/package.json
Normal file
32
packages/osc/package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.0.1",
|
||||
"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.0"
|
||||
}
|
||||
}
|
||||
30
packages/osc/server.js
Normal file
30
packages/osc/server.js
Normal 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
packages/osc/tidal-sniffer.js
Normal file
125
packages/osc/tidal-sniffer.js
Normal 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'
|
||||
]
|
||||
|
||||
*/
|
||||
5446
packages/tonal/package-lock.json
generated
5446
packages/tonal/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"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.3",
|
||||
"@tonaljs/tonal": "^4.6.5",
|
||||
"webmidi": "^3.0.15"
|
||||
}
|
||||
|
||||
@ -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,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,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
|
||||
|
||||
|
||||
4
packages/tone/package-lock.json
generated
4
packages/tone/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"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",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"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.3",
|
||||
"@tonejs/piano": "^0.2.1",
|
||||
"chord-voicings": "^0.0.1",
|
||||
"tone": "^14.7.77"
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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/
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/xen",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"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.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,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)) {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -668,3 +668,38 @@ 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()))`;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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"`} />
|
||||
|
||||
@ -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
|
||||
|
||||
@ -42,7 +42,7 @@ function useCycle(props) {
|
||||
?.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);
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user