mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 13:13:10 -04:00
Compare commits
289 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e99e53ff6d | |||
| 08cd319a0b | |||
| 896ec69329 | |||
| 32e64bbb6e | |||
| e06963e616 | |||
| a675815529 | |||
| 3d48135b32 | |||
| 8f09e7f097 | |||
| e8b09f1124 | |||
| 57ba353594 | |||
| b695e090cd | |||
| 69ecb7b54f | |||
| f57fe182ed | |||
| 10ee11c886 | |||
| 45842b1f05 | |||
| 9ca5f9ad48 | |||
| bb00036a30 | |||
| 4a2a540500 | |||
| 09217216dc | |||
| 22a64c4931 | |||
| 93f520b941 | |||
| c84714dc3a | |||
| a7aa09a13e | |||
| efa1cdfcb2 | |||
| 70695daae8 | |||
| 62fdba0600 | |||
| 5733e00908 | |||
| e56abaefd7 | |||
| c71e893e8e | |||
| 3992356517 | |||
| dbb84ef129 | |||
| 8781fa2e33 | |||
| 605a1365d3 | |||
| ebb1bbc966 | |||
| d4518f7a6e | |||
| 68c9008019 | |||
| cafde1e910 | |||
| 41f5f3fe6e | |||
| cf12ecfa34 | |||
| 4fa668927f | |||
| 24d2b5b4bd | |||
| 4da2e003e1 | |||
| 205debde9c | |||
| ba6c7b0901 | |||
| 8e5d02fd25 | |||
| 11d3251c67 | |||
| 9ec5787b4d | |||
| 027c669ad7 | |||
| 405c12ba3f | |||
| 64fd53a8e5 | |||
| 069d63e3cf | |||
| 66049e0e25 | |||
| db7ef01dbb | |||
| 1a6b649fd6 | |||
| 548619775e | |||
| 4b79a75f39 | |||
| 28912fff97 | |||
| 98a83ddcc0 | |||
| e9d965ce7a | |||
| b7624813cc | |||
| 734e451bd9 | |||
| 7a96436085 | |||
| 670af8fb1b | |||
| 0abd062e5c | |||
| 1286468d5a | |||
| 0f3da5be4a | |||
| 9170269e06 | |||
| 8bb460701f | |||
| c0a7173ca4 | |||
| 949e15d8c6 | |||
| 0caedeb481 | |||
| 83ca8d95be | |||
| 8483868783 | |||
| 6961388545 | |||
| 8ef1ae5202 | |||
| 26b8f93cea | |||
| a576523976 | |||
| cf21c4085b | |||
| 204f655d6d | |||
| fb14865785 | |||
| 5cd052cc59 | |||
| af49c4117c | |||
| dd736130a0 | |||
| cee8bda676 | |||
| 6420a72ceb | |||
| 380e7c7899 | |||
| 16b4058a8b | |||
| e3fba0257f | |||
| cb8b65f6f3 | |||
| 3944a8ed54 | |||
| 2ceafe6379 | |||
| 9f058bd88a | |||
| 1dd0ff13d8 | |||
| 6061f10230 | |||
| ea59fafb52 | |||
| 2ff71126de | |||
| 6d522e1743 | |||
| b37f3c819d | |||
| 1b41994e5f | |||
| 4fd4b0d713 | |||
| 7b43ed702e | |||
| 56cd77942b | |||
| 37dc723b4b | |||
| d7ea37c917 | |||
| c1d2bf9b9f | |||
| 36f837730a | |||
| b8f0a1dd82 | |||
| 2d1b62a978 | |||
| 0792d0d59d | |||
| 8c6da1a658 | |||
| 26be10f937 | |||
| ef4d9a13ed | |||
| 320d987ede | |||
| 51a4c4d13b | |||
| aaf3ae5f9c | |||
| 858f4f74b3 | |||
| 3feed90039 | |||
| b46330fc7a | |||
| 5ca7ce1f75 | |||
| d1d9b37ec7 | |||
| 823115c4ec | |||
| 8fe3fda27f | |||
| 63fffd7493 | |||
| 243a350e2b | |||
| 8c09e7ba36 | |||
| d89502f91f | |||
| 847602f933 | |||
| 8fcf8fe39c | |||
| d59eec9296 | |||
| b9122ebc34 | |||
| 38e066ee49 | |||
| 232a04a1a2 | |||
| dcb145635c | |||
| c9b73c68a6 | |||
| 08f7925ea9 | |||
| ed79ec7940 | |||
| 869a44dd04 | |||
| 6a9e9ebc4c | |||
| f18a2a11fa | |||
| 9aaaa755c7 | |||
| 818cd9044b | |||
| 006ca12b6d | |||
| b5f9ab7c20 | |||
| 0981f95f16 | |||
| f64ef32eaa | |||
| 7a19659c9e | |||
| b1202fcd55 | |||
| b8f991eddb | |||
| d0013f55a2 | |||
| 7efaff4f6f | |||
| bc7a84e462 | |||
| 61657bd1b1 | |||
| fc63f4f89c | |||
| 2c44c252d7 | |||
| 613fc6e3ef | |||
| 0a9604d10e | |||
| 1cabedc65e | |||
| 4e5a0d9e02 | |||
| bf373d894d | |||
| e6bbad84c1 | |||
| 4de477d46c | |||
| 264cab3c9b | |||
| 7f9ef59bea | |||
| 8ebbef4cbb | |||
| 0c98a84242 | |||
| 2bbdd21b2f | |||
| 794cfe949b | |||
| 6a5010a2d6 | |||
| 61559fee9d | |||
| 2118a20408 | |||
| b0bc20cb83 | |||
| 2cd77b075b | |||
| 03450c6085 | |||
| 5d3c522da2 | |||
| bf1ba6efc0 | |||
| 67ad82d52e | |||
| f437d976f8 | |||
| c94443faaa | |||
| c6d57f6a56 | |||
| 09b06da403 | |||
| df73ce8a60 | |||
| 8b22c2e04f | |||
| b4f4d6cea2 | |||
| cdf7b2acf1 | |||
| 94651731a5 | |||
| 56424d29a3 | |||
| c5083b2aef | |||
| 1b06795ffe | |||
| 6679cd60ce | |||
| 843a3c16ba | |||
| df40204a57 | |||
| 210910eb6e | |||
| ef427758da | |||
| def6bbf683 | |||
| 5f37a4a21c | |||
| dc869987f9 | |||
| b98d2201e5 | |||
| 8dd6440bb8 | |||
| a2d83c48eb | |||
| a0ae8f4a07 | |||
| 719dcbcd4d | |||
| caa207e868 | |||
| 306004ecd2 | |||
| bcac3c5b84 | |||
| ccc90108df | |||
| b9781915a4 | |||
| 1890e65b75 | |||
| 2f4da3ed52 | |||
| 27a6ac0c0b | |||
| b81a66f07e | |||
| bc43fc9585 | |||
| 4f7c013c78 | |||
| 09b15a07c6 | |||
| 4340f024d3 | |||
| 98e9780819 | |||
| a45d63595f | |||
| 5fbd1453b4 | |||
| f827201ee0 | |||
| 485a96962a | |||
| 98a104f731 | |||
| ff987f14d7 | |||
| 4dd08268dc | |||
| c18a13d4fc | |||
| 7a79995727 | |||
| 01eca562c5 | |||
| f491d0c0c4 | |||
| edf2287e13 | |||
| fcaab633c1 | |||
| 33b6579b5d | |||
| 72a8e4bc22 | |||
| c67b02b7b0 | |||
| 486cd00384 | |||
| caea7bb39d | |||
| e8fb7ee683 | |||
| e4ee4211c8 | |||
| 3fa3ebee14 | |||
| 8e531f400e | |||
| 5727c0dc8a | |||
| f599088cdf | |||
| d78da55f54 | |||
| 9052ff71e6 | |||
| 713da887ca | |||
| ad5950697e | |||
| 85d0d06cab | |||
| be6ec558e3 | |||
| dd2f60810a | |||
| 6c0b86d8f4 | |||
| 517ada6dbf | |||
| 0a098e3008 | |||
| c40078ffc9 | |||
| 729e0afdbe | |||
| eab86840d3 | |||
| 43aa2247f1 | |||
| afe17a507e | |||
| e621020474 | |||
| ac2aa7f288 | |||
| 805bd7e930 | |||
| f9bf2122b7 | |||
| 4e3e533e13 | |||
| 611c429912 | |||
| e2898ee5bf | |||
| e1a532500e | |||
| 4cf412b93d | |||
| 54c9c434e0 | |||
| de19f3e5fe | |||
| 8304993481 | |||
| 4c838aeaca | |||
| 4ec1145dce | |||
| 547d925065 | |||
| 874633051b | |||
| 2293f1ba15 | |||
| 483c3f61b5 | |||
| 83263968f2 | |||
| f279c61792 | |||
| 9717c696d8 | |||
| 94a594c777 | |||
| f1ae8a17cf | |||
| add9c43827 | |||
| d3f6a5c296 | |||
| 4aea78b7c0 | |||
| 4313fe670d | |||
| 2e8a1b67ea | |||
| e73044c533 | |||
| 418f09e364 | |||
| 08f207a396 | |||
| 64275cfd22 | |||
| f0b099d9f7 | |||
| ad3a324719 | |||
| 8c9e178206 |
@@ -0,0 +1,17 @@
|
|||||||
|
krill-parser.js
|
||||||
|
krill.pegjs
|
||||||
|
.eslintrc.json
|
||||||
|
server.js
|
||||||
|
tidal-sniffer.js
|
||||||
|
*.jsx
|
||||||
|
tunejs.js
|
||||||
|
out/**
|
||||||
|
postcss.config.js
|
||||||
|
postcss.config.cjs
|
||||||
|
tailwind.config.js
|
||||||
|
tailwind.config.cjs
|
||||||
|
vite.config.js
|
||||||
|
/**/dist/**/*
|
||||||
|
!**/*.mjs
|
||||||
|
**/*.tsx
|
||||||
|
**/*.ts
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": ["eslint:recommended"],
|
||||||
|
"overrides": [],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [],
|
||||||
|
"rules": {
|
||||||
|
"no-unused-vars": ["warn", { "destructuredArrayIgnorePattern": ".", "ignoreRestSiblings": false }]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,10 +24,10 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci && cd repl && npm ci && cd ../tutorial && npm ci
|
run: npm ci && cd website && npm ci
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
uses: actions/upload-pages-artifact@v1
|
uses: actions/upload-pages-artifact@v1
|
||||||
with:
|
with:
|
||||||
# Upload entire repository
|
# Upload entire repository
|
||||||
path: "./out"
|
path: "./website/dist"
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ jobs:
|
|||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'npm'
|
cache: "npm"
|
||||||
- run: npm install
|
- run: npm install
|
||||||
|
- run: npm run format-check
|
||||||
|
- run: npm run lint
|
||||||
- run: npm test
|
- run: npm test
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
*.md
|
||||||
|
*.json
|
||||||
|
*.yml
|
||||||
|
*.
|
||||||
|
**/out
|
||||||
|
**/dist
|
||||||
|
packages/mini/krill-parser.js
|
||||||
|
packages/xen/tunejs.js
|
||||||
|
paper
|
||||||
@@ -13,4 +13,3 @@
|
|||||||
"htmlWhitespaceSensitivity": "css",
|
"htmlWhitespaceSensitivity": "css",
|
||||||
"endOfLine": "lf"
|
"endOfLine": "lf"
|
||||||
}
|
}
|
||||||
|
|
||||||
Vendored
+2
-1
@@ -5,5 +5,6 @@
|
|||||||
],
|
],
|
||||||
"yaml.schemas": {
|
"yaml.schemas": {
|
||||||
"https://json.schemastore.org/github-workflow.json": "file:///home/felix/projects/strudel/.github/workflows/deploy.yml"
|
"https://json.schemastore.org/github-workflow.json": "file:///home/felix/projects/strudel/.github/workflows/deploy.yml"
|
||||||
}
|
},
|
||||||
|
"testing.automaticallyOpenPeekView": "never"
|
||||||
}
|
}
|
||||||
+5
-7
@@ -12,8 +12,8 @@ To get in touch with the contributors, either
|
|||||||
|
|
||||||
## Ask a Question
|
## Ask a Question
|
||||||
|
|
||||||
If you have any questions about strudel, make sure you've read the
|
If you have any questions about strudel, make sure you've glanced through the
|
||||||
[tutorial](https://strudel.tidalcycles.org/tutorial/) to find out if it answers your question.
|
[docs](https://strudel.tidalcycles.org/learn/) to find out if it answers your question.
|
||||||
If not, use one of the Communication Channels above!
|
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.
|
Don't be afraid to ask! Your question might be of great value for other people too.
|
||||||
@@ -29,12 +29,10 @@ If you made some music with strudel, you can give back some love and share what
|
|||||||
Your creation could also be part of the random selection in the REPL if you want.
|
Your creation could also be part of the random selection in the REPL if you want.
|
||||||
Use one of the Communication Channels listed above.
|
Use one of the Communication Channels listed above.
|
||||||
|
|
||||||
## Improve the Tutorial
|
## Improve the Docs
|
||||||
|
|
||||||
If you find some weak spots in the [tutorial](https://strudel.tidalcycles.org/),
|
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/learn/getting-started),
|
||||||
you are welcome to improve them by editing [this file](https://github.com/tidalcycles/strudel/blob/main/tutorial/tutorial.mdx).
|
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
|
||||||
|
|
||||||
This will even work without setting up a development environment, only a github account is required.
|
|
||||||
|
|
||||||
## Propose a Feature
|
## Propose a Feature
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is slowly stabilising, but please continue to tread carefully.
|
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is slowly stabilising, but please continue to tread carefully.
|
||||||
|
|
||||||
- Try it here: <https://strudel.tidalcycles.org/>
|
- Try it here: <https://strudel.tidalcycles.org/>
|
||||||
- Tutorial: <https://strudel.tidalcycles.org/tutorial/>
|
- Docs: <https://strudel.tidalcycles.org/learn/>
|
||||||
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
||||||
|
|
||||||
## Running Locally
|
## Running Locally
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# my-patterns
|
||||||
|
|
||||||
|
This directory can be used to save your own patterns, which then get
|
||||||
|
made into a pattern swatch.
|
||||||
|
|
||||||
|
0. fork and clone the strudel repository
|
||||||
|
1. run `npm run setup` in the strudel folder
|
||||||
|
1. Save one or more .txt files in this folder
|
||||||
|
2. run `npm run repl` in the top-level strudel folder
|
||||||
|
3. open `http://localhost:3000/swatch/` !
|
||||||
|
|
||||||
|
## deploy
|
||||||
|
|
||||||
|
1. in your fork, go to settings -> pages and select "Github Actions" as source
|
||||||
|
2. edit `website/public/CNAME` to contain `<your-username>.github.io/strudel`
|
||||||
|
3. edit `website/astro.config.mjs` to use site: `https://<your-username>.github.io` and base `/strudel`
|
||||||
|
4. go to Actions -> `Build and Deploy` and click `Run workflow`
|
||||||
|
5. view your patterns at `<your-username>.github.io/strudel/swatch/`
|
||||||
|
|
||||||
|
Alternatively, github pages allows you to use a custom domain, like https://mycooldomain.org/swatch/. [See their documentation for details](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site).
|
||||||
Generated
+3686
-5419
File diff suppressed because it is too large
Load Diff
+19
-10
@@ -1,23 +1,29 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/monorepo",
|
"name": "@strudel.cycles/monorepo",
|
||||||
"version": "0.0.4",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Port of tidalcycles to javascript",
|
"description": "Port of tidalcycles to javascript",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"pretest": "cd tutorial && npm run jsdoc-json",
|
"pretest": "npm run jsdoc-json",
|
||||||
|
"prebuild": "npm run jsdoc-json",
|
||||||
"test": "vitest run --version",
|
"test": "vitest run --version",
|
||||||
"test-ui": "vitest --ui",
|
"test-ui": "vitest --ui",
|
||||||
"test-coverage": "vitest --coverage",
|
"test-coverage": "vitest --coverage",
|
||||||
"bootstrap": "lerna bootstrap",
|
"bootstrap": "lerna bootstrap",
|
||||||
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
|
"setup": "npm i && npm run jsdoc-json && npm run bootstrap && cd website && npm i",
|
||||||
"snapshot": "cd repl && npm run snapshot",
|
"snapshot": "vitest run -u --silent",
|
||||||
"repl": "cd repl && npm run dev",
|
"repl": "cd website && npm run dev",
|
||||||
"osc": "cd packages/osc && npm run server",
|
"osc": "cd packages/osc && npm run server",
|
||||||
"build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build",
|
"build": "cd website && npm run build",
|
||||||
"preview": "npx serve ./out",
|
"preview": "cd website && npm run preview",
|
||||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
|
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
|
||||||
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
||||||
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json"
|
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
|
||||||
|
"lint": "eslint . --ext mjs,js --quiet",
|
||||||
|
"codeformat": "prettier --write .",
|
||||||
|
"format-check": "prettier --check .",
|
||||||
|
"check": "npm run format-check && npm run lint && npm run test",
|
||||||
|
"iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@@ -40,15 +46,18 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"homepage": "https://strudel.tidalcycles.org",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitest/ui": "^0.21.1",
|
"@vitest/ui": "^0.25.7",
|
||||||
"c8": "^7.12.0",
|
"c8": "^7.12.0",
|
||||||
|
"canvas": "^2.11.0",
|
||||||
|
"eslint": "^8.28.0",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"gh-pages": "^4.0.0",
|
"gh-pages": "^4.0.0",
|
||||||
"jsdoc": "^3.6.10",
|
"jsdoc": "^3.6.10",
|
||||||
"jsdoc-json": "^2.0.2",
|
"jsdoc-json": "^2.0.2",
|
||||||
"jsdoc-to-markdown": "^7.1.1",
|
"jsdoc-to-markdown": "^7.1.1",
|
||||||
"lerna": "^4.0.0",
|
"lerna": "^4.0.0",
|
||||||
|
"prettier": "^2.8.1",
|
||||||
"rollup-plugin-visualizer": "^5.8.1",
|
"rollup-plugin-visualizer": "^5.8.1",
|
||||||
"vitest": "^0.21.1"
|
"vitest": "^0.25.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
||||||
|
const { createParams } = controls;
|
||||||
|
|
||||||
|
let clearColor = '#22222210';
|
||||||
|
|
||||||
|
Pattern.prototype.animate = function ({ callback, sync = false, smear = 0.5 } = {}) {
|
||||||
|
window.frame && cancelAnimationFrame(window.frame);
|
||||||
|
const ctx = getDrawContext();
|
||||||
|
const { clientWidth: ww, clientHeight: wh } = ctx.canvas;
|
||||||
|
let smearPart = smear === 0 ? '99' : Number((1 - smear) * 100).toFixed(0);
|
||||||
|
smearPart = smearPart.length === 1 ? `0${smearPart}` : smearPart;
|
||||||
|
clearColor = `#200010${smearPart}`;
|
||||||
|
const render = (t) => {
|
||||||
|
let frame;
|
||||||
|
/* if (sync) {
|
||||||
|
t = scheduler.now();
|
||||||
|
frame = this.queryArc(t, t);
|
||||||
|
} else { */
|
||||||
|
t = Math.round(t);
|
||||||
|
frame = this.slow(1000).queryArc(t, t);
|
||||||
|
// }
|
||||||
|
ctx.fillStyle = clearColor;
|
||||||
|
ctx.fillRect(0, 0, ww, wh);
|
||||||
|
frame.forEach((f) => {
|
||||||
|
let { x, y, w, h, s, r, a = 0, fill = 'darkseagreen' } = f.value;
|
||||||
|
w *= ww;
|
||||||
|
h *= wh;
|
||||||
|
if (r !== undefined && a !== undefined) {
|
||||||
|
const radians = a * 2 * Math.PI;
|
||||||
|
const [cx, cy] = [(ww - w) / 2, (wh - h) / 2];
|
||||||
|
x = cx + Math.cos(radians) * r * cx;
|
||||||
|
y = cy + Math.sin(radians) * r * cy;
|
||||||
|
} else {
|
||||||
|
x *= ww - w;
|
||||||
|
y *= wh - h;
|
||||||
|
}
|
||||||
|
const val = { ...f.value, x, y, w, h };
|
||||||
|
ctx.fillStyle = fill;
|
||||||
|
if (s === 'rect') {
|
||||||
|
ctx.fillRect(x, y, w, h);
|
||||||
|
} else if (s === 'ellipse') {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(x + w / 2, y + h / 2, w / 2, h / 2, 0, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
callback && callback(ctx, val, f);
|
||||||
|
});
|
||||||
|
window.frame = requestAnimationFrame(render);
|
||||||
|
};
|
||||||
|
window.frame = requestAnimationFrame(render);
|
||||||
|
return silence;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const { x, y, w, h, a, r, fill, smear } = createParams('x', 'y', 'w', 'h', 'a', 'r', 'fill', 'smear');
|
||||||
|
|
||||||
|
export const rescale = register('rescale', function (f, pat) {
|
||||||
|
return pat.mul(x(f).w(f).y(f).h(f));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const moveXY = register('moveXY', function (dx, dy, pat) {
|
||||||
|
return pat.add(x(dx).y(dy));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const zoomIn = register('zoomIn', function (f, pat) {
|
||||||
|
const d = pure(1).sub(f).div(2);
|
||||||
|
return pat.rescale(f).move(d, d);
|
||||||
|
});
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
export const colorMap = {
|
||||||
|
aliceblue: '#f0f8ff',
|
||||||
|
antiquewhite: '#faebd7',
|
||||||
|
aqua: '#00ffff',
|
||||||
|
aquamarine: '#7fffd4',
|
||||||
|
azure: '#f0ffff',
|
||||||
|
beige: '#f5f5dc',
|
||||||
|
bisque: '#ffe4c4',
|
||||||
|
black: '#000000',
|
||||||
|
blanchedalmond: '#ffebcd',
|
||||||
|
blue: '#0000ff',
|
||||||
|
blueviolet: '#8a2be2',
|
||||||
|
brown: '#a52a2a',
|
||||||
|
burlywood: '#deb887',
|
||||||
|
cadetblue: '#5f9ea0',
|
||||||
|
chartreuse: '#7fff00',
|
||||||
|
chocolate: '#d2691e',
|
||||||
|
coral: '#ff7f50',
|
||||||
|
cornflowerblue: '#6495ed',
|
||||||
|
cornsilk: '#fff8dc',
|
||||||
|
crimson: '#dc143c',
|
||||||
|
cyan: '#00ffff',
|
||||||
|
darkblue: '#00008b',
|
||||||
|
darkcyan: '#008b8b',
|
||||||
|
darkgoldenrod: '#b8860b',
|
||||||
|
darkgray: '#a9a9a9',
|
||||||
|
darkgreen: '#006400',
|
||||||
|
darkgrey: '#a9a9a9',
|
||||||
|
darkkhaki: '#bdb76b',
|
||||||
|
darkmagenta: '#8b008b',
|
||||||
|
darkolivegreen: '#556b2f',
|
||||||
|
darkorange: '#ff8c00',
|
||||||
|
darkorchid: '#9932cc',
|
||||||
|
darkred: '#8b0000',
|
||||||
|
darksalmon: '#e9967a',
|
||||||
|
darkseagreen: '#8fbc8f',
|
||||||
|
darkslateblue: '#483d8b',
|
||||||
|
darkslategray: '#2f4f4f',
|
||||||
|
darkslategrey: '#2f4f4f',
|
||||||
|
darkturquoise: '#00ced1',
|
||||||
|
darkviolet: '#9400d3',
|
||||||
|
deeppink: '#ff1493',
|
||||||
|
deepskyblue: '#00bfff',
|
||||||
|
dimgray: '#696969',
|
||||||
|
dimgrey: '#696969',
|
||||||
|
dodgerblue: '#1e90ff',
|
||||||
|
firebrick: '#b22222',
|
||||||
|
floralwhite: '#fffaf0',
|
||||||
|
forestgreen: '#228b22',
|
||||||
|
fuchsia: '#ff00ff',
|
||||||
|
gainsboro: '#dcdcdc',
|
||||||
|
ghostwhite: '#f8f8ff',
|
||||||
|
gold: '#ffd700',
|
||||||
|
goldenrod: '#daa520',
|
||||||
|
gray: '#808080',
|
||||||
|
green: '#008000',
|
||||||
|
greenyellow: '#adff2f',
|
||||||
|
grey: '#808080',
|
||||||
|
honeydew: '#f0fff0',
|
||||||
|
hotpink: '#ff69b4',
|
||||||
|
indianred: '#cd5c5c',
|
||||||
|
indigo: '#4b0082',
|
||||||
|
ivory: '#fffff0',
|
||||||
|
khaki: '#f0e68c',
|
||||||
|
lavender: '#e6e6fa',
|
||||||
|
lavenderblush: '#fff0f5',
|
||||||
|
lawngreen: '#7cfc00',
|
||||||
|
lemonchiffon: '#fffacd',
|
||||||
|
lightblue: '#add8e6',
|
||||||
|
lightcoral: '#f08080',
|
||||||
|
lightcyan: '#e0ffff',
|
||||||
|
lightgoldenrodyellow: '#fafad2',
|
||||||
|
lightgray: '#d3d3d3',
|
||||||
|
lightgreen: '#90ee90',
|
||||||
|
lightgrey: '#d3d3d3',
|
||||||
|
lightpink: '#ffb6c1',
|
||||||
|
lightsalmon: '#ffa07a',
|
||||||
|
lightseagreen: '#20b2aa',
|
||||||
|
lightskyblue: '#87cefa',
|
||||||
|
lightslategray: '#778899',
|
||||||
|
lightslategrey: '#778899',
|
||||||
|
lightsteelblue: '#b0c4de',
|
||||||
|
lightyellow: '#ffffe0',
|
||||||
|
lime: '#00ff00',
|
||||||
|
limegreen: '#32cd32',
|
||||||
|
linen: '#faf0e6',
|
||||||
|
magenta: '#ff00ff',
|
||||||
|
maroon: '#800000',
|
||||||
|
mediumaquamarine: '#66cdaa',
|
||||||
|
mediumblue: '#0000cd',
|
||||||
|
mediumorchid: '#ba55d3',
|
||||||
|
mediumpurple: '#9370db',
|
||||||
|
mediumseagreen: '#3cb371',
|
||||||
|
mediumslateblue: '#7b68ee',
|
||||||
|
mediumspringgreen: '#00fa9a',
|
||||||
|
mediumturquoise: '#48d1cc',
|
||||||
|
mediumvioletred: '#c71585',
|
||||||
|
midnightblue: '#191970',
|
||||||
|
mintcream: '#f5fffa',
|
||||||
|
mistyrose: '#ffe4e1',
|
||||||
|
moccasin: '#ffe4b5',
|
||||||
|
navajowhite: '#ffdead',
|
||||||
|
navy: '#000080',
|
||||||
|
oldlace: '#fdf5e6',
|
||||||
|
olive: '#808000',
|
||||||
|
olivedrab: '#6b8e23',
|
||||||
|
orange: '#ffa500',
|
||||||
|
orangered: '#ff4500',
|
||||||
|
orchid: '#da70d6',
|
||||||
|
palegoldenrod: '#eee8aa',
|
||||||
|
palegreen: '#98fb98',
|
||||||
|
paleturquoise: '#afeeee',
|
||||||
|
palevioletred: '#db7093',
|
||||||
|
papayawhip: '#ffefd5',
|
||||||
|
peachpuff: '#ffdab9',
|
||||||
|
peru: '#cd853f',
|
||||||
|
pink: '#ffc0cb',
|
||||||
|
plum: '#dda0dd',
|
||||||
|
powderblue: '#b0e0e6',
|
||||||
|
purple: '#800080',
|
||||||
|
red: '#ff0000',
|
||||||
|
rosybrown: '#bc8f8f',
|
||||||
|
royalblue: '#4169e1',
|
||||||
|
saddlebrown: '#8b4513',
|
||||||
|
salmon: '#fa8072',
|
||||||
|
sandybrown: '#f4a460',
|
||||||
|
seagreen: '#2e8b57',
|
||||||
|
seashell: '#fff5ee',
|
||||||
|
sienna: '#a0522d',
|
||||||
|
silver: '#c0c0c0',
|
||||||
|
skyblue: '#87ceeb',
|
||||||
|
slateblue: '#6a5acd',
|
||||||
|
slategray: '#708090',
|
||||||
|
slategrey: '#708090',
|
||||||
|
snow: '#fffafa',
|
||||||
|
springgreen: '#00ff7f',
|
||||||
|
steelblue: '#4682b4',
|
||||||
|
tan: '#d2b48c',
|
||||||
|
teal: '#008080',
|
||||||
|
thistle: '#d8bfd8',
|
||||||
|
tomato: '#ff6347',
|
||||||
|
turquoise: '#40e0d0',
|
||||||
|
violet: '#ee82ee',
|
||||||
|
wheat: '#f5deb3',
|
||||||
|
white: '#ffffff',
|
||||||
|
whitesmoke: '#f5f5f5',
|
||||||
|
yellow: '#ffff00',
|
||||||
|
yellowgreen: '#9acd32',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function convertColorToNumber(color) {
|
||||||
|
// Convert color to lowercase for easier matching
|
||||||
|
color = color.toLowerCase();
|
||||||
|
|
||||||
|
// If the color is a hex code, convert it to a number
|
||||||
|
if (color[0] === '#') {
|
||||||
|
return convertHexToNumber(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the color is a named color, return the corresponding number
|
||||||
|
if (colorMap[color] !== undefined) {
|
||||||
|
return convertHexToNumber(colorMap[color]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the color is not recognized, return null
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertHexToNumber(hex) {
|
||||||
|
// Remove the leading '#' from the hex code
|
||||||
|
hex = hex.slice(1);
|
||||||
|
|
||||||
|
// Convert the hex code to a number
|
||||||
|
return parseInt(hex, 16);
|
||||||
|
}
|
||||||
+79
-35
@@ -51,6 +51,7 @@ const generic_params = [
|
|||||||
*
|
*
|
||||||
* @name accelerate
|
* @name accelerate
|
||||||
* @param {number | Pattern} amount acceleration.
|
* @param {number | Pattern} amount acceleration.
|
||||||
|
* @superdirtOnly
|
||||||
* @example
|
* @example
|
||||||
* s("sax").accelerate("<0 1 2 4 8 16>").slow(2).osc()
|
* s("sax").accelerate("<0 1 2 4 8 16>").slow(2).osc()
|
||||||
*
|
*
|
||||||
@@ -75,19 +76,20 @@ const generic_params = [
|
|||||||
*
|
*
|
||||||
* @name amp
|
* @name amp
|
||||||
* @param {number | Pattern} amount gain.
|
* @param {number | Pattern} amount gain.
|
||||||
|
* @superdirtOnly
|
||||||
* @example
|
* @example
|
||||||
* s("bd*8").amp(".1*2 .5 .1*2 .5 .1 .5").osc()
|
* s("bd*8").amp(".1*2 .5 .1*2 .5 .1 .5").osc()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'amp', 'like @gain@, but linear.'],
|
['f', 'amp', 'like @gain@, but linear.'],
|
||||||
// TODO: find out why 0 does not work, and it generally seems not right
|
/**
|
||||||
/*
|
|
||||||
* A pattern of numbers to specify the attack time of an envelope applied to each sample.
|
* A pattern of numbers to specify the attack time of an envelope applied to each sample.
|
||||||
|
* [More info about envelopes](/learn/synths-samples-effects/#envelope)
|
||||||
*
|
*
|
||||||
* @name attack
|
* @name attack
|
||||||
* @param {number | Pattern} attack time in seconds.
|
* @param {number | Pattern} attack time in seconds.
|
||||||
* @example
|
* @example
|
||||||
* n("c5 e5").s('superpiano').attack("<0 .1>").osc()
|
* note("c3 e3").attack("<0 .1 .5>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -107,18 +109,36 @@ const generic_params = [
|
|||||||
*/
|
*/
|
||||||
['f', 'bank', 'selects sound bank to use'],
|
['f', 'bank', 'selects sound bank to use'],
|
||||||
|
|
||||||
// TODO: find out how this works?
|
/**
|
||||||
/*
|
* Gain envelope decay time = the time it takes after the attack time to reach the sustain level.
|
||||||
* Envelope decay time = the time it takes after the attack time to reach the sustain level.
|
* [More info about envelopes](/learn/synths-samples-effects/#envelope)
|
||||||
*
|
*
|
||||||
* @name decay
|
* @name decay
|
||||||
* @param {number | Pattern} time decay time in seconds
|
* @param {number | Pattern} time decay time in seconds
|
||||||
* @example
|
* @example
|
||||||
* s("sax").cut(1).decay("<.1 .2 .3 .4>").sustain(0).osc()
|
* note("c3 e3").decay("<.1 .2 .3 .4>").sustain(0)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'decay', ''],
|
['f', 'decay', ''],
|
||||||
|
/**
|
||||||
|
* Gain envelope sustain level. [More info about envelopes](/learn/synths-samples-effects/#envelope)
|
||||||
|
*
|
||||||
|
* @name sustain
|
||||||
|
* @param {number | Pattern} gain sustain level between 0 and 1
|
||||||
|
* @example
|
||||||
|
* note("c3 e3").decay(.2).sustain("<0 .1 .4 .6 1>")
|
||||||
|
*
|
||||||
|
*/
|
||||||
['f', 'sustain', ''],
|
['f', 'sustain', ''],
|
||||||
|
/**
|
||||||
|
* Gain envelope release time. [More info about envelopes](/learn/synths-samples-effects/#envelope)
|
||||||
|
*
|
||||||
|
* @name release
|
||||||
|
* @param {number | Pattern} time release time in seconds
|
||||||
|
* @example
|
||||||
|
* note("c3 e3 g3 c4").release("<0 .1 .4 .6 1>/2")
|
||||||
|
*
|
||||||
|
*/
|
||||||
[
|
[
|
||||||
'f',
|
'f',
|
||||||
'release',
|
'release',
|
||||||
@@ -135,22 +155,26 @@ const generic_params = [
|
|||||||
*
|
*
|
||||||
* @name bandf
|
* @name bandf
|
||||||
* @param {number | Pattern} frequency center frequency
|
* @param {number | Pattern} frequency center frequency
|
||||||
|
* @synonyms bpf
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd,hh*3").bandf("<1000 2000 4000 8000>")
|
* s("bd sd,hh*3").bandf("<1000 2000 4000 8000>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'bandf', 'A pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
|
['f', 'bandf', 'A pattern of numbers from 0 to 1. Sets the center frequency of the band-pass filter.'],
|
||||||
|
['f', 'bpf', ''],
|
||||||
// TODO: in tidal, it seems to be normalized
|
// TODO: in tidal, it seems to be normalized
|
||||||
/**
|
/**
|
||||||
* Sets the q-factor of the band-pass filter
|
* Sets the q-factor of the band-pass filter
|
||||||
*
|
*
|
||||||
* @name bandq
|
* @name bandq
|
||||||
* @param {number | Pattern} q q factor
|
* @param {number | Pattern} q q factor
|
||||||
|
* @synonyms bpq
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd").bandf(500).bandq("<0 1 2 3>")
|
* s("bd sd").bandf(500).bandq("<0 1 2 3>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'bandq', 'a pattern of anumbers from 0 to 1. Sets the q-factor 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', 'bpq', ''],
|
||||||
/**
|
/**
|
||||||
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
|
||||||
*
|
*
|
||||||
@@ -204,7 +228,7 @@ const generic_params = [
|
|||||||
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
|
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'legato', 'controls the amount of overlap between two adjacent sounds'],
|
// ['f', 'legato', 'controls the amount of overlap between two adjacent sounds'],
|
||||||
// ['f', 'clhatdecay', ''],
|
// ['f', 'clhatdecay', ''],
|
||||||
/**
|
/**
|
||||||
* bit crusher effect.
|
* bit crusher effect.
|
||||||
@@ -249,7 +273,7 @@ const generic_params = [
|
|||||||
* @name cut
|
* @name cut
|
||||||
* @param {number | Pattern} group cut group number
|
* @param {number | Pattern} group cut group number
|
||||||
* @example
|
* @example
|
||||||
* s("bd sax").cut(1).osc()
|
* s("rd*4").cut(1)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -262,53 +286,50 @@ const generic_params = [
|
|||||||
*
|
*
|
||||||
* @name cutoff
|
* @name cutoff
|
||||||
* @param {number | Pattern} frequency audible between 0 and 20000
|
* @param {number | Pattern} frequency audible between 0 and 20000
|
||||||
|
* @synonyms lpf
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd,hh*3").cutoff("<4000 2000 1000 500 200 100>")
|
* s("bd sd,hh*3").cutoff("<4000 2000 1000 500 200 100>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
// TODO: add lpf synonym
|
|
||||||
['f', 'cutoff', 'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the low-pass filter.'],
|
['f', 'cutoff', 'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the low-pass filter.'],
|
||||||
|
['f', 'lpf'],
|
||||||
/**
|
/**
|
||||||
* Applies the cutoff frequency of the high-pass filter.
|
* Applies the cutoff frequency of the high-pass filter.
|
||||||
*
|
*
|
||||||
* @name hcutoff
|
* @name hcutoff
|
||||||
* @param {number | Pattern} frequency audible between 0 and 20000
|
* @param {number | Pattern} frequency audible between 0 and 20000
|
||||||
|
* @synonyms hpf
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd,hh*4").hcutoff("<4000 2000 1000 500 200 100>")
|
* s("bd sd,hh*4").hcutoff("<4000 2000 1000 500 200 100>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
// TODO: add hpf synonym
|
['f', 'hcutoff', ''],
|
||||||
[
|
['f', 'hpf', ''],
|
||||||
'f',
|
|
||||||
'hcutoff',
|
|
||||||
'a pattern of numbers from 0 to 1. Applies the cutoff frequency of the high-pass filter. Also has alias @hpf@',
|
|
||||||
],
|
|
||||||
/**
|
/**
|
||||||
* Applies the resonance of the high-pass filter.
|
* Applies the resonance of the high-pass filter.
|
||||||
*
|
*
|
||||||
* @name hresonance
|
* @name hresonance
|
||||||
* @param {number | Pattern} q resonance factor between 0 and 50
|
* @param {number | Pattern} q resonance factor between 0 and 50
|
||||||
|
* @synonyms hpq
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd,hh*4").hcutoff(2000).hresonance("<0 10 20 30>")
|
* s("bd sd,hh*4").hcutoff(2000).hresonance("<0 10 20 30>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
['f', 'hpq', ''],
|
||||||
'f',
|
['f', 'hresonance', ''],
|
||||||
'hresonance',
|
|
||||||
'a pattern of numbers from 0 to 1. Applies the resonance of the high-pass filter. Has alias @hpq@',
|
|
||||||
],
|
|
||||||
// TODO: add hpq synonym
|
// TODO: add hpq synonym
|
||||||
/**
|
/**
|
||||||
* Applies the cutoff frequency of the low-pass filter.
|
* Applies the cutoff frequency of the low-pass filter.
|
||||||
*
|
*
|
||||||
* @name resonance
|
* @name resonance
|
||||||
* @param {number | Pattern} q resonance factor between 0 and 50
|
* @param {number | Pattern} q resonance factor between 0 and 50
|
||||||
|
* @synonyms lpq
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd,hh*4").cutoff(2000).resonance("<0 10 20 30>")
|
* s("bd sd,hh*4").cutoff(2000).resonance("<0 10 20 30>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'resonance', 'a pattern of numbers from 0 to 1. Specifies the resonance of the low-pass filter.'],
|
['f', 'lpq'],
|
||||||
// TODO: add lpq synonym?
|
['f', 'resonance', ''],
|
||||||
/**
|
/**
|
||||||
* DJ filter, below 0.5 is low pass filter, above is high pass filter.
|
* DJ filter, below 0.5 is low pass filter, above is high pass filter.
|
||||||
*
|
*
|
||||||
@@ -321,17 +342,36 @@ const generic_params = [
|
|||||||
['f', 'djf', 'DJ filter, below 0.5 is low pass filter, above is high pass filter.'],
|
['f', 'djf', 'DJ filter, below 0.5 is low pass filter, above is high pass filter.'],
|
||||||
// ['f', 'cutoffegint', ''],
|
// ['f', 'cutoffegint', ''],
|
||||||
// TODO: does not seem to work
|
// TODO: does not seem to work
|
||||||
/*
|
/**
|
||||||
* Sets the level of the delay signal.
|
* Sets the level of the delay signal.
|
||||||
*
|
*
|
||||||
* @name delay
|
* @name delay
|
||||||
* @param {number | Pattern} level between 0 and 1
|
* @param {number | Pattern} level between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* s("bd").delay("<0 .5 .75 1>").osc()
|
* s("bd").delay("<0 .25 .5 1>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'delay', 'a pattern of numbers from 0 to 1. Sets the level of the delay signal.'],
|
['f', 'delay', 'a pattern of numbers from 0 to 1. Sets the level of the delay signal.'],
|
||||||
|
/**
|
||||||
|
* Sets the level of the signal that is fed back into the delay.
|
||||||
|
* Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it
|
||||||
|
*
|
||||||
|
* @name delayfeedback
|
||||||
|
* @param {number | Pattern} feedback between 0 and 1
|
||||||
|
* @example
|
||||||
|
* s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>").slow(2)
|
||||||
|
*
|
||||||
|
*/
|
||||||
['f', 'delayfeedback', 'a pattern of numbers from 0 to 1. Sets the amount of delay feedback.'],
|
['f', 'delayfeedback', 'a pattern of numbers from 0 to 1. Sets the amount of delay feedback.'],
|
||||||
|
/**
|
||||||
|
* Sets the time of the delay effect.
|
||||||
|
*
|
||||||
|
* @name delaytime
|
||||||
|
* @param {number | Pattern} seconds between 0 and Infinity
|
||||||
|
* @example
|
||||||
|
* s("bd").delay(.25).delaytime("<.125 .25 .5 1>").slow(2)
|
||||||
|
*
|
||||||
|
*/
|
||||||
['f', 'delaytime', 'a pattern of numbers from 0 to 1. Sets the length of the delay.'],
|
['f', 'delaytime', 'a pattern of numbers from 0 to 1. Sets the length of the delay.'],
|
||||||
/* // TODO: test
|
/* // TODO: test
|
||||||
* Specifies whether delaytime is calculated relative to cps.
|
* Specifies whether delaytime is calculated relative to cps.
|
||||||
@@ -400,7 +440,7 @@ const generic_params = [
|
|||||||
* @example
|
* @example
|
||||||
* freq("220 110 440 110").s("superzow").osc()
|
* freq("220 110 440 110").s("superzow").osc()
|
||||||
* @example
|
* @example
|
||||||
* freq("110".mulOut(".5 1.5 .6 [2 3]")).s("superzow").osc()
|
* freq("110".mul.out(".5 1.5 .6 [2 3]")).s("superzow").osc()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'freq', ''],
|
['f', 'freq', ''],
|
||||||
@@ -484,11 +524,15 @@ const generic_params = [
|
|||||||
// ['f', 'ophatdecay', ''],
|
// ['f', 'ophatdecay', ''],
|
||||||
// TODO: example
|
// TODO: example
|
||||||
/**
|
/**
|
||||||
* 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.
|
* An `orbit` is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.
|
||||||
*
|
*
|
||||||
* @name orbit
|
* @name orbit
|
||||||
* @param {number | Pattern} number
|
* @param {number | Pattern} number
|
||||||
*
|
* @example
|
||||||
|
* stack(
|
||||||
|
* s("hh*3").delay(.5).delaytime(.25).orbit(1),
|
||||||
|
* s("~ sd").delay(.5).delaytime(.125).orbit(2)
|
||||||
|
* )
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
'i',
|
'i',
|
||||||
@@ -562,7 +606,7 @@ const generic_params = [
|
|||||||
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
// TODO: detune? https://tidalcycles.org/docs/patternlib/tutorials/synthesizers/#supersquare
|
||||||
['f', 'semitone', ''],
|
['f', 'semitone', ''],
|
||||||
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
|
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
|
||||||
['f', 'velocity', ''],
|
// ['f', 'velocity', ''],
|
||||||
['f', 'voice', ''], // TODO: synth param
|
['f', 'voice', ''], // TODO: synth param
|
||||||
/**
|
/**
|
||||||
* Sets the level of reverb.
|
* Sets the level of reverb.
|
||||||
@@ -570,7 +614,7 @@ const generic_params = [
|
|||||||
* @name room
|
* @name room
|
||||||
* @param {number | Pattern} level between 0 and 1
|
* @param {number | Pattern} level between 0 and 1
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd").room("<0 .2 .4 .6 .8 1>").osc()
|
* s("bd sd").room("<0 .2 .4 .6 .8 1>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
['f', 'room', 'a pattern of numbers from 0 to 1. Sets the level of reverb.'],
|
['f', 'room', 'a pattern of numbers from 0 to 1. Sets the level of reverb.'],
|
||||||
@@ -578,9 +622,9 @@ const generic_params = [
|
|||||||
* Sets the room size of the reverb, see {@link room}.
|
* Sets the room size of the reverb, see {@link room}.
|
||||||
*
|
*
|
||||||
* @name size
|
* @name size
|
||||||
* @param {number | Pattern} size between 0 and 1
|
* @param {number | Pattern} size between 0 and 10
|
||||||
* @example
|
* @example
|
||||||
* s("bd sd").room(.8).size("<0 .2 .4 .6 .8 1>").osc()
|
* s("bd sd").room(.8).size("<0 1 2 4 8>")
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
// TODO: find out why :
|
// TODO: find out why :
|
||||||
@@ -620,9 +664,9 @@ const generic_params = [
|
|||||||
* @name speed
|
* @name speed
|
||||||
* @param {number | Pattern} speed -inf to inf, negative numbers play the sample backwards.
|
* @param {number | Pattern} speed -inf to inf, negative numbers play the sample backwards.
|
||||||
* @example
|
* @example
|
||||||
* s("bd").speed("<1 2 4 1 -2 -4>").osc()
|
* s("bd").speed("<1 2 4 1 -2 -4>")
|
||||||
* @example
|
* @example
|
||||||
* speed("1 1.5*2 [2 1.1]").s("sax").cut(1).osc()
|
* speed("1 1.5*2 [2 1.1]").s("piano").clip(1)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
[
|
[
|
||||||
@@ -787,6 +831,6 @@ controls.createParam = (name) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
controls.createParams = (...names) =>
|
controls.createParams = (...names) =>
|
||||||
names.reduce((acc, name) => Object.assign(acc, { [name]: createParam(name) }), {});
|
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
|
||||||
|
|
||||||
export default controls;
|
export default controls;
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export class Cyclist {
|
|||||||
getPhase() {
|
getPhase() {
|
||||||
return this.getTime() - this.origin - this.latency;
|
return this.getTime() - this.origin - this.latency;
|
||||||
}
|
}
|
||||||
|
now() {
|
||||||
|
return this.getTime() - this.origin + this.clock.minLatency;
|
||||||
|
}
|
||||||
setStarted(v) {
|
setStarted(v) {
|
||||||
this.started = v;
|
this.started = v;
|
||||||
this.onToggle?.(v);
|
this.onToggle?.(v);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, getTime } from './index.mjs';
|
import { Pattern, getTime, State, TimeSpan } from './index.mjs';
|
||||||
|
|
||||||
export const getDrawContext = (id = 'test-canvas') => {
|
export const getDrawContext = (id = 'test-canvas') => {
|
||||||
let canvas = document.querySelector('#' + id);
|
let canvas = document.querySelector('#' + id);
|
||||||
|
|||||||
+102
-29
@@ -1,47 +1,97 @@
|
|||||||
/*
|
/*
|
||||||
euclid.mjs - <short description TODO>
|
euclid.mjs - Bjorklund/Euclidean/Diaspora rhythms
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs>
|
Copyright (C) 2023 Rohan Drape and strudel contributors
|
||||||
|
|
||||||
|
See <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs> for authors of this file.
|
||||||
|
|
||||||
|
The Bjorklund algorithm implementation is ported from the Haskell Music Theory Haskell module by Rohan Drape -
|
||||||
|
https://rohandrape.net/?t=hmt
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, timeCat } from './pattern.mjs';
|
import { Pattern, timeCat, register, silence } from './pattern.mjs';
|
||||||
import bjork from 'bjork';
|
import { rotate, flatten } from './util.mjs';
|
||||||
import { rotate } from './util.mjs';
|
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
|
|
||||||
const euclid = (pulses, steps, rotation = 0) => {
|
const splitAt = function (index, value) {
|
||||||
const b = bjork(steps, pulses);
|
return [value.slice(0, index), value.slice(index)];
|
||||||
if (rotation) {
|
};
|
||||||
return rotate(b, -rotation);
|
|
||||||
}
|
const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||||
return b;
|
|
||||||
|
const left = function (n, x) {
|
||||||
|
const [ons, offs] = n;
|
||||||
|
const [xs, ys] = x;
|
||||||
|
const [_xs, __xs] = splitAt(offs, xs);
|
||||||
|
return [
|
||||||
|
[offs, ons - offs],
|
||||||
|
[zipWith((a, b) => a.concat(b), _xs, ys), __xs],
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const right = function (n, x) {
|
||||||
|
const [ons, offs] = n;
|
||||||
|
const [xs, ys] = x;
|
||||||
|
const [_ys, __ys] = splitAt(ons, ys);
|
||||||
|
const result = [
|
||||||
|
[ons, offs - ons],
|
||||||
|
[zipWith((a, b) => a.concat(b), xs, _ys), __ys],
|
||||||
|
];
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _bjork = function (n, x) {
|
||||||
|
const [ons, offs] = n;
|
||||||
|
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const bjork = function (ons, steps) {
|
||||||
|
const offs = steps - ons;
|
||||||
|
const x = Array(ons).fill([1]);
|
||||||
|
const y = Array(offs).fill([0]);
|
||||||
|
const result = _bjork([ons, offs], [x, y]);
|
||||||
|
return flatten(result[1][0]).concat(flatten(result[1][1]));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the structure of the pattern to form an euclidean rhythm.
|
* Changes the structure of the pattern to form an euclidean rhythm.
|
||||||
* Euclidian rhythms are rhythms obtained using the greatest common divisor of two numbers.
|
* Euclidian rhythms are rhythms obtained using the greatest common
|
||||||
* They were described in 2004 by Godfried Toussaint, a canadian computer scientist.
|
* divisor of two numbers. They were described in 2004 by Godfried
|
||||||
* Euclidian rhythms are really useful for computer/algorithmic music because they can accurately
|
* Toussaint, a canadian computer scientist. Euclidian rhythms are
|
||||||
* describe a large number of rhythms used in the most important music world traditions.
|
* really useful for computer/algorithmic music because they can
|
||||||
|
* describe a large number of rhythms with a couple of numbers.
|
||||||
*
|
*
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @name euclid
|
* @name euclid
|
||||||
* @param {number} pulses the number of onsets / beats
|
* @param {number} pulses the number of onsets / beats
|
||||||
* @param {number} steps the number of steps to fill
|
* @param {number} steps the number of steps to fill
|
||||||
* @param {number} rotation (optional) offset in steps
|
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* // The Cuban tresillo pattern.
|
* // The Cuban tresillo pattern.
|
||||||
* note("c3").euclid(3,8)
|
* note("c3").euclid(3,8)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence.
|
||||||
|
* @memberof Pattern
|
||||||
|
* @name euclidRot
|
||||||
|
* @param {number} pulses the number of onsets / beats
|
||||||
|
* @param {number} steps the number of steps to fill
|
||||||
|
* @param {number} rotation offset in steps
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* // A Samba rhythm necklace from Brazil
|
||||||
|
* note("c3").euclidRot(3,16,14)
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
|
* @example // A thirteenth century Persian rhythm called Khafif-e-ramal.
|
||||||
* note("c3").euclid(2,5)
|
* note("c3").euclid(2,5)
|
||||||
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
|
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
|
||||||
* note("c3").euclid(3,4)
|
* note("c3").euclid(3,4)
|
||||||
* @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm.
|
* @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm.
|
||||||
* note("c3").euclid(3,5,2)
|
* note("c3").euclidRot(3,5,2)
|
||||||
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
|
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
|
||||||
* note("c3").euclid(3,7)
|
* note("c3").euclid(3,7)
|
||||||
* @example // The Cuban tresillo pattern.
|
* @example // The Cuban tresillo pattern.
|
||||||
@@ -71,34 +121,57 @@ const euclid = (pulses, steps, rotation = 0) => {
|
|||||||
* @example // A common West African bell pattern.
|
* @example // A common West African bell pattern.
|
||||||
* note("c3").euclid(7,12)
|
* note("c3").euclid(7,12)
|
||||||
* @example // A Samba rhythm necklace from Brazil.
|
* @example // A Samba rhythm necklace from Brazil.
|
||||||
* note("c3").euclid(7,16,14)
|
* note("c3").euclidRot(7,16,14)
|
||||||
* @example // A rhythm necklace used in the Central African Republic.
|
* @example // A rhythm necklace used in the Central African Republic.
|
||||||
* note("c3").euclid(9,16)
|
* note("c3").euclid(9,16)
|
||||||
* @example // A rhythm necklace of the Aka Pygmies of Central Africa.
|
* @example // A rhythm necklace of the Aka Pygmies of Central Africa.
|
||||||
* note("c3").euclid(11,24,14)
|
* note("c3").euclidRot(11,24,14)
|
||||||
* @example // Another rhythm necklace of the Aka Pygmies of the upper Sangha.
|
* @example // Another rhythm necklace of the Aka Pygmies of the upper Sangha.
|
||||||
* note("c3").euclid(13,24,5)
|
* note("c3").euclidRot(13,24,5)
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.euclid = function (pulses, steps, rotation = 0) {
|
|
||||||
return this.struct(euclid(pulses, steps, rotation));
|
const _euclidRot = function (pulses, steps, rotation) {
|
||||||
|
const b = bjork(pulses, steps);
|
||||||
|
if (rotation) {
|
||||||
|
return rotate(b, -rotation);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const euclid = register('euclid', function (pulses, steps, pat) {
|
||||||
|
return pat.struct(_euclidRot(pulses, steps, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) {
|
||||||
|
return pat.struct(_euclidRot(pulses, steps, rotation));
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Similar to `.euclid`, but each pulse is held until the next pulse, so there will be no gaps.
|
* Similar to `euclid`, but each pulse is held until the next pulse,
|
||||||
|
* so there will be no gaps.
|
||||||
* @name euclidLegato
|
* @name euclidLegato
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @example
|
* @example
|
||||||
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
|
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.euclidLegato = function (pulses, steps, rotation = 0) {
|
|
||||||
const bin_pat = euclid(pulses, steps, rotation);
|
const _euclidLegato = function (pulses, steps, rotation, pat) {
|
||||||
const firstOne = bin_pat.indexOf(1);
|
if (pulses < 1) {
|
||||||
const gapless = rotate(bin_pat, firstOne)
|
return silence;
|
||||||
|
}
|
||||||
|
const bin_pat = _euclidRot(pulses, steps, rotation);
|
||||||
|
const gapless = bin_pat
|
||||||
.join('')
|
.join('')
|
||||||
.split('1')
|
.split('1')
|
||||||
.slice(1)
|
.slice(1)
|
||||||
.map((s) => [s.length + 1, true]);
|
.map((s) => [s.length + 1, true]);
|
||||||
return this.struct(timeCat(...gapless)).late(Fraction(firstOne).div(steps));
|
return pat.struct(timeCat(...gapless));
|
||||||
};
|
};
|
||||||
|
|
||||||
export default euclid;
|
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {
|
||||||
|
return _euclidLegato(pulses, steps, 0, pat);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
||||||
|
return _euclidLegato(pulses, steps, rotation, pat);
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isPattern, Pattern } from './index.mjs';
|
import { isPattern } from './index.mjs';
|
||||||
|
|
||||||
let scoped = false;
|
let scoped = false;
|
||||||
export const evalScope = async (...args) => {
|
export const evalScope = async (...args) => {
|
||||||
@@ -19,7 +19,14 @@ export const evalScope = async (...args) => {
|
|||||||
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
|
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap());
|
// Object.assign(globalThis, ...modules);
|
||||||
|
// below is a fix for above commented out line
|
||||||
|
// same error as https://github.com/vitest-dev/vitest/issues/1807 when running this on astro server
|
||||||
|
modules.forEach((module) => {
|
||||||
|
Object.entries(module).forEach(([name, value]) => {
|
||||||
|
globalThis[name] = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function safeEval(str, options = {}) {
|
function safeEval(str, options = {}) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ evalScope(
|
|||||||
import('@strudel.cycles/tonal'),
|
import('@strudel.cycles/tonal'),
|
||||||
);
|
);
|
||||||
|
|
||||||
setStringParser(mini)
|
setStringParser(mini);
|
||||||
|
|
||||||
const { evaluate } = repl({
|
const { evaluate } = repl({
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: webaudioOutput,
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Buildless Vanilla Strudel REPL</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; background: #222">
|
||||||
|
<div style="display: grid; height: 100vh">
|
||||||
|
<textarea
|
||||||
|
id="text"
|
||||||
|
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
|
||||||
|
spellcheck="false"
|
||||||
|
>
|
||||||
|
// LOADING</textarea
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="start"
|
||||||
|
style="
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 10px;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid white;
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
evaluate
|
||||||
|
</button>
|
||||||
|
<div id="output"></div>
|
||||||
|
<script type="module">
|
||||||
|
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.4.1';
|
||||||
|
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.4.1';
|
||||||
|
|
||||||
|
const modules = [
|
||||||
|
import('https://cdn.skypack.dev/@strudel.cycles/core@0.4.1'),
|
||||||
|
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.4.1'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const input = document.getElementById('text');
|
||||||
|
|
||||||
|
Promise.all(modules).then(() => {
|
||||||
|
input.innerHTML = `note("<c3 [d3 e3]>").cutoff(1000)`;
|
||||||
|
document.getElementById('start').addEventListener('click', () => {
|
||||||
|
evaluate(input.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
evalScope(controls, ...modules);
|
||||||
|
|
||||||
|
const { evaluate } = repl({
|
||||||
|
defaultOutput: (hap, deadline, duration) => {
|
||||||
|
console.log(deadline, duration, hap.value);
|
||||||
|
},
|
||||||
|
getTime: () => Date.now() / 1000,
|
||||||
|
transpiler,
|
||||||
|
beforeEval: (code) => console.log('evaluate', code),
|
||||||
|
afterEval: (code) => {},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -74,7 +74,7 @@ const fraction = (n) => {
|
|||||||
-> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
|
-> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
|
||||||
-> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
|
-> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
|
||||||
*/
|
*/
|
||||||
n = String(n);
|
// n = String(n); // this is actually faster but imprecise...
|
||||||
}
|
}
|
||||||
return Fraction(n);
|
return Fraction(n);
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-4
@@ -79,10 +79,33 @@ export class Hap {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show(compact = false) {
|
||||||
return (
|
const value =
|
||||||
'(' + (this.whole == undefined ? '~' : this.whole.show()) + ', ' + this.part.show() + ', ' + this.value + ')'
|
typeof this.value === 'object'
|
||||||
);
|
? compact
|
||||||
|
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||||
|
: JSON.stringify(this.value)
|
||||||
|
: this.value;
|
||||||
|
var spans = '';
|
||||||
|
if (this.whole == undefined) {
|
||||||
|
spans = '~' + this.part.show;
|
||||||
|
} else {
|
||||||
|
var is_whole = this.whole.begin.equals(this.part.begin) && this.whole.end.equals(this.part.end);
|
||||||
|
if (!this.whole.begin.equals(this.part.begin)) {
|
||||||
|
spans = this.whole.begin.show() + ' ⇜ ';
|
||||||
|
}
|
||||||
|
if (!is_whole) {
|
||||||
|
spans += '(';
|
||||||
|
}
|
||||||
|
spans += this.part.show();
|
||||||
|
if (!is_whole) {
|
||||||
|
spans += ')';
|
||||||
|
}
|
||||||
|
if (!this.whole.end.equals(this.part.end)) {
|
||||||
|
spans += ' ⇝ ' + this.whole.end.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '[ ' + spans + ' | ' + value + ' ]';
|
||||||
}
|
}
|
||||||
|
|
||||||
showWhole(compact = false) {
|
showWhole(compact = false) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export * from './repl.mjs';
|
|||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './time.mjs';
|
export * from './time.mjs';
|
||||||
export * from './draw.mjs';
|
export * from './draw.mjs';
|
||||||
|
export * from './animate.mjs';
|
||||||
export * from './pianoroll.mjs';
|
export * from './pianoroll.mjs';
|
||||||
export * from './ui.mjs';
|
export * from './ui.mjs';
|
||||||
export { default as drawLine } from './drawLine.mjs';
|
export { default as drawLine } from './drawLine.mjs';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export const logKey = 'strudel.log';
|
|||||||
|
|
||||||
export function logger(message, type, data = {}) {
|
export function logger(message, type, data = {}) {
|
||||||
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
||||||
if (typeof CustomEvent !== 'undefined') {
|
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
new CustomEvent(logKey, {
|
new CustomEvent(logKey, {
|
||||||
detail: {
|
detail: {
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/core",
|
"name": "@strudel.cycles/core",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/core",
|
"name": "@strudel.cycles/core",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"description": "Port of Tidal Cycles to JavaScript",
|
"description": "Port of Tidal Cycles to JavaScript",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"homepage": "https://strudel.tidalcycles.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bjork": "^0.0.1",
|
|
||||||
"fraction.js": "^4.2.0"
|
"fraction.js": "^4.2.0"
|
||||||
},
|
},
|
||||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2"
|
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2"
|
||||||
|
|||||||
+1299
-1218
File diff suppressed because it is too large
Load Diff
+146
-8
@@ -4,13 +4,27 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern } from './index.mjs';
|
import { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
||||||
|
|
||||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||||
const getValue = (e) => {
|
const getValue = (e) => {
|
||||||
let value = typeof e.value === 'object' ? e.value.note ?? e.value.n : e.value;
|
let { value } = e;
|
||||||
if (typeof value === 'string') {
|
if (typeof e.value !== 'object') {
|
||||||
value = toMidi(value);
|
value = { value };
|
||||||
|
}
|
||||||
|
let { note, n, freq, s } = value;
|
||||||
|
if (freq) {
|
||||||
|
return freqToMidi(freq);
|
||||||
|
}
|
||||||
|
note = note ?? n;
|
||||||
|
if (typeof note === 'string') {
|
||||||
|
return toMidi(note);
|
||||||
|
}
|
||||||
|
if (typeof note === 'number') {
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
if (s) {
|
||||||
|
return '_' + s;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
@@ -48,9 +62,6 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
from = 0;
|
from = 0;
|
||||||
to = timeframeProp;
|
to = timeframeProp;
|
||||||
}
|
}
|
||||||
if (!autorange && fold) {
|
|
||||||
console.warn('disabling autorange has no effect when fold is enabled');
|
|
||||||
}
|
|
||||||
const timeAxis = vertical ? h : w;
|
const timeAxis = vertical ? h : w;
|
||||||
const valueAxis = vertical ? w : h;
|
const valueAxis = vertical ? w : h;
|
||||||
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
|
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
|
||||||
@@ -138,10 +149,137 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
maxMidi = max;
|
maxMidi = max;
|
||||||
valueExtent = maxMidi - minMidi + 1;
|
valueExtent = maxMidi - minMidi + 1;
|
||||||
}
|
}
|
||||||
foldValues = values.sort((a, b) => a - b);
|
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
|
||||||
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// this function allows drawing a pianoroll without ties to Pattern.prototype
|
||||||
|
// it will probably replace the above in the future
|
||||||
|
export function pianoroll({
|
||||||
|
time,
|
||||||
|
haps,
|
||||||
|
cycles = 4,
|
||||||
|
playhead = 0.5,
|
||||||
|
flipTime = 0,
|
||||||
|
flipValues = 0,
|
||||||
|
hideNegative = false,
|
||||||
|
// inactive = '#C9E597',
|
||||||
|
// inactive = '#FFCA28',
|
||||||
|
inactive = '#7491D2',
|
||||||
|
active = '#FFCA28',
|
||||||
|
// background = '#2A3236',
|
||||||
|
background = 'transparent',
|
||||||
|
smear = 0,
|
||||||
|
playheadColor = 'white',
|
||||||
|
minMidi = 10,
|
||||||
|
maxMidi = 90,
|
||||||
|
autorange = 0,
|
||||||
|
timeframe: timeframeProp,
|
||||||
|
fold = 0,
|
||||||
|
vertical = 0,
|
||||||
|
ctx,
|
||||||
|
} = {}) {
|
||||||
|
const w = ctx.canvas.width;
|
||||||
|
const h = ctx.canvas.height;
|
||||||
|
let from = -cycles * playhead;
|
||||||
|
let to = cycles * (1 - playhead);
|
||||||
|
|
||||||
|
if (timeframeProp) {
|
||||||
|
console.warn('timeframe is deprecated! use from/to instead');
|
||||||
|
from = 0;
|
||||||
|
to = timeframeProp;
|
||||||
|
}
|
||||||
|
const timeAxis = vertical ? h : w;
|
||||||
|
const valueAxis = vertical ? w : h;
|
||||||
|
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
|
||||||
|
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
|
||||||
|
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
|
||||||
|
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
|
||||||
|
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
|
||||||
|
let foldValues = [];
|
||||||
|
flipTime && timeRange.reverse();
|
||||||
|
flipValues && valueRange.reverse();
|
||||||
|
|
||||||
|
// onQuery
|
||||||
|
const { min, max, values } = haps.reduce(
|
||||||
|
({ min, max, values }, e) => {
|
||||||
|
const v = getValue(e);
|
||||||
|
return {
|
||||||
|
min: v < min ? v : min,
|
||||||
|
max: v > max ? v : max,
|
||||||
|
values: values.includes(v) ? values : [...values, v],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ min: Infinity, max: -Infinity, values: [] },
|
||||||
|
);
|
||||||
|
if (autorange) {
|
||||||
|
minMidi = min;
|
||||||
|
maxMidi = max;
|
||||||
|
valueExtent = maxMidi - minMidi + 1;
|
||||||
|
}
|
||||||
|
// foldValues = values.sort((a, b) => a - b);
|
||||||
|
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
|
||||||
|
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
||||||
|
|
||||||
|
ctx.fillStyle = background;
|
||||||
|
ctx.globalAlpha = 1; // reset!
|
||||||
|
if (!smear) {
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
ctx.fillRect(0, 0, w, h);
|
||||||
|
}
|
||||||
|
/* const inFrame = (event) =>
|
||||||
|
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
|
||||||
|
haps
|
||||||
|
// .filter(inFrame)
|
||||||
|
.forEach((event) => {
|
||||||
|
const isActive = event.whole.begin <= time && event.whole.end > time;
|
||||||
|
const color = event.value?.color || event.context?.color;
|
||||||
|
ctx.fillStyle = color || inactive;
|
||||||
|
ctx.strokeStyle = color || active;
|
||||||
|
ctx.globalAlpha = event.context.velocity ?? 1;
|
||||||
|
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
||||||
|
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||||
|
const value = getValue(event);
|
||||||
|
const valuePx = scale(
|
||||||
|
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
|
||||||
|
...valueRange,
|
||||||
|
);
|
||||||
|
let margin = 0;
|
||||||
|
const offset = scale(time / timeExtent, ...timeRange);
|
||||||
|
let coords;
|
||||||
|
if (vertical) {
|
||||||
|
coords = [
|
||||||
|
valuePx + 1 - (flipValues ? barThickness : 0), // x
|
||||||
|
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
|
||||||
|
barThickness - 2, // width
|
||||||
|
durationPx - 2, // height
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
coords = [
|
||||||
|
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
|
||||||
|
valuePx + 1 - (flipValues ? 0 : barThickness), // y
|
||||||
|
durationPx - 2, // widith
|
||||||
|
barThickness - 2, // height
|
||||||
|
];
|
||||||
|
}
|
||||||
|
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
||||||
|
});
|
||||||
|
ctx.globalAlpha = 1; // reset!
|
||||||
|
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||||
|
// draw playhead
|
||||||
|
ctx.strokeStyle = playheadColor;
|
||||||
|
ctx.beginPath();
|
||||||
|
if (vertical) {
|
||||||
|
ctx.moveTo(0, playheadPosition);
|
||||||
|
ctx.lineTo(valueAxis, playheadPosition);
|
||||||
|
} else {
|
||||||
|
ctx.moveTo(playheadPosition, 0);
|
||||||
|
ctx.lineTo(playheadPosition, valueAxis);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export function repl({
|
|||||||
afterEval,
|
afterEval,
|
||||||
getTime,
|
getTime,
|
||||||
transpiler,
|
transpiler,
|
||||||
|
editPattern,
|
||||||
onToggle,
|
onToggle,
|
||||||
}) {
|
}) {
|
||||||
const scheduler = new Cyclist({
|
const scheduler = new Cyclist({
|
||||||
@@ -40,11 +41,13 @@ export function repl({
|
|||||||
throw new Error('no code to evaluate');
|
throw new Error('no code to evaluate');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
beforeEval({ code });
|
beforeEval?.({ code });
|
||||||
const { pattern } = await _evaluate(code, transpiler);
|
let { pattern } = await _evaluate(code, transpiler);
|
||||||
|
|
||||||
logger(`[eval] code updated`);
|
logger(`[eval] code updated`);
|
||||||
|
pattern = editPattern?.(pattern) || pattern;
|
||||||
scheduler.setPattern(pattern, autostart);
|
scheduler.setPattern(pattern, autostart);
|
||||||
afterEval({ code, pattern });
|
afterEval?.({ code, pattern });
|
||||||
return pattern;
|
return pattern;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.warn(`[repl] eval error: ${err.message}`);
|
// console.warn(`[repl] eval error: ${err.message}`);
|
||||||
|
|||||||
+54
-88
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Hap } from './hap.mjs';
|
import { Hap } from './hap.mjs';
|
||||||
import { Pattern, fastcat, reify, silence, stack, isPattern } from './pattern.mjs';
|
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
import { id } from './util.mjs';
|
import { id } from './util.mjs';
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export const signal = (func) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isaw = signal((t) => 1 - (t % 1));
|
export const isaw = signal((t) => 1 - (t % 1));
|
||||||
export const isaw2 = isaw._toBipolar();
|
export const isaw2 = isaw.toBipolar();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sawtooth signal between 0 and 1.
|
* A sawtooth signal between 0 and 1.
|
||||||
@@ -33,7 +33,7 @@ export const isaw2 = isaw._toBipolar();
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const saw = signal((t) => t % 1);
|
export const saw = signal((t) => t % 1);
|
||||||
export const saw2 = saw._toBipolar();
|
export const saw2 = saw.toBipolar();
|
||||||
|
|
||||||
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
|||||||
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
|
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const sine = sine2._fromBipolar();
|
export const sine = sine2.fromBipolar();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cosine signal between 0 and 1.
|
* A cosine signal between 0 and 1.
|
||||||
@@ -67,7 +67,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
export const square = signal((t) => Math.floor((t * 2) % 2));
|
||||||
export const square2 = square._toBipolar();
|
export const square2 = square.toBipolar();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A triangle signal between 0 and 1.
|
* A triangle signal between 0 and 1.
|
||||||
@@ -127,7 +127,7 @@ export const rand = signal(timeToRand);
|
|||||||
/**
|
/**
|
||||||
* A continuous pattern of random numbers, between -1 and 1
|
* A continuous pattern of random numbers, between -1 and 1
|
||||||
*/
|
*/
|
||||||
export const rand2 = rand._toBipolar();
|
export const rand2 = rand.toBipolar();
|
||||||
|
|
||||||
export const _brandBy = (p) => rand.fmap((x) => x < p);
|
export const _brandBy = (p) => rand.fmap((x) => x < p);
|
||||||
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
||||||
@@ -201,7 +201,7 @@ Pattern.prototype.choose = function (...xs) {
|
|||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.choose2 = function (...xs) {
|
Pattern.prototype.choose2 = function (...xs) {
|
||||||
return chooseWith(this._fromBipolar(), xs);
|
return chooseWith(this.fromBipolar(), xs);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -258,9 +258,9 @@ export const perlinWith = (pat) => {
|
|||||||
*/
|
*/
|
||||||
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
||||||
|
|
||||||
Pattern.prototype._degradeByWith = function (withPat, x) {
|
export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
|
||||||
return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x));
|
pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
|
||||||
};
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Randomly removes events from the pattern by a given amount.
|
* Randomly removes events from the pattern by a given amount.
|
||||||
@@ -276,9 +276,9 @@ Pattern.prototype._degradeByWith = function (withPat, x) {
|
|||||||
* @example
|
* @example
|
||||||
* s("[hh?0.2]*8")
|
* s("[hh?0.2]*8")
|
||||||
*/
|
*/
|
||||||
Pattern.prototype._degradeBy = function (x) {
|
export const degradeBy = register('degradeBy', function (x, pat) {
|
||||||
return this._degradeByWith(rand, x);
|
return pat._degradeByWith(rand, x);
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -292,9 +292,7 @@ Pattern.prototype._degradeBy = function (x) {
|
|||||||
* @example
|
* @example
|
||||||
* s("[hh?]*8")
|
* s("[hh?]*8")
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.degrade = function () {
|
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
||||||
return this._degradeBy(0.5);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inverse of {@link Pattern#degradeBy}: Randomly removes events from the pattern by a given amount.
|
* Inverse of {@link Pattern#degradeBy}: Randomly removes events from the pattern by a given amount.
|
||||||
@@ -309,26 +307,14 @@ Pattern.prototype.degrade = function () {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*8").undegradeBy(0.2)
|
* s("hh*8").undegradeBy(0.2)
|
||||||
*/
|
*/
|
||||||
Pattern.prototype._undegradeBy = function (x) {
|
export const undegradeBy = register('undegradeBy', function (x, pat) {
|
||||||
return this._degradeByWith(
|
return pat._degradeByWith(
|
||||||
rand.fmap((r) => 1 - r),
|
rand.fmap((r) => 1 - r),
|
||||||
x,
|
x,
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
Pattern.prototype.undegrade = function () {
|
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
||||||
return this._undegradeBy(0.5);
|
|
||||||
};
|
|
||||||
|
|
||||||
Pattern.prototype._sometimesBy = function (x, func) {
|
|
||||||
return stack(this._degradeBy(x), func(this._undegradeBy(1 - x)));
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://github.com/tidalcycles/strudel/discussions/198
|
|
||||||
/* Pattern.prototype._sometimesBy = function (x, other) {
|
|
||||||
other = typeof other === 'function' ? other(this._undegradeBy(1 - x)) : reify(other)._undegradeBy(1 - x);
|
|
||||||
return stack(this._degradeBy(x), other);
|
|
||||||
}; */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -343,24 +329,12 @@ Pattern.prototype._sometimesBy = function (x, func) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
|
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.sometimesBy = function (patx, func) {
|
|
||||||
const pat = this;
|
|
||||||
return reify(patx)
|
|
||||||
.fmap((x) => pat._sometimesBy(x, func))
|
|
||||||
.innerJoin();
|
|
||||||
};
|
|
||||||
|
|
||||||
// why does this exist? it is identical to sometimesBy
|
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
||||||
Pattern.prototype._sometimesByPre = function (x, func) {
|
|
||||||
return stack(this._degradeBy(x), func(this).undegradeBy(1 - x));
|
|
||||||
};
|
|
||||||
|
|
||||||
Pattern.prototype.sometimesByPre = function (patx, func) {
|
|
||||||
const pat = this;
|
|
||||||
return reify(patx)
|
return reify(patx)
|
||||||
.fmap((x) => pat._sometimesByPre(x, func))
|
.fmap((x) => stack(pat._degradeBy(x), func(pat._undegradeBy(1 - x))))
|
||||||
.innerJoin();
|
.innerJoin();
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -373,20 +347,9 @@ Pattern.prototype.sometimesByPre = function (patx, func) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*4").sometimes(x=>x.speed("0.5"))
|
* s("hh*4").sometimes(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.sometimes = function (func) {
|
export const sometimes = register('sometimes', function (func, pat) {
|
||||||
return this._sometimesBy(0.5, func);
|
return pat._sometimesBy(0.5, func);
|
||||||
};
|
});
|
||||||
|
|
||||||
Pattern.prototype.sometimesPre = function (func) {
|
|
||||||
return this._sometimesByPre(0.5, func);
|
|
||||||
};
|
|
||||||
|
|
||||||
Pattern.prototype._someCyclesBy = function (x, func) {
|
|
||||||
return stack(
|
|
||||||
this._degradeByWith(rand._segment(1), x),
|
|
||||||
func(this._degradeByWith(rand.fmap((r) => 1 - r)._segment(1), 1 - x)),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -401,12 +364,17 @@ Pattern.prototype._someCyclesBy = function (x, func) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
|
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.someCyclesBy = function (patx, func) {
|
|
||||||
const pat = this;
|
export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
|
||||||
return reify(patx)
|
return reify(patx)
|
||||||
.fmap((x) => pat._someCyclesBy(x, func))
|
.fmap((x) =>
|
||||||
|
stack(
|
||||||
|
pat._degradeByWith(rand._segment(1), x),
|
||||||
|
func(pat._degradeByWith(rand.fmap((r) => 1 - r)._segment(1), 1 - x)),
|
||||||
|
),
|
||||||
|
)
|
||||||
.innerJoin();
|
.innerJoin();
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -418,9 +386,9 @@ Pattern.prototype.someCyclesBy = function (patx, func) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
|
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.someCycles = function (func) {
|
export const someCycles = register('someCycles', function (func, pat) {
|
||||||
return this._someCyclesBy(0.5, func);
|
return pat._someCyclesBy(0.5, func);
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -432,9 +400,9 @@ Pattern.prototype.someCycles = function (func) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*8").often(x=>x.speed("0.5"))
|
* s("hh*8").often(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.often = function (func) {
|
export const often = register('often', function (func, pat) {
|
||||||
return this.sometimesBy(0.75, func);
|
return pat.sometimesBy(0.75, func);
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -446,9 +414,9 @@ Pattern.prototype.often = function (func) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*8").rarely(x=>x.speed("0.5"))
|
* s("hh*8").rarely(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.rarely = function (func) {
|
export const rarely = register('rarely', function (func, pat) {
|
||||||
return this.sometimesBy(0.25, func);
|
return pat.sometimesBy(0.25, func);
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -460,9 +428,9 @@ Pattern.prototype.rarely = function (func) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*8").almostNever(x=>x.speed("0.5"))
|
* s("hh*8").almostNever(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.almostNever = function (func) {
|
export const almostNever = register('almostNever', function (func, pat) {
|
||||||
return this.sometimesBy(0.1, func);
|
return pat.sometimesBy(0.1, func);
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -474,9 +442,9 @@ Pattern.prototype.almostNever = function (func) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*8").almostAlways(x=>x.speed("0.5"))
|
* s("hh*8").almostAlways(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.almostAlways = function (func) {
|
export const almostAlways = register('almostAlways', function (func, pat) {
|
||||||
return this.sometimesBy(0.9, func);
|
return pat.sometimesBy(0.9, func);
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -488,9 +456,9 @@ Pattern.prototype.almostAlways = function (func) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*8").never(x=>x.speed("0.5"))
|
* s("hh*8").never(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.never = function (func) {
|
export const never = register('never', function (_, pat) {
|
||||||
return this;
|
return pat;
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -502,8 +470,6 @@ Pattern.prototype.never = function (func) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*8").always(x=>x.speed("0.5"))
|
* s("hh*8").always(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.always = function (func) {
|
export const always = register('always', function (func, pat) {
|
||||||
return func(this);
|
return func(pat);
|
||||||
};
|
});
|
||||||
|
|
||||||
Pattern.prototype.patternified.push('degradeBy', 'undegradeBy');
|
|
||||||
|
|||||||
+6
-10
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, patternify2 } from './index.mjs';
|
import { register } from './index.mjs';
|
||||||
|
|
||||||
let synth;
|
let synth;
|
||||||
try {
|
try {
|
||||||
@@ -16,7 +16,7 @@ try {
|
|||||||
let allVoices = synth?.getVoices();
|
let allVoices = synth?.getVoices();
|
||||||
// console.log('voices', allVoices);
|
// console.log('voices', allVoices);
|
||||||
|
|
||||||
function speak(words, lang, voice) {
|
function triggerSpeech(words, lang, voice) {
|
||||||
synth.cancel();
|
synth.cancel();
|
||||||
const utterance = new SpeechSynthesisUtterance(words);
|
const utterance = new SpeechSynthesisUtterance(words);
|
||||||
utterance.lang = lang;
|
utterance.lang = lang;
|
||||||
@@ -31,12 +31,8 @@ function speak(words, lang, voice) {
|
|||||||
speechSynthesis.speak(utterance);
|
speechSynthesis.speak(utterance);
|
||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype._speak = function (lang, voice) {
|
export const speak = register('speak', function (lang, voice, pat) {
|
||||||
return this.onTrigger((_, hap) => {
|
return pat.onTrigger((_, hap) => {
|
||||||
speak(hap.value, lang, voice);
|
triggerSpeech(hap.value, lang, voice);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
Pattern.prototype.speak = function (lang, voice) {
|
|
||||||
return patternify2(Pattern.prototype._speak)(reify(lang), reify(voice), this);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
pure,
|
pure,
|
||||||
stack,
|
stack,
|
||||||
fastcat,
|
fastcat,
|
||||||
|
firstOf,
|
||||||
slowcat,
|
slowcat,
|
||||||
cat,
|
cat,
|
||||||
sequence,
|
sequence,
|
||||||
@@ -49,7 +50,7 @@ import { steady } from '../signal.mjs';
|
|||||||
|
|
||||||
import controls from '../controls.mjs';
|
import controls from '../controls.mjs';
|
||||||
|
|
||||||
const { n } = controls;
|
const { n, s } = controls;
|
||||||
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 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);
|
||||||
@@ -58,7 +59,7 @@ const third = Fraction(1, 3);
|
|||||||
const twothirds = Fraction(2, 3);
|
const twothirds = Fraction(2, 3);
|
||||||
|
|
||||||
const sameFirst = (a, b) => {
|
const sameFirst = (a, b) => {
|
||||||
return expect(a._sortHapsByPart().firstCycle()).toStrictEqual(b._sortHapsByPart().firstCycle());
|
return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle());
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('TimeSpan', () => {
|
describe('TimeSpan', () => {
|
||||||
@@ -153,35 +154,46 @@ describe('Pattern', () => {
|
|||||||
).toBe(7);
|
).toBe(7);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('out()', () => {
|
||||||
|
it('is an alias for set.out()', () => {
|
||||||
|
sameFirst(sequence(1, 2).out(5, 6, 7, 8), sequence(1, 2).set.out(5, 6, 7, 8));
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('add()', () => {
|
describe('add()', () => {
|
||||||
|
it('works as toplevel function', () => {
|
||||||
|
expect(add(pure(4), pure(5)).query(st(0, 1))[0].value).toBe(9);
|
||||||
|
});
|
||||||
|
it('works as toplevel function, with bare values for arguments', () => {
|
||||||
|
expect(add(4, 5).query(st(0, 1))[0].value).toBe(9);
|
||||||
|
});
|
||||||
it('can structure In()', () => {
|
it('can structure In()', () => {
|
||||||
expect(pure(3).add(pure(4)).query(st(0, 1))[0].value).toBe(7);
|
expect(pure(3).add(pure(4)).query(st(0, 1))[0].value).toBe(7);
|
||||||
expect(pure(3).addIn(pure(4)).query(st(0, 1))[0].value).toBe(7);
|
expect(pure(3).add.in(pure(4)).query(st(0, 1))[0].value).toBe(7);
|
||||||
});
|
});
|
||||||
it('can structure Out()', () => {
|
it('can structure Out()', () => {
|
||||||
sameFirst(sequence(1, 2).addOut(4), sequence(5, 6).struct(true));
|
sameFirst(sequence(1, 2).add.out(4), sequence(5, 6).struct(true));
|
||||||
});
|
});
|
||||||
it('can Mix() structure', () => {
|
it('can Mix() structure', () => {
|
||||||
expect(sequence(1, 2).addMix(silence, 5, silence).firstCycle()).toStrictEqual([
|
expect(sequence(1, 2).add.mix(silence, 5, silence).firstCycle()).toStrictEqual([
|
||||||
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 6),
|
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 6),
|
||||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
|
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('can Trig() structure', () => {
|
it('can Trig() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).addTrig(20, 30).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trig(20, 30).early(2),
|
||||||
sequence(26, 27, 36, 37),
|
sequence(26, 27, 36, 37),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Trigzero() structure', () => {
|
it('can Trigzero() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).addTrigzero(20, 30).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trigzero(20, 30).early(2),
|
||||||
sequence(21, 22, 31, 32),
|
sequence(21, 22, 31, 32),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Squeeze() structure', () => {
|
it('can Squeeze() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
sequence(1, [2, 3]).addSqueeze(sequence(10, 20, 30)),
|
sequence(1, [2, 3]).add.squeeze(sequence(10, 20, 30)),
|
||||||
sequence(
|
sequence(
|
||||||
[11, 21, 31],
|
[11, 21, 31],
|
||||||
[
|
[
|
||||||
@@ -193,7 +205,7 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
it('can SqueezeOut() structure', () => {
|
it('can SqueezeOut() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
sequence(1, [2, 3]).addSqueezeOut(10, 20, 30),
|
sequence(1, [2, 3]).add.squeezeout(10, 20, 30),
|
||||||
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
|
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -204,32 +216,32 @@ describe('Pattern', () => {
|
|||||||
describe('keep()', () => {
|
describe('keep()', () => {
|
||||||
it('can structure In()', () => {
|
it('can structure In()', () => {
|
||||||
expect(pure(3).keep(pure(4)).query(st(0, 1))[0].value).toBe(3);
|
expect(pure(3).keep(pure(4)).query(st(0, 1))[0].value).toBe(3);
|
||||||
expect(pure(3).keepIn(pure(4)).query(st(0, 1))[0].value).toBe(3);
|
expect(pure(3).keep.in(pure(4)).query(st(0, 1))[0].value).toBe(3);
|
||||||
});
|
});
|
||||||
it('can structure Out()', () => {
|
it('can structure Out()', () => {
|
||||||
sameFirst(sequence(1, 2).keepOut(4), sequence(1, 2).struct(true));
|
sameFirst(sequence(1, 2).keep.out(4), sequence(1, 2).struct(true));
|
||||||
});
|
});
|
||||||
it('can Mix() structure', () => {
|
it('can Mix() structure', () => {
|
||||||
expect(sequence(1, 2).keepMix(silence, 5, silence).firstCycle()).toStrictEqual([
|
expect(sequence(1, 2).keep.mix(silence, 5, silence).firstCycle()).toStrictEqual([
|
||||||
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
|
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
|
||||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('can Trig() structure', () => {
|
it('can Trig() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepTrig(20, 30).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trig(20, 30).early(2),
|
||||||
sequence(6, 7, 6, 7),
|
sequence(6, 7, 6, 7),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Trigzero() structure', () => {
|
it('can Trigzero() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepTrigzero(20, 30).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trigzero(20, 30).early(2),
|
||||||
sequence(1, 2, 1, 2),
|
sequence(1, 2, 1, 2),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Squeeze() structure', () => {
|
it('can Squeeze() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
sequence(1, [2, 3]).keepSqueeze(sequence(10, 20, 30)),
|
sequence(1, [2, 3]).keep.squeeze(sequence(10, 20, 30)),
|
||||||
sequence(
|
sequence(
|
||||||
[1, 1, 1],
|
[1, 1, 1],
|
||||||
[
|
[
|
||||||
@@ -240,38 +252,38 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can SqueezeOut() structure', () => {
|
it('can SqueezeOut() structure', () => {
|
||||||
sameFirst(sequence(1, [2, 3]).keepSqueezeOut(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
|
sameFirst(sequence(1, [2, 3]).keep.squeezeout(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('keepif()', () => {
|
describe('keepif()', () => {
|
||||||
it('can structure In()', () => {
|
it('can structure In()', () => {
|
||||||
sameFirst(sequence(3, 4).keepif(true, false), sequence(3, silence));
|
sameFirst(sequence(3, 4).keepif(true, false), sequence(3, silence));
|
||||||
sameFirst(sequence(3, 4).keepifIn(true, false), sequence(3, silence));
|
sameFirst(sequence(3, 4).keepif.in(true, false), sequence(3, silence));
|
||||||
});
|
});
|
||||||
it('can structure Out()', () => {
|
it('can structure Out()', () => {
|
||||||
sameFirst(pure(1).keepifOut(true, false), sequence(1, silence));
|
sameFirst(pure(1).keepif.out(true, false), sequence(1, silence));
|
||||||
});
|
});
|
||||||
it('can Mix() structure', () => {
|
it('can Mix() structure', () => {
|
||||||
expect(sequence(1, 2).keepifMix(false, true, false).firstCycle()).toStrictEqual([
|
expect(sequence(1, 2).keepif.mix(false, true, false).firstCycle()).toStrictEqual([
|
||||||
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
|
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
|
||||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('can Trig() structure', () => {
|
it('can Trig() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepifTrig(false, true).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trig(false, true).early(2),
|
||||||
sequence(silence, silence, 6, 7),
|
sequence(silence, silence, 6, 7),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Trigzero() structure', () => {
|
it('can Trigzero() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepifTrigzero(false, true).early(2),
|
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trigzero(false, true).early(2),
|
||||||
sequence(silence, silence, 1, 2),
|
sequence(silence, silence, 1, 2),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Squeeze() structure', () => {
|
it('can Squeeze() structure', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
sequence(1, [2, 3]).keepifSqueeze(sequence(true, true, false)),
|
sequence(1, [2, 3]).keepif.squeeze(sequence(true, true, false)),
|
||||||
sequence(
|
sequence(
|
||||||
[1, 1, silence],
|
[1, 1, silence],
|
||||||
[
|
[
|
||||||
@@ -282,7 +294,7 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can SqueezeOut() structure', () => {
|
it('can SqueezeOut() structure', () => {
|
||||||
sameFirst(sequence(1, [2, 3]).keepifSqueezeOut(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
|
sameFirst(sequence(1, [2, 3]).keepif.squeezeout(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('sub()', () => {
|
describe('sub()', () => {
|
||||||
@@ -322,13 +334,13 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
describe('setOut()', () => {
|
describe('setOut()', () => {
|
||||||
it('Can set things with structure from second pattern', () => {
|
it('Can set things with structure from second pattern', () => {
|
||||||
sameFirst(sequence(1, 2).setOut(4), pure(4).mask(true, true));
|
sameFirst(sequence(1, 2).set.out(4), pure(4).mask(true, true));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('setSqueeze()', () => {
|
describe('setSqueeze()', () => {
|
||||||
it('Can squeeze one pattern inside the haps of another', () => {
|
it('Can squeeze one pattern inside the haps of another', () => {
|
||||||
sameFirst(
|
sameFirst(
|
||||||
sequence(1, [2, 3]).setSqueeze(sequence('a', 'b', 'c')),
|
sequence(1, [2, 3]).set.squeeze(sequence('a', 'b', 'c')),
|
||||||
sequence(
|
sequence(
|
||||||
['a', 'b', 'c'],
|
['a', 'b', 'c'],
|
||||||
[
|
[
|
||||||
@@ -338,7 +350,7 @@ describe('Pattern', () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
sameFirst(
|
sameFirst(
|
||||||
sequence(1, [2, 3]).setSqueeze('a', 'b', 'c'),
|
sequence(1, [2, 3]).set.squeeze('a', 'b', 'c'),
|
||||||
sequence(
|
sequence(
|
||||||
['a', 'b', 'c'],
|
['a', 'b', 'c'],
|
||||||
[
|
[
|
||||||
@@ -382,7 +394,7 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('copes with breaking up events across cycles', () => {
|
it('copes with breaking up events across cycles', () => {
|
||||||
expect(pure('a').slow(2)._fastGap(2)._setContext({}).query(st(0, 2))).toStrictEqual([
|
expect(pure('a').slow(2)._fastGap(2).setContext({}).query(st(0, 2))).toStrictEqual([
|
||||||
hap(ts(0, 1), ts(0, 0.5), 'a'),
|
hap(ts(0, 1), ts(0, 0.5), 'a'),
|
||||||
hap(ts(0.5, 1.5), ts(1, 1.5), 'a'),
|
hap(ts(0.5, 1.5), ts(1, 1.5), 'a'),
|
||||||
]);
|
]);
|
||||||
@@ -406,8 +418,8 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('defaults to accepting sequences', () => {
|
it('defaults to accepting sequences', () => {
|
||||||
expect(sequence(1, 2, 3).fast(sequence(1.5, 2)).firstCycle()).toStrictEqual(
|
expect(sequence('a', 'b', 'c').fast(sequence(1.5, 2)).sortHapsByPart().firstCycle()).toStrictEqual(
|
||||||
sequence(1, 2, 3).fast(1.5, 2).firstCycle(),
|
sequence('a', 'b', 'c').fast(1.5, 2).sortHapsByPart().firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('works as a static function', () => {
|
it('works as a static function', () => {
|
||||||
@@ -446,8 +458,8 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
describe('slow()', () => {
|
describe('slow()', () => {
|
||||||
it('Supports zero-length queries', () => {
|
it('Supports zero-length queries', () => {
|
||||||
expect(steady('a').slow(1)._setContext({}).queryArc(0, 0)).toStrictEqual(
|
expect(steady('a').slow(1).setContext({}).queryArc(0, 0)).toStrictEqual(
|
||||||
steady('a')._setContext({}).queryArc(0, 0),
|
steady('a').setContext({}).queryArc(0, 0),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -465,7 +477,7 @@ describe('Pattern', () => {
|
|||||||
it('Filters true', () => {
|
it('Filters true', () => {
|
||||||
expect(
|
expect(
|
||||||
pure(true)
|
pure(true)
|
||||||
._filterValues((x) => x)
|
.filterValues((x) => x)
|
||||||
.firstCycle().length,
|
.firstCycle().length,
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
});
|
});
|
||||||
@@ -490,7 +502,7 @@ describe('Pattern', () => {
|
|||||||
pure(10)
|
pure(10)
|
||||||
.when(slowcat(true, false), (x) => x.add(3))
|
.when(slowcat(true, false), (x) => x.add(3))
|
||||||
.fast(4)
|
.fast(4)
|
||||||
._sortHapsByPart()
|
.sortHapsByPart()
|
||||||
.firstCycle(),
|
.firstCycle(),
|
||||||
).toStrictEqual(fastcat(13, 10, 13, 10).firstCycle());
|
).toStrictEqual(fastcat(13, 10, 13, 10).firstCycle());
|
||||||
});
|
});
|
||||||
@@ -577,26 +589,36 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('every()', () => {
|
describe('firstOf()', () => {
|
||||||
it('Can apply a function every 3rd time', () => {
|
it('Can apply a function every 3rd time', () => {
|
||||||
expect(
|
expect(
|
||||||
pure('a')
|
pure('a')
|
||||||
.every(3, (x) => x._fast(2))
|
.firstOf(3, (x) => x._fast(2))
|
||||||
._fast(3)
|
._fast(3)
|
||||||
.firstCycle(),
|
.firstCycle(),
|
||||||
).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle());
|
).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle());
|
||||||
});
|
});
|
||||||
it('works with currying', () => {
|
it('Works as a toplevel function', () => {
|
||||||
expect(pure('a').every(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
|
expect(firstOf(3, fast(2), pure('a'))._fast(3).firstCycle()).toStrictEqual(
|
||||||
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
||||||
);
|
);
|
||||||
expect(sequence(3, 4, 5).every(3, add(3)).fast(5).firstCycle()).toStrictEqual(
|
});
|
||||||
|
it('Works as a toplevel function, with a patterned first argument', () => {
|
||||||
|
expect(firstOf(pure(3), fast(2), pure('a'))._fast(3).firstCycle()).toStrictEqual(
|
||||||
|
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('works with currying', () => {
|
||||||
|
expect(pure('a').firstOf(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
|
||||||
|
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
||||||
|
);
|
||||||
|
expect(sequence(3, 4, 5).firstOf(3, add(3)).fast(5).firstCycle()).toStrictEqual(
|
||||||
sequence(6, 7, 8, 3, 4, 5, 3, 4, 5, 6, 7, 8, 3, 4, 5).firstCycle(),
|
sequence(6, 7, 8, 3, 4, 5, 3, 4, 5, 6, 7, 8, 3, 4, 5).firstCycle(),
|
||||||
);
|
);
|
||||||
expect(sequence(3, 4, 5).every(2, sub(1)).fast(5).firstCycle()).toStrictEqual(
|
expect(sequence(3, 4, 5).firstOf(2, sub(1)).fast(5).firstCycle()).toStrictEqual(
|
||||||
sequence(2, 3, 4, 3, 4, 5, 2, 3, 4, 3, 4, 5, 2, 3, 4).firstCycle(),
|
sequence(2, 3, 4, 3, 4, 5, 2, 3, 4, 3, 4, 5, 2, 3, 4).firstCycle(),
|
||||||
);
|
);
|
||||||
expect(sequence(3, 4, 5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle()).toStrictEqual(
|
expect(sequence(3, 4, 5).firstOf(3, add(3)).firstOf(2, sub(1)).fast(2).firstCycle()).toStrictEqual(
|
||||||
sequence(5, 6, 7, 3, 4, 5).firstCycle(),
|
sequence(5, 6, 7, 3, 4, 5).firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -692,7 +714,7 @@ describe('Pattern', () => {
|
|||||||
it('Can set the hap context', () => {
|
it('Can set the hap context', () => {
|
||||||
expect(
|
expect(
|
||||||
pure('a')
|
pure('a')
|
||||||
._setContext([
|
.setContext([
|
||||||
[
|
[
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[1, 2],
|
[1, 2],
|
||||||
@@ -713,13 +735,13 @@ describe('Pattern', () => {
|
|||||||
it('Can update the hap context', () => {
|
it('Can update the hap context', () => {
|
||||||
expect(
|
expect(
|
||||||
pure('a')
|
pure('a')
|
||||||
._setContext([
|
.setContext([
|
||||||
[
|
[
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[1, 2],
|
[1, 2],
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
._withContext((c) => [
|
.withContext((c) => [
|
||||||
...c,
|
...c,
|
||||||
[
|
[
|
||||||
[3, 4],
|
[3, 4],
|
||||||
@@ -743,7 +765,7 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
describe('apply', () => {
|
describe('apply', () => {
|
||||||
it('Can apply a function', () => {
|
it('Can apply a function', () => {
|
||||||
expect(sequence('a', 'b')._apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
|
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
|
||||||
}),
|
}),
|
||||||
it('Can apply a pattern of functions', () => {
|
it('Can apply a pattern of functions', () => {
|
||||||
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
|
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
|
||||||
@@ -784,18 +806,18 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
describe('jux', () => {
|
describe('jux', () => {
|
||||||
it('Can juxtapose', () => {
|
it('Can juxtapose', () => {
|
||||||
expect(pure({ a: 1 }).jux(fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
|
expect(pure({ a: 1 }).jux(fast(2)).sortHapsByPart().firstCycle()).toStrictEqual(
|
||||||
stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
|
stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
|
||||||
._sortHapsByPart()
|
.sortHapsByPart()
|
||||||
.firstCycle(),
|
.firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('juxBy', () => {
|
describe('juxBy', () => {
|
||||||
it('Can juxtapose by half', () => {
|
it('Can juxtapose by half', () => {
|
||||||
expect(pure({ a: 1 }).juxBy(0.5, fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
|
expect(pure({ a: 1 }).juxBy(0.5, fast(2)).sortHapsByPart().firstCycle()).toStrictEqual(
|
||||||
stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
|
stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
|
||||||
._sortHapsByPart()
|
.sortHapsByPart()
|
||||||
.firstCycle(),
|
.firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -805,7 +827,7 @@ describe('Pattern', () => {
|
|||||||
expect(
|
expect(
|
||||||
sequence('a', ['a', 'a'])
|
sequence('a', ['a', 'a'])
|
||||||
.fmap((a) => fastcat('b', 'c'))
|
.fmap((a) => fastcat('b', 'c'))
|
||||||
._squeezeJoin()
|
.squeezeJoin()
|
||||||
.firstCycle(),
|
.firstCycle(),
|
||||||
).toStrictEqual(
|
).toStrictEqual(
|
||||||
sequence(
|
sequence(
|
||||||
@@ -820,7 +842,7 @@ describe('Pattern', () => {
|
|||||||
it('Squeezes to the correct cycle', () => {
|
it('Squeezes to the correct cycle', () => {
|
||||||
expect(
|
expect(
|
||||||
pure(time.struct(true))
|
pure(time.struct(true))
|
||||||
._squeezeJoin()
|
.squeezeJoin()
|
||||||
.queryArc(3, 4)
|
.queryArc(3, 4)
|
||||||
.map((x) => x.value),
|
.map((x) => x.value),
|
||||||
).toStrictEqual([Fraction(3.5)]);
|
).toStrictEqual([Fraction(3.5)]);
|
||||||
@@ -857,7 +879,7 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('Can chop(2,3)', () => {
|
it('Can chop(2,3)', () => {
|
||||||
expect(pure({ sound: 'a' }).fast(2).chop(2, 3)._sortHapsByPart().firstCycle()).toStrictEqual(
|
expect(pure({ sound: 'a' }).fast(2).chop(2, 3).sortHapsByPart().firstCycle()).toStrictEqual(
|
||||||
sequence(
|
sequence(
|
||||||
[
|
[
|
||||||
{ sound: 'a', begin: 0, end: 0.5 },
|
{ sound: 'a', begin: 0, end: 0.5 },
|
||||||
@@ -869,7 +891,7 @@ describe('Pattern', () => {
|
|||||||
{ sound: 'a', begin: 2 / 3, end: 1 },
|
{ sound: 'a', begin: 2 / 3, end: 1 },
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
._sortHapsByPart()
|
.sortHapsByPart()
|
||||||
.firstCycle(),
|
.firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -893,4 +915,25 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('alignments', () => {
|
||||||
|
it('Can squeeze arguments', () => {
|
||||||
|
expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('defragmentHaps', () => {
|
||||||
|
it('Can merge two touching haps with same whole and value', () => {
|
||||||
|
expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1);
|
||||||
|
});
|
||||||
|
it('Doesnt merge two overlapping haps', () => {
|
||||||
|
expect(stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('Doesnt merge two touching haps with different values', () => {
|
||||||
|
expect(stack(pure('a').mask(1, 0), pure('b').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
|
||||||
|
});
|
||||||
|
it('Doesnt merge two touching haps with different wholes', () => {
|
||||||
|
expect(stack(sequence('a', silence), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
tokenizeNote,
|
tokenizeNote,
|
||||||
toMidi,
|
toMidi,
|
||||||
fromMidi,
|
fromMidi,
|
||||||
mod,
|
freqToMidi,
|
||||||
|
_mod,
|
||||||
compose,
|
compose,
|
||||||
getFrequency,
|
getFrequency,
|
||||||
getPlayableNoteValue,
|
getPlayableNoteValue,
|
||||||
@@ -32,9 +33,12 @@ describe('isNote', () => {
|
|||||||
expect(isNote(note)).toBe(true);
|
expect(isNote(note)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('should recognize notes without octave', () => {
|
||||||
|
expect(isNote('C')).toBe(true);
|
||||||
|
expect(isNote('Bb')).toBe(true);
|
||||||
|
});
|
||||||
it('should not recognize invalid notes', () => {
|
it('should not recognize invalid notes', () => {
|
||||||
expect(isNote('H5')).toBe(false);
|
expect(isNote('H5')).toBe(false);
|
||||||
expect(isNote('C')).toBe(false);
|
|
||||||
expect(isNote('X')).toBe(false);
|
expect(isNote('X')).toBe(false);
|
||||||
expect(isNote(1)).toBe(false);
|
expect(isNote(1)).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -94,6 +98,12 @@ describe('fromMidi', () => {
|
|||||||
expect(fromMidi(57)).toEqual(220);
|
expect(fromMidi(57)).toEqual(220);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('freqToMidi', () => {
|
||||||
|
it('should turn frequency into midi', () => {
|
||||||
|
expect(freqToMidi(440)).toEqual(69);
|
||||||
|
expect(freqToMidi(220)).toEqual(57);
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('getFrequency', () => {
|
describe('getFrequency', () => {
|
||||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||||
it('should turn note into frequency', () => {
|
it('should turn note into frequency', () => {
|
||||||
@@ -118,22 +128,22 @@ describe('getFrequency', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mod', () => {
|
describe('_mod', () => {
|
||||||
it('should work like regular modulo with positive numbers', () => {
|
it('should work like regular modulo with positive numbers', () => {
|
||||||
expect(mod(0, 3)).toEqual(0);
|
expect(_mod(0, 3)).toEqual(0);
|
||||||
expect(mod(1, 3)).toEqual(1);
|
expect(_mod(1, 3)).toEqual(1);
|
||||||
expect(mod(2, 3)).toEqual(2);
|
expect(_mod(2, 3)).toEqual(2);
|
||||||
expect(mod(3, 3)).toEqual(0);
|
expect(_mod(3, 3)).toEqual(0);
|
||||||
expect(mod(4, 3)).toEqual(1);
|
expect(_mod(4, 3)).toEqual(1);
|
||||||
expect(mod(4, 2)).toEqual(0);
|
expect(_mod(4, 2)).toEqual(0);
|
||||||
});
|
});
|
||||||
it('should work with negative numbers', () => {
|
it('should work with negative numbers', () => {
|
||||||
expect(mod(-1, 3)).toEqual(2);
|
expect(_mod(-1, 3)).toEqual(2);
|
||||||
expect(mod(-2, 3)).toEqual(1);
|
expect(_mod(-2, 3)).toEqual(1);
|
||||||
expect(mod(-3, 3)).toEqual(0);
|
expect(_mod(-3, 3)).toEqual(0);
|
||||||
expect(mod(-4, 3)).toEqual(2);
|
expect(_mod(-4, 3)).toEqual(2);
|
||||||
expect(mod(-5, 3)).toEqual(1);
|
expect(_mod(-5, 3)).toEqual(1);
|
||||||
expect(mod(-3, 2)).toEqual(1);
|
expect(_mod(-3, 2)).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -182,6 +192,7 @@ describe('parseNumeral', () => {
|
|||||||
expect(parseNumeral(1.5)).toBe(1.5);
|
expect(parseNumeral(1.5)).toBe(1.5);
|
||||||
});
|
});
|
||||||
it('should parse notes', () => {
|
it('should parse notes', () => {
|
||||||
|
expect(parseNumeral('c')).toBe(48);
|
||||||
expect(parseNumeral('c4')).toBe(60);
|
expect(parseNumeral('c4')).toBe(60);
|
||||||
expect(parseNumeral('c#4')).toBe(61);
|
expect(parseNumeral('c#4')).toBe(61);
|
||||||
expect(parseNumeral('db4')).toBe(61);
|
expect(parseNumeral('db4')).toBe(61);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { map, valued, mul } from '../value.mjs';
|
import { map, valued, mul } from '../value.mjs';
|
||||||
|
import controls from '../controls.mjs';
|
||||||
|
const { n } = controls;
|
||||||
|
|
||||||
describe('Value', () => {
|
describe('Value', () => {
|
||||||
it('unionWith', () => {
|
it('unionWith', () => {
|
||||||
@@ -21,4 +23,8 @@ describe('Value', () => {
|
|||||||
expect(valued(mul).ap(3).ap(3).value).toEqual(9);
|
expect(valued(mul).ap(3).ap(3).value).toEqual(9);
|
||||||
expect(valued(3).mul(3).value).toEqual(9);
|
expect(valued(3).mul(3).value).toEqual(9);
|
||||||
});
|
});
|
||||||
|
it('union bare numbers for numeral props', () => {
|
||||||
|
expect(n(3).cutoff(500).add(10).firstCycleValues).toEqual([{ n: 13, cutoff: 510 }]);
|
||||||
|
expect(n(3).cutoff(500).mul(2).firstCycleValues).toEqual([{ n: 6, cutoff: 1000 }]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class TimeSpan {
|
|||||||
|
|
||||||
// Support zero-width timespans
|
// Support zero-width timespans
|
||||||
if (begin.equals(end)) {
|
if (begin.equals(end)) {
|
||||||
return([new TimeSpan(begin, end)]);
|
return [new TimeSpan(begin, end)];
|
||||||
}
|
}
|
||||||
|
|
||||||
while (end.gt(begin)) {
|
while (end.gt(begin)) {
|
||||||
@@ -110,7 +110,7 @@ export class TimeSpan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
return this.begin.show() + ' -> ' + this.end.show();
|
return this.begin.show() + ' → ' + this.end.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+31
-7
@@ -5,7 +5,8 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// returns true if the given string is a note
|
// returns true if the given string is a note
|
||||||
export const isNote = (name) => /^[a-gA-G][#b]*[0-9]$/.test(name);
|
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
||||||
|
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]?$/.test(name);
|
||||||
export const tokenizeNote = (note) => {
|
export const tokenizeNote = (note) => {
|
||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
return [];
|
||||||
@@ -19,7 +20,7 @@ export const tokenizeNote = (note) => {
|
|||||||
|
|
||||||
// turns the given note into its midi number representation
|
// turns the given note into its midi number representation
|
||||||
export const toMidi = (note) => {
|
export const toMidi = (note) => {
|
||||||
const [pc, acc, oct] = tokenizeNote(note);
|
const [pc, acc, oct = 3] = tokenizeNote(note);
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
throw new Error('not a note: "' + note + '"');
|
throw new Error('not a note: "' + note + '"');
|
||||||
}
|
}
|
||||||
@@ -31,6 +32,30 @@ export const fromMidi = (n) => {
|
|||||||
return Math.pow(2, (n - 69) / 12) * 440;
|
return Math.pow(2, (n - 69) / 12) * 440;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const freqToMidi = (freq) => {
|
||||||
|
return (12 * Math.log(freq / 440)) / Math.LN2 + 69;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const valueToMidi = (value, fallbackValue) => {
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
throw new Error('valueToMidi: expected object value');
|
||||||
|
}
|
||||||
|
let { freq, note } = value;
|
||||||
|
if (typeof freq === 'number') {
|
||||||
|
return freqToMidi(freq);
|
||||||
|
}
|
||||||
|
if (typeof note === 'string') {
|
||||||
|
return toMidi(note);
|
||||||
|
}
|
||||||
|
if (typeof note === 'number') {
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
if (!fallbackValue) {
|
||||||
|
throw new Error('valueToMidi: expected freq or note to be set');
|
||||||
|
}
|
||||||
|
return fallbackValue;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||||
*/
|
*/
|
||||||
@@ -50,9 +75,8 @@ export const midi2note = (n) => {
|
|||||||
return pc + oct;
|
return pc + oct;
|
||||||
};
|
};
|
||||||
|
|
||||||
// modulo that works with negative numbers e.g. mod(-1, 3) = 2
|
// modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does)
|
||||||
// const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m);
|
export const _mod = (n, m) => ((n % m) + m) % m;
|
||||||
export const mod = (n, m) => ((n % m) + m) % m;
|
|
||||||
|
|
||||||
export const getPlayableNoteValue = (hap) => {
|
export const getPlayableNoteValue = (hap) => {
|
||||||
let { value, context } = hap;
|
let { value, context } = hap;
|
||||||
@@ -117,9 +141,9 @@ export const constant = (a, b) => a;
|
|||||||
|
|
||||||
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
||||||
|
|
||||||
export function curry(func, overload) {
|
export function curry(func, overload, arity = func.length) {
|
||||||
const fn = function curried(...args) {
|
const fn = function curried(...args) {
|
||||||
if (args.length >= func.length) {
|
if (args.length >= arity) {
|
||||||
return func.apply(this, args);
|
return func.apply(this, args);
|
||||||
} else {
|
} else {
|
||||||
const partial = function (...args2) {
|
const partial = function (...args2) {
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
import { curry } from './util.mjs';
|
import { curry } from './util.mjs';
|
||||||
|
|
||||||
export function unionWithObj(a, b, func) {
|
export function unionWithObj(a, b, func) {
|
||||||
|
if (typeof b?.value === 'number') {
|
||||||
|
// https://github.com/tidalcycles/strudel/issues/262
|
||||||
|
const numKeys = Object.keys(a).filter((k) => typeof a[k] === 'number');
|
||||||
|
const numerals = Object.fromEntries(numKeys.map((k) => [k, b.value]));
|
||||||
|
b = Object.assign(b, numerals);
|
||||||
|
delete b.value;
|
||||||
|
}
|
||||||
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
|
const common = Object.keys(a).filter((k) => Object.keys(b).includes(k));
|
||||||
return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])])));
|
return Object.assign({}, a, b, Object.fromEntries(common.map((k) => [k, func(a[k], b[k])])));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,6 @@ function createClock(
|
|||||||
};
|
};
|
||||||
const getPhase = () => phase;
|
const getPhase = () => phase;
|
||||||
// setCallback
|
// setCallback
|
||||||
return { setDuration, start, stop, pause, duration, getPhase };
|
return { setDuration, start, stop, pause, duration, getPhase, minLatency };
|
||||||
}
|
}
|
||||||
export default createClock;
|
export default createClock;
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { getFrequency, logger, register } from '@strudel.cycles/core';
|
||||||
|
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||||
|
import csd from './project.csd?raw';
|
||||||
|
// import livecodeOrc from './livecode.orc?raw';
|
||||||
|
import presetsOrc from './presets.orc?raw';
|
||||||
|
|
||||||
|
let csoundLoader, _csound;
|
||||||
|
|
||||||
|
// initializes csound + can be used to reevaluate given instrument code
|
||||||
|
export async function loadCSound(code = '') {
|
||||||
|
await init();
|
||||||
|
if (code) {
|
||||||
|
code = `${code}`;
|
||||||
|
// ^ ^
|
||||||
|
// wrapping in backticks makes sure it works when calling as templated function
|
||||||
|
await _csound?.evalCode(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const loadcsound = loadCSound;
|
||||||
|
export const loadCsound = loadCSound;
|
||||||
|
|
||||||
|
export const csound = register('csound', (instrument, pat) => {
|
||||||
|
instrument = instrument || 'triangle';
|
||||||
|
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||||
|
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
||||||
|
return pat.onTrigger((time, hap) => {
|
||||||
|
if (!_csound) {
|
||||||
|
logger('[csound] not loaded yet', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof hap.value !== 'object') {
|
||||||
|
throw new Error('csound only support objects as hap values');
|
||||||
|
}
|
||||||
|
let { gain = 0.8 } = hap.value;
|
||||||
|
gain *= 0.2;
|
||||||
|
|
||||||
|
const freq = Math.round(getFrequency(hap));
|
||||||
|
const controls = Object.entries({ ...hap.value, freq })
|
||||||
|
.flat()
|
||||||
|
.join('/');
|
||||||
|
// TODO: find out how to send a precise ctx based time
|
||||||
|
// http://www.csounds.com/manual/html/i.html
|
||||||
|
const params = [
|
||||||
|
`"${instrument}"`, // p1: instrument name
|
||||||
|
time - getAudioContext().currentTime, //.toFixed(precision), // p2: starting time in arbitrary unit called beats
|
||||||
|
hap.duration + 0, // p3: duration in beats
|
||||||
|
// instrument specific params:
|
||||||
|
freq, //.toFixed(precision), // p4: frequency
|
||||||
|
gain, // p5: gain
|
||||||
|
`"${controls}"`, // p6 controls as string (like superdirt osc message)
|
||||||
|
];
|
||||||
|
const msg = `i ${params.join(' ')}`;
|
||||||
|
_csound.inputMessage(msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function eventLogger(type, args) {
|
||||||
|
const [msg] = args;
|
||||||
|
if (
|
||||||
|
type === 'message' &&
|
||||||
|
(['[commit: HEAD]'].includes(msg) ||
|
||||||
|
msg.startsWith('--Csound version') ||
|
||||||
|
msg.startsWith('libsndfile') ||
|
||||||
|
msg.startsWith('sr =') ||
|
||||||
|
msg.startsWith('0dBFS') ||
|
||||||
|
msg.startsWith('audio buffered') ||
|
||||||
|
msg.startsWith('writing') ||
|
||||||
|
msg.startsWith('SECTION 1:'))
|
||||||
|
) {
|
||||||
|
// ignore
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let logType = 'info';
|
||||||
|
if (msg.startsWith('error:')) {
|
||||||
|
logType = 'error';
|
||||||
|
}
|
||||||
|
logger(`[csound] ${msg || ''}`, logType);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
if (window.__csound__) {
|
||||||
|
// Allows using some other csound instance.
|
||||||
|
// In that case, the external Csound is responsible
|
||||||
|
// for compiling an orchestra and starting to perform.
|
||||||
|
logger('[load] Using external Csound', 'warning');
|
||||||
|
_csound = window.__csound__;
|
||||||
|
return _csound;
|
||||||
|
} else {
|
||||||
|
const { Csound } = await import('@csound/browser');
|
||||||
|
_csound = await Csound({ audioContext: getAudioContext() });
|
||||||
|
_csound.removeAllListeners('message');
|
||||||
|
['message'].forEach((k) => _csound.on(k, (...args) => eventLogger(k, args)));
|
||||||
|
await _csound.setOption('-m0d'); // see -m flag https://csound.com/docs/manual/CommandFlags.html
|
||||||
|
await _csound.setOption('--sample-accurate');
|
||||||
|
await _csound.compileCsdText(csd);
|
||||||
|
// await _csound.compileOrc(livecodeOrc);
|
||||||
|
await _csound.compileOrc(presetsOrc);
|
||||||
|
await _csound.start();
|
||||||
|
return _csound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
csoundLoader = csoundLoader || load();
|
||||||
|
return csoundLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
let orcCache = {};
|
||||||
|
export async function loadOrc(url) {
|
||||||
|
await init();
|
||||||
|
if (typeof url !== 'string') {
|
||||||
|
throw new Error('loadOrc: expected url string');
|
||||||
|
}
|
||||||
|
if (url.startsWith('github:')) {
|
||||||
|
const [_, path] = url.split('github:');
|
||||||
|
url = `https://raw.githubusercontent.com/${path}`;
|
||||||
|
}
|
||||||
|
if (!orcCache[url]) {
|
||||||
|
orcCache[url] = fetch(url)
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((code) => _csound.compileOrc(code));
|
||||||
|
}
|
||||||
|
await orcCache[url];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends notes to Csound for rendering with MIDI semantics. The hap value is
|
||||||
|
* translated to these Csound pfields:
|
||||||
|
*
|
||||||
|
* p1 -- Csound instrument either as a number (1-based, can be a fraction),
|
||||||
|
* or as a string name.
|
||||||
|
* p2 -- time in beats (usually seconds) from start of performance.
|
||||||
|
* p3 -- duration in beats (usually seconds).
|
||||||
|
* p4 -- MIDI key number (as a real number, not an integer but in [0, 127].
|
||||||
|
* p5 -- MIDI velocity (as a real number, not an integer but in [0, 127].
|
||||||
|
* p6 -- Strudel controls, as a string.
|
||||||
|
*/
|
||||||
|
export const csoundm = register('csoundm', (instrument, pat) => {
|
||||||
|
let p1 = instrument;
|
||||||
|
if (typeof instrument === 'string') {
|
||||||
|
p1 = `"{instrument}"`;
|
||||||
|
}
|
||||||
|
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||||
|
return pat.onTrigger((tidal_time, hap) => {
|
||||||
|
if (!_csound) {
|
||||||
|
logger('[csound] not loaded yet', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof hap.value !== 'object') {
|
||||||
|
throw new Error('csound only support objects as hap values');
|
||||||
|
}
|
||||||
|
// Time in seconds counting from now.
|
||||||
|
const p2 = tidal_time - getAudioContext().currentTime;
|
||||||
|
const p3 = hap.duration.valueOf() + 0;
|
||||||
|
const frequency = getFrequency(hap);
|
||||||
|
// Translate frequency to MIDI key number _without_ rounding.
|
||||||
|
const C4 = 261.62558;
|
||||||
|
let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0;
|
||||||
|
const p4 = octave * 12.0 - 36.0;
|
||||||
|
// We prefer floating point precision, but over the MIDI range [0, 127].
|
||||||
|
const p5 = 127 * (hap.context?.velocity ?? 0.9);
|
||||||
|
// The Strudel controls as a string.
|
||||||
|
const p6 = Object.entries({ ...hap.value, frequency })
|
||||||
|
.flat()
|
||||||
|
.join('/');
|
||||||
|
const i_statement = `i ${p1} ${p2} ${p3} ${p4} ${p5} "${p6}"`;
|
||||||
|
console.log('[csoundm]:', i_statement);
|
||||||
|
_csound.inputMessage(i_statement);
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "@strudel.cycles/csound",
|
||||||
|
"version": "0.5.1",
|
||||||
|
"description": "csound bindings for strudel",
|
||||||
|
"main": "csound.mjs",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"No tests present.\" && exit 0"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"tidalcycles",
|
||||||
|
"strudel",
|
||||||
|
"pattern",
|
||||||
|
"livecoding",
|
||||||
|
"algorave"
|
||||||
|
],
|
||||||
|
"author": "Felix Roos <flix91@gmail.com>",
|
||||||
|
"contributors": [
|
||||||
|
"Alex McLean <alex@slab.org>"
|
||||||
|
],
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"@csound/browser": "^6.18.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
; returns value of given key in given "string map"
|
||||||
|
; keymap("freq", "note/c3/freq/220/gain/0.5")
|
||||||
|
; yields "220"
|
||||||
|
opcode keymap, S, SS
|
||||||
|
Skey, Smap xin
|
||||||
|
idelimiter = strindex(Smap, strcat(Skey, "/"))
|
||||||
|
ifrom = idelimiter + strlen(Skey) + 1
|
||||||
|
Svalue = strsub(Smap, ifrom, strlen(Smap))
|
||||||
|
Svalue = strsub(Svalue, 0, strindex(Svalue, "/"))
|
||||||
|
xout Svalue
|
||||||
|
endop
|
||||||
|
|
||||||
|
; TODO add incredibly dope synths
|
||||||
|
instr organ
|
||||||
|
iduration = p3
|
||||||
|
ifreq = p4
|
||||||
|
igain = p5
|
||||||
|
ioct = octcps(ifreq)
|
||||||
|
|
||||||
|
asig = vco2(igain, ifreq, 12, .5) ; my edit
|
||||||
|
kpwm = oscili(.1, 5)
|
||||||
|
asig = vco2(igain, ifreq, 4, .5 + kpwm)
|
||||||
|
asig += vco2(igain/4, ifreq * 2)
|
||||||
|
|
||||||
|
; filter
|
||||||
|
; idepth = 2
|
||||||
|
; acut = transegr:a(0, .005, 0, idepth, .06, -4.2, 0.001, .01, -4.2, 0) ; filter envelope
|
||||||
|
; asig = zdf_2pole(asig, cpsoct(ioct + acut), 0.5)
|
||||||
|
|
||||||
|
; amp envelope
|
||||||
|
iattack = .001
|
||||||
|
irelease = .05
|
||||||
|
asig *= linsegr:a(0, iattack, 1, iduration, 1, irelease, 0)
|
||||||
|
|
||||||
|
out(asig, asig)
|
||||||
|
|
||||||
|
endin
|
||||||
|
|
||||||
|
instr triangle
|
||||||
|
iduration = p3
|
||||||
|
ifreq = p4
|
||||||
|
igain = p5
|
||||||
|
ioct = octcps(ifreq)
|
||||||
|
|
||||||
|
asig = vco2(igain, ifreq, 12, .5)
|
||||||
|
|
||||||
|
; amp envelope
|
||||||
|
iattack = .001
|
||||||
|
irelease = .05
|
||||||
|
asig *= linsegr:a(0, iattack, 1, iduration, 1, irelease, 0)
|
||||||
|
|
||||||
|
out(asig, asig)
|
||||||
|
endin
|
||||||
|
|
||||||
|
instr pad
|
||||||
|
iduration = p3
|
||||||
|
ifreq = p4
|
||||||
|
igain = p5
|
||||||
|
ioct = octcps(ifreq)
|
||||||
|
|
||||||
|
asig = vco2(igain, ifreq, 0)
|
||||||
|
|
||||||
|
; amp envelope
|
||||||
|
iattack = .5
|
||||||
|
irelease = .1
|
||||||
|
asig *= linsegr:a(0, iattack, 1, iduration, 1, irelease, 0)
|
||||||
|
|
||||||
|
idepth = 2
|
||||||
|
acut = transegr:a(0, .005, 0, idepth, .06, -4.2, 0.001, .01, -4.2, 0)
|
||||||
|
asig = zdf_2pole(asig, 1000, 2)
|
||||||
|
|
||||||
|
out(asig, asig)
|
||||||
|
endin
|
||||||
|
|
||||||
|
|
||||||
|
gisine ftgen 0, 0, 4096, 10, 1
|
||||||
|
|
||||||
|
instr bow
|
||||||
|
kpres = 2
|
||||||
|
krat = 0.16
|
||||||
|
kvibf = 6.12723
|
||||||
|
|
||||||
|
kvib linseg 0, 0.5, 0, 1, 1, p3-0.5, 1
|
||||||
|
kvamp = kvib * 0.01
|
||||||
|
asig wgbow .7, p4, kpres, krat, kvibf, kvamp, gisine
|
||||||
|
asig = asig*p5
|
||||||
|
outs asig, asig
|
||||||
|
endin
|
||||||
|
|
||||||
|
|
||||||
|
instr Meta
|
||||||
|
Smap = strget(p6)
|
||||||
|
Sinstrument = keymap("s", Smap)
|
||||||
|
schedule(Sinstrument, 0, p3, p4, p5)
|
||||||
|
; TODO find a way to pipe Sinstrument through effects
|
||||||
|
endin
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<CsoundSynthesizer>
|
||||||
|
<CsInstruments>
|
||||||
|
|
||||||
|
sr=48000
|
||||||
|
ksmps=64
|
||||||
|
nchnls=2
|
||||||
|
0dbfs=1
|
||||||
|
|
||||||
|
</CsInstruments>
|
||||||
|
</CsoundSynthesizer>
|
||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/eval",
|
"name": "@strudel.cycles/eval",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/eval",
|
"name": "@strudel.cycles/eval",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"description": "Code evaluator for strudel",
|
"description": "Code evaluator for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.4.0",
|
"@strudel.cycles/core": "^0.5.0",
|
||||||
"estraverse": "^5.3.0",
|
"estraverse": "^5.3.0",
|
||||||
"shift-ast": "^7.0.0",
|
"shift-ast": "^7.0.0",
|
||||||
"shift-codegen": "^8.1.0",
|
"shift-codegen": "^8.1.0",
|
||||||
|
|||||||
@@ -225,9 +225,8 @@ function isPatternArg(parents) {
|
|||||||
function hasModifierCall(parent) {
|
function hasModifierCall(parent) {
|
||||||
// TODO: modifiers are more than composables, for example every is not composable but should be seen as modifier..
|
// TODO: modifiers are more than composables, for example every is not composable but should be seen as modifier..
|
||||||
// need all prototypes of Pattern
|
// need all prototypes of Pattern
|
||||||
return (
|
return parent?.type === 'StaticMemberExpression';
|
||||||
parent?.type === 'StaticMemberExpression' && Object.keys(Pattern.prototype.composable).includes(parent.property)
|
// && Object.keys(Pattern.prototype.composable).includes(parent.property)
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const factories = Object.keys(Pattern.prototype.factories).concat(['mini']);
|
const factories = Object.keys(Pattern.prototype.factories).concat(['mini']);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const { fastcat, evalScope } = strudel;
|
|||||||
|
|
||||||
describe('evaluate', async () => {
|
describe('evaluate', async () => {
|
||||||
await evalScope({ mini }, strudel);
|
await evalScope({ mini }, strudel);
|
||||||
const ev = async (code) => (await evaluate(code)).pattern._firstCycleValues;
|
const ev = async (code) => (await evaluate(code)).pattern.firstCycleValues;
|
||||||
it('Should evaluate strudel functions', async () => {
|
it('Should evaluate strudel functions', async () => {
|
||||||
expect(await ev('pure("c3")')).toEqual(['c3']);
|
expect(await ev('pure("c3")')).toEqual(['c3']);
|
||||||
expect(await ev('cat("c3")')).toEqual(['c3']);
|
expect(await ev('cat("c3")')).toEqual(['c3']);
|
||||||
|
|||||||
+34
-20
@@ -11,10 +11,14 @@ import { getAudioContext } from '@strudel.cycles/webaudio';
|
|||||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
export const { WebMidi } = _WebMidi;
|
export const { WebMidi } = _WebMidi;
|
||||||
|
|
||||||
|
function supportsMidi() {
|
||||||
|
return typeof navigator.requestMIDIAccess === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
export function enableWebMidi(options = {}) {
|
export function enableWebMidi(options = {}) {
|
||||||
const { onReady, onConnected, onDisconnected } = options;
|
const { onReady, onConnected, onDisconnected } = options;
|
||||||
|
|
||||||
if (typeof navigator.requestMIDIAccess !== 'function') {
|
if (!supportsMidi()) {
|
||||||
throw new Error('Your Browser does not support WebMIDI.');
|
throw new Error('Your Browser does not support WebMIDI.');
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -42,23 +46,43 @@ export function enableWebMidi(options = {}) {
|
|||||||
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
|
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
|
||||||
const outputByName = (name) => WebMidi.getOutputByName(name);
|
const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||||
|
|
||||||
|
let midiReady;
|
||||||
|
|
||||||
|
// output?: string | number, outputs: typeof WebMidi.outputs
|
||||||
|
function getDevice(output, outputs) {
|
||||||
|
if (!outputs.length) {
|
||||||
|
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||||
|
}
|
||||||
|
if (typeof output === 'number') {
|
||||||
|
return outputs[output];
|
||||||
|
}
|
||||||
|
if (typeof output === 'string') {
|
||||||
|
return outputByName(output);
|
||||||
|
}
|
||||||
|
return outputs[0];
|
||||||
|
}
|
||||||
|
|
||||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||||
Pattern.prototype.midi = async function (output, channel = 1) {
|
Pattern.prototype.midi = function (output, channel = 1) {
|
||||||
await enableWebMidi({
|
if (!supportsMidi()) {
|
||||||
|
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||||
|
}
|
||||||
|
/* await */ enableWebMidi({
|
||||||
onConnected: ({ outputs }) =>
|
onConnected: ({ outputs }) =>
|
||||||
logger(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
logger(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
||||||
onDisconnected: ({ outputs }) =>
|
onDisconnected: ({ outputs }) =>
|
||||||
logger(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
logger(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
||||||
onReady: ({ outputs }) => {
|
onReady: ({ outputs }) => {
|
||||||
const chosenOutput = output ?? outputs[0];
|
const device = getDevice(output, outputs);
|
||||||
const otherOutputs = outputs
|
const otherOutputs = outputs
|
||||||
.filter((o) => o.name !== chosenOutput.name)
|
.filter((o) => o.name !== device.name)
|
||||||
.map((o) => `'${o.name}'`)
|
.map((o) => `'${o.name}'`)
|
||||||
.join(' | ');
|
.join(' | ');
|
||||||
logger(`Midi connected! Using "${chosenOutput.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
|
midiReady = true;
|
||||||
|
logger(`Midi connected! Using "${device.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (isPattern(output?.constructor?.name)) {
|
if (isPattern(output)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
`.midi does not accept Pattern input. Make sure to pass device name with single quotes. Example: .midi('${
|
||||||
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
WebMidi.outputs?.[0]?.name || 'IAC Driver Bus 1'
|
||||||
@@ -71,20 +95,10 @@ Pattern.prototype.midi = async function (output, channel = 1) {
|
|||||||
if (!isNote(note)) {
|
if (!isNote(note)) {
|
||||||
throw new Error('not a note: ' + note);
|
throw new Error('not a note: ' + note);
|
||||||
}
|
}
|
||||||
if (!WebMidi.enabled) {
|
if (!midiReady) {
|
||||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
return;
|
||||||
}
|
|
||||||
if (!WebMidi.outputs.length) {
|
|
||||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
|
||||||
}
|
|
||||||
let device;
|
|
||||||
if (typeof output === 'number') {
|
|
||||||
device = WebMidi.outputs[output];
|
|
||||||
} else if (typeof output === 'string') {
|
|
||||||
device = outputByName(output);
|
|
||||||
} else {
|
|
||||||
device = WebMidi.outputs[0];
|
|
||||||
}
|
}
|
||||||
|
const device = getDevice(output, WebMidi.outputs);
|
||||||
if (!device) {
|
if (!device) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/midi",
|
"name": "@strudel.cycles/midi",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/midi",
|
"name": "@strudel.cycles/midi",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"description": "Midi API for strudel",
|
"description": "Midi API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/tone": "^0.4.0",
|
"@strudel.cycles/tone": "^0.5.0",
|
||||||
"tone": "^14.7.77",
|
"tone": "^14.7.77",
|
||||||
"webmidi": "^3.0.21"
|
"webmidi": "^3.0.21"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ yields:
|
|||||||
|
|
||||||
## Mini Notation API
|
## Mini Notation API
|
||||||
|
|
||||||
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
|
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/learn/mini-notation)
|
||||||
|
|
||||||
## Building the Parser
|
## Building the Parser
|
||||||
|
|
||||||
|
|||||||
+482
-394
File diff suppressed because it is too large
Load Diff
+79
-55
@@ -5,13 +5,20 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Some terminology:
|
// Some terminology:
|
||||||
// a sequence = a serie of elements placed between quotes
|
// mini(notation) = a series of elements placed between quotes
|
||||||
// a stack = a serie of vertically aligned slices sharing the same overall length
|
// a stack = a series of vertically aligned slices sharing the same overall length
|
||||||
// a slice = a serie of horizontally aligned elements
|
// a sequence = a series of horizontally aligned elements
|
||||||
// a choose = a serie of elements, one of which is chosen at random
|
// a choose = a series of elements, one of which is chosen at random
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
|
var AtomStub = function(source)
|
||||||
|
{
|
||||||
|
this.type_ = "atom";
|
||||||
|
this.source_ = source;
|
||||||
|
this.location_ = location();
|
||||||
|
}
|
||||||
|
|
||||||
var PatternStub = function(source, alignment)
|
var PatternStub = function(source, alignment)
|
||||||
{
|
{
|
||||||
this.type_ = "pattern";
|
this.type_ = "pattern";
|
||||||
@@ -90,82 +97,99 @@ quote = '"' / "'"
|
|||||||
|
|
||||||
// single step definition (e.g bd)
|
// single step definition (e.g bd)
|
||||||
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
|
step_char = [0-9a-zA-Z~] / "-" / "#" / "." / "^" / "_" / ":"
|
||||||
step = ws chars:step_char+ ws { return chars.join("") }
|
step = ws chars:step_char+ ws { return new AtomStub(chars.join("")) }
|
||||||
|
|
||||||
// define a sub cycle e.g. [1 2, 3 [4]]
|
// define a sub cycle e.g. [1 2, 3 [4]]
|
||||||
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s }
|
sub_cycle = ws "[" ws s:stack_or_choose ws "]" ws { return s }
|
||||||
|
|
||||||
// define a timeline e.g <1 3 [3 5]>. We simply defer to a stack and change the alignement
|
// define a polymeter e.g. {1 2, 3 4 5}
|
||||||
timeline = ws "<" ws sc:single_cycle ws ">" ws
|
polymeter = ws "{" ws s:polymeter_stack ws "}" stepsPerCycle:polymeter_steps? ws
|
||||||
{ sc.arguments_.alignment = "t"; return sc;}
|
{ s.arguments_.stepsPerCycle = stepsPerCycle ; return s; }
|
||||||
|
|
||||||
|
polymeter_steps = "%"a:slice
|
||||||
|
{ return a }
|
||||||
|
|
||||||
|
// define a step-per-cycle timeline e.g <1 3 [3 5]>. We simply defer to a sequence and
|
||||||
|
// change the alignment to slowcat
|
||||||
|
slow_sequence = ws "<" ws s:sequence ws ">" ws
|
||||||
|
{ s.arguments_.alignment = 'slowcat'; return s; }
|
||||||
|
|
||||||
// a slice is either a single step or a sub cycle
|
// a slice is either a single step or a sub cycle
|
||||||
slice = step / sub_cycle / timeline
|
slice = step / sub_cycle / polymeter / slow_sequence
|
||||||
|
|
||||||
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
// slice modifier affects the timing/size of a slice (e.g. [a b c]@3)
|
||||||
// at this point, we assume we can represent them as regular sequence operators
|
// at this point, we assume we can represent them as regular sequence operators
|
||||||
slice_modifier = slice_weight / slice_bjorklund / slice_slow / slice_fast / slice_fixed_step / slice_replicate / slice_degrade
|
slice_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade
|
||||||
|
|
||||||
slice_weight = "@" a:number
|
op_weight = "@" a:number
|
||||||
{ return { weight: a} }
|
{ return x => x.options_['weight'] = a }
|
||||||
|
|
||||||
slice_replicate = "!"a:number
|
op_replicate = "!"a:number
|
||||||
{ return { replicate: a } }
|
{ return x => x.options_['reps'] = a }
|
||||||
|
|
||||||
slice_bjorklund = "(" ws p:number ws comma ws s:number ws comma? ws r:number? ws ")"
|
op_bjorklund = "(" ws p:slice_with_ops ws comma ws s:slice_with_ops ws comma? ws r:slice_with_ops? ws ")"
|
||||||
{ return { operator : { type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r || 0 } } } }
|
{ return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }
|
||||||
|
|
||||||
slice_slow = "/"a:number
|
op_slow = "/"a:slice
|
||||||
{ return { operator : { type_: "stretch", arguments_ :{ amount:a } } } }
|
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) }
|
||||||
|
|
||||||
slice_fast = "*"a:number
|
op_fast = "*"a:slice
|
||||||
{ return { operator : { type_: "stretch", arguments_ :{ amount:"1/"+a } } } }
|
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
|
||||||
|
|
||||||
slice_fixed_step = "%"a:number
|
op_degrade = "?"a:number?
|
||||||
{ return { operator : { type_: "fixed-step", arguments_ :{ amount:a } } } }
|
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a } }) }
|
||||||
|
|
||||||
slice_degrade = "?"a:number?
|
|
||||||
{ return { operator : { type_: "degradeBy", arguments_ :{ amount:(a? a : 0.5) } } } }
|
|
||||||
|
|
||||||
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
// a slice with an modifier applied i.e [bd@4 sd@3]@2 hh]
|
||||||
slice_with_modifier = s:slice o:slice_modifier?
|
slice_with_ops = s:slice ops:slice_op*
|
||||||
{ return new ElementStub(s, o);}
|
{ const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
||||||
|
for (const op of ops) {
|
||||||
|
op(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// a single cycle is a combination of one or more successive slices (as an array). If we
|
// a sequence is a combination of one or more successive slices (as an array)
|
||||||
// have only one element, we skip the array and return the element itself
|
sequence = s:(slice_with_ops)+
|
||||||
single_cycle = s:(slice_with_modifier)+
|
{ return new PatternStub(s, 'fastcat'); }
|
||||||
{ return new PatternStub(s,"h"); }
|
|
||||||
|
|
||||||
// a stack is a serie of vertically aligned single cycles, separated by a comma
|
// a stack is a series of vertically aligned sequence, separated by a comma
|
||||||
stack_tail = tail:(comma @single_cycle)+
|
stack_tail = tail:(comma @sequence)+
|
||||||
{ return { alignment: 'v', list: tail }; }
|
{ return { alignment: 'stack', list: tail }; }
|
||||||
|
|
||||||
// a choose is a serie of pipe-separated single cycles, one of which is chosen
|
// a choose is a series of pipe-separated sequence, one of which is
|
||||||
// at random each time through the pattern
|
// chosen at random, each cycle
|
||||||
choose_tail = tail:(pipe @single_cycle)+
|
choose_tail = tail:(pipe @sequence)+
|
||||||
{ return { alignment: 'r', list: tail }; }
|
{ return { alignment: 'rand', list: tail }; }
|
||||||
|
|
||||||
// if the stack contains only one element, we don't create a stack but return the
|
// if the stack contains only one element, we don't create a stack but return the
|
||||||
// underlying element
|
// underlying element
|
||||||
stack_or_choose = head:single_cycle tail:(stack_tail / choose_tail)?
|
stack_or_choose = head:sequence tail:(stack_tail / choose_tail)?
|
||||||
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
|
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
|
||||||
|
|
||||||
// a sequence is a quoted stack
|
polymeter_stack = head:sequence tail:stack_tail?
|
||||||
sequence = ws quote sc:stack_or_choose quote
|
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
|
||||||
|
|
||||||
|
|
||||||
|
// Mini-notation innards ends
|
||||||
|
// ---------->8---------->8---------->8---------->8---------->8----------
|
||||||
|
// Experimental haskellish parser begins
|
||||||
|
|
||||||
|
// mini-notation = a quoted stack
|
||||||
|
mini = ws quote sc:stack_or_choose quote
|
||||||
{ return sc; }
|
{ return sc; }
|
||||||
|
|
||||||
// ------------------ operators ---------------------------
|
// ------------------ operators ---------------------------
|
||||||
|
|
||||||
operator = scale / slow / fast / target / bjorklund / struct / rotR / rotL
|
operator = scale / slow / fast / target / bjorklund / struct / rotR / rotL
|
||||||
|
|
||||||
struct = "struct" ws s:sequence_or_operator
|
struct = "struct" ws s:mini_or_operator
|
||||||
{ return { name: "struct", args: { sequence:s }}}
|
{ return { name: "struct", args: { mini:s }}}
|
||||||
|
|
||||||
target = "target" ws quote s:step quote
|
target = "target" ws quote s:step quote
|
||||||
{ return { name: "target", args : { name:s}}}
|
{ return { name: "target", args : { name:s}}}
|
||||||
|
|
||||||
bjorklund = "euclid" ws p:int ws s:int ws r:int?
|
bjorklund = "euclid" ws p:int ws s:int ws r:int?
|
||||||
{ return { name: "bjorklund", args :{ pulse: parseInt(p), step:parseInt(s) }}}
|
{ return { name: "bjorklund", args :{ pulse: p, step:parseInt(s) }}}
|
||||||
|
|
||||||
slow = "slow" ws a:number
|
slow = "slow" ws a:number
|
||||||
{ return { name: "stretch", args :{ amount: a}}}
|
{ return { name: "stretch", args :{ amount: a}}}
|
||||||
@@ -189,27 +213,27 @@ comment = '//' p:([^\n]*)
|
|||||||
group_operator = cat
|
group_operator = cat
|
||||||
|
|
||||||
// cat is another form of timeline
|
// cat is another form of timeline
|
||||||
cat = "cat" ws "[" ws s:sequence_or_operator ss:(comma v:sequence_or_operator { return v})* ws "]"
|
cat = "cat" ws "[" ws s:mini_or_operator ss:(comma v:mini_or_operator { return v})* ws "]"
|
||||||
{ ss.unshift(s); return new PatternStub(ss,"t"); }
|
{ ss.unshift(s); return new PatternStub(ss, 'slowcat'); }
|
||||||
|
|
||||||
// ------------------ high level sequence ---------------------------
|
// ------------------ high level mini ---------------------------
|
||||||
|
|
||||||
sequence_or_group =
|
mini_or_group =
|
||||||
group_operator /
|
group_operator /
|
||||||
sequence
|
mini
|
||||||
|
|
||||||
sequence_or_operator =
|
mini_or_operator =
|
||||||
sg:sequence_or_group ws (comment)*
|
sg:mini_or_group ws (comment)*
|
||||||
{return sg}
|
{return sg}
|
||||||
/ o:operator ws "$" ws soc:sequence_or_operator
|
/ o:operator ws "$" ws soc:mini_or_operator
|
||||||
{ return new OperatorStub(o.name,o.args,soc)}
|
{ return new OperatorStub(o.name,o.args,soc)}
|
||||||
|
|
||||||
sequ_or_operator_or_comment =
|
sequ_or_operator_or_comment =
|
||||||
sc: sequence_or_operator
|
sc: mini_or_operator
|
||||||
{ return sc }
|
{ return sc }
|
||||||
/ comment
|
/ comment
|
||||||
|
|
||||||
sequence_definition = s:sequ_or_operator_or_comment
|
mini_definition = s:sequ_or_operator_or_comment
|
||||||
|
|
||||||
// ---------------------- statements ----------------------------
|
// ---------------------- statements ----------------------------
|
||||||
|
|
||||||
@@ -227,4 +251,4 @@ hush = "hush"
|
|||||||
|
|
||||||
// ---------------------- statements ----------------------------
|
// ---------------------- statements ----------------------------
|
||||||
|
|
||||||
statement = sequence_definition / command
|
statement = mini_definition / command
|
||||||
|
|||||||
+115
-92
@@ -6,118 +6,136 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import * as krill from './krill-parser.js';
|
import * as krill from './krill-parser.js';
|
||||||
import * as strudel from '@strudel.cycles/core';
|
import * as strudel from '@strudel.cycles/core';
|
||||||
// import { addMiniLocations } from '@strudel.cycles/eval/shapeshifter.mjs';
|
|
||||||
|
|
||||||
const { pure, Pattern, Fraction, stack, slowcat, sequence, timeCat, silence, reify } = strudel;
|
/* var _seedState = 0;
|
||||||
|
|
||||||
var _seedState = 0;
|
|
||||||
const randOffset = 0.0002;
|
const randOffset = 0.0002;
|
||||||
|
|
||||||
function _nextSeed() {
|
function _nextSeed() {
|
||||||
return _seedState++;
|
return _seedState++;
|
||||||
}
|
} */
|
||||||
|
|
||||||
const applyOptions = (parent) => (pat, i) => {
|
const applyOptions = (parent, code) => (pat, i) => {
|
||||||
const ast = parent.source_[i];
|
const ast = parent.source_[i];
|
||||||
const options = ast.options_;
|
const options = ast.options_;
|
||||||
const operator = options?.operator;
|
const ops = options?.ops;
|
||||||
if (operator) {
|
if (ops) {
|
||||||
switch (operator.type_) {
|
for (const op of ops) {
|
||||||
case 'stretch':
|
switch (op.type_) {
|
||||||
const speed = Fraction(operator.arguments_.amount).inverse();
|
case 'stretch': {
|
||||||
return reify(pat).fast(speed);
|
const legalTypes = ['fast', 'slow'];
|
||||||
case 'bjorklund':
|
const { type, amount } = op.arguments_;
|
||||||
return pat.euclid(operator.arguments_.pulse, operator.arguments_.step, operator.arguments_.rotation);
|
if (!legalTypes.includes(type)) {
|
||||||
case 'degradeBy':
|
throw new Error(`mini: stretch: type must be one of ${legalTypes.join('|')} but got ${type}`);
|
||||||
return reify(pat)._degradeByWith(
|
}
|
||||||
|
pat = strudel.reify(pat)[type](patternifyAST(amount, code));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'bjorklund': {
|
||||||
|
if (op.arguments_.rotation) {
|
||||||
|
pat = pat.euclidRot(
|
||||||
|
patternifyAST(op.arguments_.pulse, code),
|
||||||
|
patternifyAST(op.arguments_.step, code),
|
||||||
|
patternifyAST(op.arguments_.rotation, code),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
pat = pat.euclid(patternifyAST(op.arguments_.pulse, code), patternifyAST(op.arguments_.step, code));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'degradeBy': {
|
||||||
|
// TODO: find out what is right here
|
||||||
|
// example:
|
||||||
|
/*
|
||||||
|
stack(
|
||||||
|
s("hh*8").degrade(),
|
||||||
|
s("[ht*8]?")
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
// above example will only be in sync when _degradeBy is used...
|
||||||
|
// it also seems that the nextSeed will create undeterministic behaviour
|
||||||
|
// as it uses a global _seedState. This is probably the reason for
|
||||||
|
// https://github.com/tidalcycles/strudel/issues/245
|
||||||
|
|
||||||
|
// this is how it was:
|
||||||
|
/*
|
||||||
|
return strudel.reify(pat)._degradeByWith(
|
||||||
strudel.rand.early(randOffset * _nextSeed()).segment(1),
|
strudel.rand.early(randOffset * _nextSeed()).segment(1),
|
||||||
operator.arguments_.amount,
|
op.arguments_.amount ?? 0.5,
|
||||||
);
|
);
|
||||||
// TODO: case 'fixed-step': "%"
|
*/
|
||||||
|
pat = strudel.reify(pat).degradeBy(op.arguments_.amount === null ? 0.5 : op.arguments_.amount);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
console.warn(`operator "${operator.type_}" not implemented`);
|
default: {
|
||||||
|
console.warn(`operator "${op.type_}" not implemented`);
|
||||||
}
|
}
|
||||||
if (options?.weight) {
|
|
||||||
// weight is handled by parent
|
|
||||||
return pat;
|
|
||||||
}
|
}
|
||||||
// TODO: bjorklund e.g. "c3(5,8)"
|
|
||||||
const unimplemented = Object.keys(options || {}).filter((key) => key !== 'operator');
|
|
||||||
if (unimplemented.length) {
|
|
||||||
console.warn(
|
|
||||||
`option${unimplemented.length > 1 ? 's' : ''} ${unimplemented.map((o) => `"${o}"`).join(', ')} not implemented`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return pat;
|
return pat;
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveReplications(ast) {
|
function resolveReplications(ast) {
|
||||||
// the general idea here: x!3 = [x*3]@3
|
ast.source_ = strudel.flatten(
|
||||||
// could this be made easier?!
|
ast.source_.map((child) => {
|
||||||
ast.source_ = ast.source_.map((child) => {
|
const { reps } = child.options_ || {};
|
||||||
const { replicate, ...options } = child.options_ || {};
|
if (!reps) {
|
||||||
if (replicate) {
|
return [child];
|
||||||
return {
|
|
||||||
...child,
|
|
||||||
options_: { ...options, weight: replicate },
|
|
||||||
source_: {
|
|
||||||
type_: 'pattern',
|
|
||||||
arguments_: {
|
|
||||||
alignment: 'h',
|
|
||||||
},
|
|
||||||
source_: [
|
|
||||||
{
|
|
||||||
type_: 'element',
|
|
||||||
source_: child.source_,
|
|
||||||
location_: child.location_,
|
|
||||||
options_: {
|
|
||||||
operator: {
|
|
||||||
type_: 'stretch',
|
|
||||||
arguments_: { amount: Fraction(replicate).inverse().toString() },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return child;
|
delete child.options_.reps;
|
||||||
});
|
return Array(reps).fill(child);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function patternifyAST(ast) {
|
export function patternifyAST(ast, code) {
|
||||||
switch (ast.type_) {
|
switch (ast.type_) {
|
||||||
case 'pattern':
|
case 'pattern': {
|
||||||
resolveReplications(ast);
|
resolveReplications(ast);
|
||||||
const children = ast.source_.map(patternifyAST).map(applyOptions(ast));
|
const children = ast.source_.map((child) => patternifyAST(child, code)).map(applyOptions(ast, code));
|
||||||
const alignment = ast.arguments_.alignment;
|
const alignment = ast.arguments_.alignment;
|
||||||
if (alignment === 'v') {
|
if (alignment === 'stack') {
|
||||||
return stack(...children);
|
return strudel.stack(...children);
|
||||||
}
|
}
|
||||||
if (alignment === 'r') {
|
if (alignment === 'polymeter') {
|
||||||
return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
|
// polymeter
|
||||||
|
const stepsPerCycle = ast.arguments_.stepsPerCycle
|
||||||
|
? patternifyAST(ast.arguments_.stepsPerCycle, code).fmap((x) => strudel.Fraction(x))
|
||||||
|
: strudel.pure(strudel.Fraction(children.length > 0 ? children[0].__weight : 1));
|
||||||
|
|
||||||
|
const aligned = children.map((child) => child.fast(stepsPerCycle.fmap((x) => x.div(child.__weight || 1))));
|
||||||
|
return strudel.stack(...aligned);
|
||||||
|
}
|
||||||
|
if (alignment === 'rand') {
|
||||||
|
// https://github.com/tidalcycles/strudel/issues/245#issuecomment-1345406422
|
||||||
|
// return strudel.chooseInWith(strudel.rand.early(randOffset * _nextSeed()).segment(1), children);
|
||||||
|
return strudel.chooseCycles(...children);
|
||||||
}
|
}
|
||||||
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
const weightedChildren = ast.source_.some((child) => !!child.options_?.weight);
|
||||||
if (!weightedChildren && alignment === 't') {
|
if (!weightedChildren && alignment === 'slowcat') {
|
||||||
return slowcat(...children);
|
return strudel.slowcat(...children);
|
||||||
}
|
}
|
||||||
if (weightedChildren) {
|
if (weightedChildren) {
|
||||||
const pat = timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
|
|
||||||
if (alignment === 't') {
|
|
||||||
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
|
const weightSum = ast.source_.reduce((sum, child) => sum + (child.options_?.weight || 1), 0);
|
||||||
|
const pat = strudel.timeCat(...ast.source_.map((child, i) => [child.options_?.weight || 1, children[i]]));
|
||||||
|
if (alignment === 'slowcat') {
|
||||||
return pat._slow(weightSum); // timecat + slow
|
return pat._slow(weightSum); // timecat + slow
|
||||||
}
|
}
|
||||||
|
pat.__weight = weightSum;
|
||||||
return pat;
|
return pat;
|
||||||
}
|
}
|
||||||
return sequence(...children);
|
const pat = strudel.sequence(...children);
|
||||||
case 'element':
|
pat.__weight = children.length;
|
||||||
if (ast.source_ === '~') {
|
return pat;
|
||||||
return silence;
|
}
|
||||||
|
case 'element': {
|
||||||
|
return patternifyAST(ast.source_, code);
|
||||||
|
}
|
||||||
|
case 'atom': {
|
||||||
|
if (ast.source_ === '~') {
|
||||||
|
return strudel.silence;
|
||||||
}
|
}
|
||||||
if (typeof ast.source_ !== 'object') {
|
|
||||||
/* if (!addMiniLocations) {
|
|
||||||
return ast.source_;
|
|
||||||
} */
|
|
||||||
if (!ast.location_) {
|
if (!ast.location_) {
|
||||||
console.warn('no location for', ast);
|
console.warn('no location for', ast);
|
||||||
return ast.source_;
|
return ast.source_;
|
||||||
@@ -126,11 +144,20 @@ export function patternifyAST(ast) {
|
|||||||
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
|
const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_;
|
||||||
// the following line expects the shapeshifter append .withMiniLocation
|
// the following line expects the shapeshifter append .withMiniLocation
|
||||||
// because location_ is only relative to the mini string, but we need it relative to whole code
|
// because location_ is only relative to the mini string, but we need it relative to whole code
|
||||||
return pure(value).withLocation([start.line, start.column, start.offset], [end.line, end.column, end.offset]);
|
// make sure whitespaces are not part of the highlight:
|
||||||
|
const actual = code?.split('').slice(start.offset, end.offset).join('');
|
||||||
|
const [offsetStart = 0, offsetEnd = 0] = actual
|
||||||
|
? actual.split(ast.source_).map((p) => p.split('').filter((c) => c === ' ').length)
|
||||||
|
: [];
|
||||||
|
return strudel
|
||||||
|
.pure(value)
|
||||||
|
.withLocation(
|
||||||
|
[start.line, start.column + offsetStart, start.offset + offsetStart],
|
||||||
|
[start.line, end.column - offsetEnd, end.offset - offsetEnd],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return patternifyAST(ast.source_);
|
|
||||||
case 'stretch':
|
case 'stretch':
|
||||||
return patternifyAST(ast.source_).slow(ast.arguments_.amount);
|
return patternifyAST(ast.source_, code).slow(patternifyAST(ast.arguments_.amount, code));
|
||||||
/* case 'scale':
|
/* case 'scale':
|
||||||
let [tonic, scale] = Scale.tokenize(ast.arguments_.scale);
|
let [tonic, scale] = Scale.tokenize(ast.arguments_.scale);
|
||||||
const intervals = Scale.get(scale).intervals;
|
const intervals = Scale.get(scale).intervals;
|
||||||
@@ -152,37 +179,33 @@ export function patternifyAST(ast) {
|
|||||||
}); */
|
}); */
|
||||||
/* case 'struct':
|
/* case 'struct':
|
||||||
// TODO:
|
// TODO:
|
||||||
return silence; */
|
return strudel.silence; */
|
||||||
default:
|
default:
|
||||||
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
|
console.warn(`node type "${ast.type_}" not implemented -> returning silence`);
|
||||||
return silence;
|
return strudel.silence;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mini notation only (wraps in "")
|
// mini notation only (wraps in "")
|
||||||
export const mini = (...strings) => {
|
export const mini = (...strings) => {
|
||||||
const pats = strings.map((str) => {
|
const pats = strings.map((str) => {
|
||||||
const ast = krill.parse(`"${str}"`);
|
const code = `"${str}"`;
|
||||||
return patternifyAST(ast);
|
const ast = krill.parse(code);
|
||||||
|
return patternifyAST(ast, code);
|
||||||
});
|
});
|
||||||
return sequence(...pats);
|
return strudel.sequence(...pats);
|
||||||
};
|
};
|
||||||
|
|
||||||
// includes haskell style (raw krill parsing)
|
// includes haskell style (raw krill parsing)
|
||||||
export const h = (string) => {
|
export const h = (string) => {
|
||||||
const ast = krill.parse(string);
|
const ast = krill.parse(string);
|
||||||
// console.log('ast', ast);
|
// console.log('ast', ast);
|
||||||
return patternifyAST(ast);
|
return patternifyAST(ast, string);
|
||||||
};
|
};
|
||||||
|
|
||||||
// shorthand for mini
|
|
||||||
Pattern.prototype.define('mini', mini, { composable: true });
|
|
||||||
Pattern.prototype.define('m', mini, { composable: true });
|
|
||||||
Pattern.prototype.define('h', h, { composable: true });
|
|
||||||
|
|
||||||
export function minify(thing) {
|
export function minify(thing) {
|
||||||
if (typeof thing === 'string') {
|
if (typeof thing === 'string') {
|
||||||
return mini(thing);
|
return mini(thing);
|
||||||
}
|
}
|
||||||
return reify(thing);
|
return strudel.reify(thing);
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/mini",
|
"name": "@strudel.cycles/mini",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/mini",
|
"name": "@strudel.cycles/mini",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"description": "Mini notation for strudel",
|
"description": "Mini notation for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -26,9 +26,9 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.4.0",
|
"@strudel.cycles/core": "^0.5.0",
|
||||||
"@strudel.cycles/eval": "^0.4.0",
|
"@strudel.cycles/eval": "^0.5.0",
|
||||||
"@strudel.cycles/tone": "^0.4.0"
|
"@strudel.cycles/tone": "^0.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"peggy": "^2.0.1"
|
"peggy": "^2.0.1"
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import '@strudel.cycles/core/euclid.mjs';
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('mini', () => {
|
describe('mini', () => {
|
||||||
const minV = (v) => mini(v)._firstCycleValues;
|
const minV = (v) => mini(v).sortHapsByPart().firstCycleValues;
|
||||||
const minS = (v) => mini(v)._showFirstCycle;
|
const minS = (v) => mini(v).sortHapsByPart().showFirstCycle;
|
||||||
it('supports single elements', () => {
|
it('supports single elements', () => {
|
||||||
expect(minV('a')).toEqual(['a']);
|
expect(minV('a')).toEqual(['a']);
|
||||||
});
|
});
|
||||||
@@ -21,6 +21,21 @@ describe('mini', () => {
|
|||||||
expect(minS('a b')).toEqual(['a: 0 - 1/2', 'b: 1/2 - 1']);
|
expect(minS('a b')).toEqual(['a: 0 - 1/2', 'b: 1/2 - 1']);
|
||||||
expect(minS('a b c')).toEqual(['a: 0 - 1/3', 'b: 1/3 - 2/3', 'c: 2/3 - 1']);
|
expect(minS('a b c')).toEqual(['a: 0 - 1/3', 'b: 1/3 - 2/3', 'c: 2/3 - 1']);
|
||||||
});
|
});
|
||||||
|
it('supports fast', () => {
|
||||||
|
expect(minS('a*3 b')).toEqual(minS('[a a a] b'));
|
||||||
|
});
|
||||||
|
it('supports patterned fast', () => {
|
||||||
|
expect(minS('[a*<3 5>]*2')).toEqual(minS('[a a a] [a a a a a]'));
|
||||||
|
});
|
||||||
|
it('supports slow', () => {
|
||||||
|
expect(minS('[a a a]/3 b')).toEqual(minS('a b'));
|
||||||
|
});
|
||||||
|
it('supports patterned slow', () => {
|
||||||
|
expect(minS('[a a a a a a a a]/[2 4]')).toEqual(minS('[a a] a'));
|
||||||
|
});
|
||||||
|
it('supports patterned fast', () => {
|
||||||
|
expect(minS('[a*<3 5>]*2')).toEqual(minS('[a a a] [a a a a a]'));
|
||||||
|
});
|
||||||
it('supports slowcat', () => {
|
it('supports slowcat', () => {
|
||||||
expect(minV('<a b>')).toEqual(['a']);
|
expect(minV('<a b>')).toEqual(['a']);
|
||||||
});
|
});
|
||||||
@@ -36,6 +51,16 @@ describe('mini', () => {
|
|||||||
expect(minS('c3 [d3 e3]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 1']);
|
expect(minS('c3 [d3 e3]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 1']);
|
||||||
expect(minS('c3 [d3 [e3 f3]]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 7/8', 'f3: 7/8 - 1']);
|
expect(minS('c3 [d3 [e3 f3]]')).toEqual(['c3: 0 - 1/2', 'd3: 1/2 - 3/4', 'e3: 3/4 - 7/8', 'f3: 7/8 - 1']);
|
||||||
});
|
});
|
||||||
|
it('supports curly brackets', () => {
|
||||||
|
expect(minS('{a b, c d e}*3')).toEqual(minS('[a b a b a b, c d e c d e]'));
|
||||||
|
expect(minS('{a b, c [d e] f}*3')).toEqual(minS('[a b a b a b, c [d e] f c [d e] f]'));
|
||||||
|
expect(minS('{a b c, d e}*2')).toEqual(minS('[a b c a b c, d e d e d e]'));
|
||||||
|
});
|
||||||
|
it('supports curly brackets with explicit step-per-cycle', () => {
|
||||||
|
expect(minS('{a b, c d e}%3')).toEqual(minS('[a b a, c d e]'));
|
||||||
|
expect(minS('{a b, c d e}%5')).toEqual(minS('[a b a b a, c d e c d]'));
|
||||||
|
expect(minS('{a b, c d e}%6')).toEqual(minS('[a b a b a b, c d e c d e]'));
|
||||||
|
});
|
||||||
it('supports commas', () => {
|
it('supports commas', () => {
|
||||||
expect(minS('c3,e3,g3')).toEqual(['c3: 0 - 1', 'e3: 0 - 1', 'g3: 0 - 1']);
|
expect(minS('c3,e3,g3')).toEqual(['c3: 0 - 1', 'e3: 0 - 1', 'g3: 0 - 1']);
|
||||||
expect(minS('[c3,e3,g3] f3')).toEqual(['c3: 0 - 1/2', 'e3: 0 - 1/2', 'g3: 0 - 1/2', 'f3: 1/2 - 1']);
|
expect(minS('[c3,e3,g3] f3')).toEqual(['c3: 0 - 1/2', 'e3: 0 - 1/2', 'g3: 0 - 1/2', 'f3: 1/2 - 1']);
|
||||||
@@ -46,10 +71,48 @@ describe('mini', () => {
|
|||||||
});
|
});
|
||||||
it('supports replication', () => {
|
it('supports replication', () => {
|
||||||
expect(minS('a!3 b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
|
expect(minS('a!3 b')).toEqual(['a: 0 - 1/4', 'a: 1/4 - 1/2', 'a: 1/2 - 3/4', 'b: 3/4 - 1']);
|
||||||
|
expect(minS('[<a b c>]!3 d')).toEqual(minS('<a b c> <a b c> <a b c> d'));
|
||||||
});
|
});
|
||||||
it('supports euclidean rhythms', () => {
|
it('supports euclidean rhythms', () => {
|
||||||
expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
|
expect(minS('a(3, 8)')).toEqual(['a: 0 - 1/8', 'a: 3/8 - 1/2', 'a: 3/4 - 7/8']);
|
||||||
});
|
});
|
||||||
|
it('supports patterning euclidean rhythms', () => {
|
||||||
|
expect(minS('[a(<3 5>, <8 16>)]*2')).toEqual(minS('a(3,8) a(5,16)'));
|
||||||
|
});
|
||||||
|
it("reproduces Toussaint's example euclidean algorithms", () => {
|
||||||
|
const checkEuclid = function (spec, target) {
|
||||||
|
expect(minS(`x(${spec[0]},${spec[1]})`)).toEqual(minS(target));
|
||||||
|
};
|
||||||
|
checkEuclid([1, 2], 'x ~');
|
||||||
|
checkEuclid([1, 3], 'x ~ ~');
|
||||||
|
checkEuclid([1, 4], 'x ~ ~ ~');
|
||||||
|
checkEuclid([4, 12], 'x ~ ~ x ~ ~ x ~ ~ x ~ ~');
|
||||||
|
checkEuclid([2, 5], 'x ~ x ~ ~');
|
||||||
|
// checkEuclid([3, 4], "x ~ x x"); // Toussaint is wrong..
|
||||||
|
checkEuclid([3, 4], 'x x x ~'); // correction
|
||||||
|
checkEuclid([3, 5], 'x ~ x ~ x');
|
||||||
|
checkEuclid([3, 7], 'x ~ x ~ x ~ ~');
|
||||||
|
checkEuclid([3, 8], 'x ~ ~ x ~ ~ x ~');
|
||||||
|
checkEuclid([4, 7], 'x ~ x ~ x ~ x');
|
||||||
|
checkEuclid([4, 9], 'x ~ x ~ x ~ x ~ ~');
|
||||||
|
checkEuclid([4, 11], 'x ~ ~ x ~ ~ x ~ ~ x ~');
|
||||||
|
// checkEuclid([5, 6], "x ~ x x x x"); // Toussaint is wrong..
|
||||||
|
checkEuclid([5, 6], 'x x x x x ~'); // correction
|
||||||
|
checkEuclid([5, 7], 'x ~ x x ~ x x');
|
||||||
|
checkEuclid([5, 8], 'x ~ x x ~ x x ~');
|
||||||
|
checkEuclid([5, 9], 'x ~ x ~ x ~ x ~ x');
|
||||||
|
checkEuclid([5, 11], 'x ~ x ~ x ~ x ~ x ~ ~');
|
||||||
|
checkEuclid([5, 12], 'x ~ ~ x ~ x ~ ~ x ~ x ~');
|
||||||
|
// checkEuclid([5, 16], "x ~ ~ x ~ ~ x ~ ~ x ~ ~ x ~ ~ ~ ~"); // Toussaint is wrong..
|
||||||
|
checkEuclid([5, 16], 'x ~ ~ x ~ ~ x ~ ~ x ~ ~ x ~ ~ ~'); // correction
|
||||||
|
// checkEuclid([7, 8], "x ~ x x x x x x"); // Toussaint is wrong..
|
||||||
|
checkEuclid([7, 8], 'x x x x x x x ~'); // Correction
|
||||||
|
checkEuclid([7, 12], 'x ~ x x ~ x ~ x x ~ x ~');
|
||||||
|
checkEuclid([7, 16], 'x ~ ~ x ~ x ~ x ~ ~ x ~ x ~ x ~');
|
||||||
|
checkEuclid([9, 16], 'x ~ x x ~ x ~ x ~ x x ~ x ~ x ~');
|
||||||
|
checkEuclid([11, 24], 'x ~ ~ x ~ x ~ x ~ x ~ x ~ ~ x ~ x ~ x ~ x ~ x ~');
|
||||||
|
checkEuclid([13, 24], 'x ~ x x ~ x ~ x ~ x ~ x ~ x x ~ x ~ x ~ x ~ x ~');
|
||||||
|
});
|
||||||
it('supports the ? operator', () => {
|
it('supports the ? operator', () => {
|
||||||
expect(
|
expect(
|
||||||
mini('a?')
|
mini('a?')
|
||||||
@@ -77,7 +140,7 @@ describe('mini', () => {
|
|||||||
expect(haps.length < 230).toBe(true);
|
expect(haps.length < 230).toBe(true);
|
||||||
// 'Had too many cycles remaining after degradeBy 0.8');
|
// 'Had too many cycles remaining after degradeBy 0.8');
|
||||||
});
|
});
|
||||||
it('supports the random choice operator ("|") with nesting', () => {
|
/*it('supports the random choice operator ("|") with nesting', () => {
|
||||||
const numCycles = 900;
|
const numCycles = 900;
|
||||||
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
|
const haps = mini('a | [b | c] | [d | e | f]').queryArc(0, numCycles);
|
||||||
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
|
// Should have about 1/3 a, 1/6 each of b | c, and 1/9 each of d | e | f.
|
||||||
@@ -103,5 +166,5 @@ describe('mini', () => {
|
|||||||
// PRNG, this test should succeed
|
// PRNG, this test should succeed
|
||||||
expect(chisq <= 15.086).toBe(true);
|
expect(chisq <= 15.086).toBe(true);
|
||||||
// assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
|
// assert(chisq <= 15.086, chisq + ' was expected to be less than 15.086 under chi-squared test');
|
||||||
});
|
});*/
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,4 +36,4 @@ s("<bd sd> hh").osc()
|
|||||||
|
|
||||||
or just [click here](http://localhost:3000/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
|
or just [click here](http://localhost:3000/#cygiPGJkIHNkPiBoaCIpLm9zYygp)...
|
||||||
|
|
||||||
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.tidalcycles.org/tutorial/#superdirt-api)
|
You can read more about [how to use Superdirt with Strudel the Tutorial](https://strudel.tidalcycles.org/learn/outputs#superdirt-api)
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/osc",
|
"name": "@strudel.cycles/osc",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/osc",
|
"name": "@strudel.cycles/osc",
|
||||||
"version": "0.3.0",
|
"version": "0.4.0",
|
||||||
"description": "OSC messaging for strudel",
|
"description": "OSC messaging for strudel",
|
||||||
"main": "osc.mjs",
|
"main": "osc.mjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"No tests present.\" && exit 0",
|
"test": "echo \"No tests present.\" && exit 0",
|
||||||
"server": "node server.js",
|
"server": "node server.js",
|
||||||
"tidal-sniffer": "node tidal-sniffer.js",
|
"tidal-sniffer": "node tidal-sniffer.js",
|
||||||
"client": "npx serve -p 4321"
|
"client": "npx serve -p 4321",
|
||||||
|
"build": "npx pkg server.js --targets node16-macos-x64,node16-win-x64,node16-linux-x64 --out-path bin"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -31,5 +32,8 @@
|
|||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"osc-js": "^2.4.0"
|
"osc-js": "^2.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"pkg": "^5.7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ const config = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
|
const osc = new OSC({ plugin: new OSC.BridgePlugin(config) });
|
||||||
|
|
||||||
osc.open(); // start a WebSocket server on port 8080
|
osc.open(); // start a WebSocket server on port 8080
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+321
-230
@@ -1,15 +1,15 @@
|
|||||||
import n, { useCallback as N, useRef as x, useEffect as R, useState as w, useMemo as I, useLayoutEffect as K } from "react";
|
import l, { useCallback as N, useRef as k, useEffect as _, useMemo as K, useState as w, useLayoutEffect as G } from "react";
|
||||||
import Q from "@uiw/react-codemirror";
|
import Z from "@uiw/react-codemirror";
|
||||||
import { Decoration as E, EditorView as O } from "@codemirror/view";
|
import { Decoration as y, EditorView as J } from "@codemirror/view";
|
||||||
import { StateEffect as j, StateField as U } from "@codemirror/state";
|
import { StateEffect as Q, StateField as X } from "@codemirror/state";
|
||||||
import { javascript as W } from "@codemirror/lang-javascript";
|
import { javascript as ee } from "@codemirror/lang-javascript";
|
||||||
import { tags as r } from "@lezer/highlight";
|
import { tags as s } from "@lezer/highlight";
|
||||||
import { createTheme as X } from "@uiw/codemirror-themes";
|
import { createTheme as te } from "@uiw/codemirror-themes";
|
||||||
import { useInView as Y } from "react-hook-inview";
|
import { repl as re, logger as ne, pianoroll as oe } from "@strudel.cycles/core";
|
||||||
import { webaudioOutput as Z, getAudioContext as ee } from "@strudel.cycles/webaudio";
|
import { webaudioOutput as ae, getAudioContext as ce } from "@strudel.cycles/webaudio";
|
||||||
import { repl as te } from "@strudel.cycles/core";
|
import { useInView as se } from "react-hook-inview";
|
||||||
import { transpiler as re } from "@strudel.cycles/transpiler";
|
import { transpiler as ie } from "@strudel.cycles/transpiler";
|
||||||
const oe = X({
|
const le = te({
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
settings: {
|
settings: {
|
||||||
background: "#222",
|
background: "#222",
|
||||||
@@ -22,289 +22,380 @@ const oe = X({
|
|||||||
gutterForeground: "#8a919966"
|
gutterForeground: "#8a919966"
|
||||||
},
|
},
|
||||||
styles: [
|
styles: [
|
||||||
{ tag: r.keyword, color: "#c792ea" },
|
{ tag: s.keyword, color: "#c792ea" },
|
||||||
{ tag: r.operator, color: "#89ddff" },
|
{ tag: s.operator, color: "#89ddff" },
|
||||||
{ tag: r.special(r.variableName), color: "#eeffff" },
|
{ tag: s.special(s.variableName), color: "#eeffff" },
|
||||||
{ tag: r.typeName, color: "#c3e88d" },
|
{ tag: s.typeName, color: "#c3e88d" },
|
||||||
{ tag: r.atom, color: "#f78c6c" },
|
{ tag: s.atom, color: "#f78c6c" },
|
||||||
{ tag: r.number, color: "#c3e88d" },
|
{ tag: s.number, color: "#c3e88d" },
|
||||||
{ tag: r.definition(r.variableName), color: "#82aaff" },
|
{ tag: s.definition(s.variableName), color: "#82aaff" },
|
||||||
{ tag: r.string, color: "#c3e88d" },
|
{ tag: s.string, color: "#c3e88d" },
|
||||||
{ tag: r.special(r.string), color: "#c3e88d" },
|
{ tag: s.special(s.string), color: "#c3e88d" },
|
||||||
{ tag: r.comment, color: "#7d8799" },
|
{ tag: s.comment, color: "#7d8799" },
|
||||||
{ tag: r.variableName, color: "#c792ea" },
|
{ tag: s.variableName, color: "#c792ea" },
|
||||||
{ tag: r.tagName, color: "#c3e88d" },
|
{ tag: s.tagName, color: "#c3e88d" },
|
||||||
{ tag: r.bracket, color: "#525154" },
|
{ tag: s.bracket, color: "#525154" },
|
||||||
{ tag: r.meta, color: "#ffcb6b" },
|
{ tag: s.meta, color: "#ffcb6b" },
|
||||||
{ tag: r.attributeName, color: "#c792ea" },
|
{ tag: s.attributeName, color: "#c792ea" },
|
||||||
{ tag: r.propertyName, color: "#c792ea" },
|
{ tag: s.propertyName, color: "#c792ea" },
|
||||||
{ tag: r.className, color: "#decb6b" },
|
{ tag: s.className, color: "#decb6b" },
|
||||||
{ tag: r.invalid, color: "#ffffff" }
|
{ tag: s.invalid, color: "#ffffff" }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
const T = j.define(), ne = U.define({
|
const j = Q.define(), ue = X.define({
|
||||||
create() {
|
create() {
|
||||||
return E.none;
|
return y.none;
|
||||||
},
|
},
|
||||||
update(e, t) {
|
update(e, r) {
|
||||||
try {
|
try {
|
||||||
for (let o of t.effects)
|
for (let t of r.effects)
|
||||||
if (o.is(T))
|
if (t.is(j))
|
||||||
if (o.value) {
|
if (t.value) {
|
||||||
const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } });
|
const a = y.mark({ attributes: { style: "background-color: #FFCA2880" } });
|
||||||
e = E.set([a.range(0, t.newDoc.length)]);
|
e = y.set([a.range(0, r.newDoc.length)]);
|
||||||
} else
|
} else
|
||||||
e = E.set([]);
|
e = y.set([]);
|
||||||
return e;
|
return e;
|
||||||
} catch (o) {
|
} catch (t) {
|
||||||
return console.warn("flash error", o), e;
|
return console.warn("flash error", t), e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
provide: (e) => O.decorations.from(e)
|
provide: (e) => J.decorations.from(e)
|
||||||
}), ae = (e) => {
|
}), de = (e) => {
|
||||||
e.dispatch({ effects: T.of(!0) }), setTimeout(() => {
|
e.dispatch({ effects: j.of(!0) }), setTimeout(() => {
|
||||||
e.dispatch({ effects: T.of(!1) });
|
e.dispatch({ effects: j.of(!1) });
|
||||||
}, 200);
|
}, 200);
|
||||||
}, A = j.define(), se = U.define({
|
}, z = Q.define(), fe = X.define({
|
||||||
create() {
|
create() {
|
||||||
return E.none;
|
return y.none;
|
||||||
},
|
},
|
||||||
update(e, t) {
|
update(e, r) {
|
||||||
try {
|
try {
|
||||||
for (let o of t.effects)
|
for (let t of r.effects)
|
||||||
if (o.is(A)) {
|
if (t.is(z)) {
|
||||||
const a = o.value.map(
|
const a = t.value.map(
|
||||||
(s) => (s.context.locations || []).map(({ start: m, end: l }) => {
|
(o) => (o.context.locations || []).map(({ start: i, end: u }) => {
|
||||||
const d = s.context.color || "#FFCA28";
|
const m = o.context.color || "#FFCA28";
|
||||||
let c = t.newDoc.line(m.line).from + m.column, i = t.newDoc.line(l.line).from + l.column;
|
let n = r.newDoc.line(i.line).from + i.column, d = r.newDoc.line(u.line).from + u.column;
|
||||||
const g = t.newDoc.length;
|
const v = r.newDoc.length;
|
||||||
return c > g || i > g ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${d};` } }).range(c, i);
|
return n > v || d > v ? void 0 : y.mark({ attributes: { style: `outline: 1.5px solid ${m};` } }).range(n, d);
|
||||||
})
|
})
|
||||||
).flat().filter(Boolean) || [];
|
).flat().filter(Boolean) || [];
|
||||||
e = E.set(a, !0);
|
e = y.set(a, !0);
|
||||||
}
|
}
|
||||||
return e;
|
return e;
|
||||||
} catch {
|
} catch {
|
||||||
return E.set([]);
|
return y.set([]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
provide: (e) => O.decorations.from(e)
|
provide: (e) => J.decorations.from(e)
|
||||||
}), ce = [W(), oe, se, ne];
|
}), me = [ee(), le, fe, ue];
|
||||||
function ie({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: m }) {
|
function ge({ value: e, onChange: r, onViewChanged: t, onSelectionChange: a, options: o, editorDidMount: i }) {
|
||||||
const l = N(
|
const u = N(
|
||||||
(i) => {
|
(d) => {
|
||||||
t?.(i);
|
r?.(d);
|
||||||
|
},
|
||||||
|
[r]
|
||||||
|
), m = N(
|
||||||
|
(d) => {
|
||||||
|
t?.(d);
|
||||||
},
|
},
|
||||||
[t]
|
[t]
|
||||||
), d = N(
|
), n = N(
|
||||||
(i) => {
|
(d) => {
|
||||||
o?.(i);
|
d.selectionSet && a && a?.(d.state.selection);
|
||||||
},
|
|
||||||
[o]
|
|
||||||
), c = N(
|
|
||||||
(i) => {
|
|
||||||
i.selectionSet && a && a?.(i.state.selection);
|
|
||||||
},
|
},
|
||||||
[a]
|
[a]
|
||||||
);
|
);
|
||||||
return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(Q, {
|
return /* @__PURE__ */ l.createElement(l.Fragment, null, /* @__PURE__ */ l.createElement(Z, {
|
||||||
value: e,
|
value: e,
|
||||||
onChange: l,
|
onChange: u,
|
||||||
onCreateEditor: d,
|
onCreateEditor: m,
|
||||||
onUpdate: c,
|
onUpdate: n,
|
||||||
extensions: ce
|
extensions: me
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
function B(...e) {
|
function W(...e) {
|
||||||
return e.filter(Boolean).join(" ");
|
return e.filter(Boolean).join(" ");
|
||||||
}
|
}
|
||||||
function le({ view: e, pattern: t, active: o, getTime: a }) {
|
function pe({ view: e, pattern: r, active: t, getTime: a }) {
|
||||||
const s = x([]), m = x();
|
const o = k([]), i = k();
|
||||||
R(() => {
|
_(() => {
|
||||||
if (e)
|
if (e)
|
||||||
if (t && o) {
|
if (r && t) {
|
||||||
let d = function() {
|
let u = requestAnimationFrame(function m() {
|
||||||
try {
|
try {
|
||||||
const c = a(), g = [Math.max(m.current || c, c - 1 / 10, 0), c + 1 / 60];
|
const n = a(), v = [Math.max(i.current || n, n - 1 / 10, 0), n + 1 / 60];
|
||||||
m.current = g[1], s.current = s.current.filter((p) => p.whole.end > c);
|
i.current = v[1], o.current = o.current.filter((p) => p.whole.end > n);
|
||||||
const v = t.queryArc(...g).filter((p) => p.hasOnset());
|
const h = r.queryArc(...v).filter((p) => p.hasOnset());
|
||||||
s.current = s.current.concat(v), e.dispatch({ effects: A.of(s.current) });
|
o.current = o.current.concat(h), e.dispatch({ effects: z.of(o.current) });
|
||||||
} catch {
|
} catch {
|
||||||
e.dispatch({ effects: A.of([]) });
|
e.dispatch({ effects: z.of([]) });
|
||||||
}
|
}
|
||||||
l = requestAnimationFrame(d);
|
u = requestAnimationFrame(m);
|
||||||
}, l = requestAnimationFrame(d);
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(l);
|
cancelAnimationFrame(u);
|
||||||
};
|
};
|
||||||
} else
|
} else
|
||||||
s.current = [], e.dispatch({ effects: A.of([]) });
|
o.current = [], e.dispatch({ effects: z.of([]) });
|
||||||
}, [t, o, e]);
|
}, [r, t, e]);
|
||||||
}
|
}
|
||||||
const de = "_container_3i85k_1", ue = "_header_3i85k_5", fe = "_buttons_3i85k_9", me = "_button_3i85k_9", ge = "_buttonDisabled_3i85k_17", pe = "_error_3i85k_21", he = "_body_3i85k_25", b = {
|
function he(e, r = !1) {
|
||||||
container: de,
|
const t = k(), a = k(), o = (m) => {
|
||||||
header: ue,
|
if (a.current !== void 0) {
|
||||||
buttons: fe,
|
const n = m - a.current;
|
||||||
button: me,
|
e(m, n);
|
||||||
buttonDisabled: ge,
|
}
|
||||||
error: pe,
|
a.current = m, t.current = requestAnimationFrame(o);
|
||||||
body: he
|
}, i = () => {
|
||||||
|
t.current = requestAnimationFrame(o);
|
||||||
|
}, u = () => {
|
||||||
|
t.current && cancelAnimationFrame(t.current), delete t.current;
|
||||||
};
|
};
|
||||||
function q({ type: e }) {
|
return _(() => {
|
||||||
return /* @__PURE__ */ n.createElement("svg", {
|
t.current && (u(), i());
|
||||||
|
}, [e]), _(() => (r && i(), u), []), {
|
||||||
|
start: i,
|
||||||
|
stop: u
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function ve({ pattern: e, started: r, getTime: t, onDraw: a }) {
|
||||||
|
let o = k([]), i = k(null);
|
||||||
|
const { start: u, stop: m } = he(
|
||||||
|
N(() => {
|
||||||
|
const n = t();
|
||||||
|
if (i.current === null) {
|
||||||
|
i.current = n;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const d = e.queryArc(Math.max(i.current, n - 1 / 10), n), v = 4;
|
||||||
|
i.current = n, o.current = (o.current || []).filter((h) => h.whole.end > n - v).concat(d.filter((h) => h.hasOnset())), a(n, o.current);
|
||||||
|
}, [e])
|
||||||
|
);
|
||||||
|
_(() => {
|
||||||
|
r ? u() : (o.current = [], m());
|
||||||
|
}, [r]);
|
||||||
|
}
|
||||||
|
function be(e) {
|
||||||
|
return _(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((r) => window.postMessage(r, "*"), []);
|
||||||
|
}
|
||||||
|
function Ee({
|
||||||
|
defaultOutput: e,
|
||||||
|
interval: r,
|
||||||
|
getTime: t,
|
||||||
|
evalOnMount: a = !1,
|
||||||
|
initialCode: o = "",
|
||||||
|
autolink: i = !1,
|
||||||
|
beforeEval: u,
|
||||||
|
afterEval: m,
|
||||||
|
editPattern: n,
|
||||||
|
onEvalError: d,
|
||||||
|
onToggle: v,
|
||||||
|
canvasId: h
|
||||||
|
}) {
|
||||||
|
const p = K(() => we(), []);
|
||||||
|
h = h || `canvas-${p}`;
|
||||||
|
const [F, A] = w(), [P, D] = w(), [b, q] = w(o), [x, V] = w(), [I, R] = w(), [L, B] = w(!1), H = b !== x, { scheduler: M, evaluate: c, start: f, stop: C, pause: O } = K(
|
||||||
|
() => re({
|
||||||
|
interval: r,
|
||||||
|
defaultOutput: e,
|
||||||
|
onSchedulerError: A,
|
||||||
|
onEvalError: (g) => {
|
||||||
|
D(g), d?.(g);
|
||||||
|
},
|
||||||
|
getTime: t,
|
||||||
|
transpiler: ie,
|
||||||
|
beforeEval: ({ code: g }) => {
|
||||||
|
q(g), u?.();
|
||||||
|
},
|
||||||
|
editPattern: n ? (g) => n(g, p) : void 0,
|
||||||
|
afterEval: ({ pattern: g, code: T }) => {
|
||||||
|
V(T), R(g), D(), A(), i && (window.location.hash = "#" + encodeURIComponent(btoa(T))), m?.();
|
||||||
|
},
|
||||||
|
onToggle: (g) => {
|
||||||
|
B(g), v?.(g);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[e, r, t]
|
||||||
|
), Y = be(({ data: { from: g, type: T } }) => {
|
||||||
|
T === "start" && g !== p && C();
|
||||||
|
}), S = N(
|
||||||
|
async (g = !0) => {
|
||||||
|
await c(b, g), Y({ type: "start", from: p });
|
||||||
|
},
|
||||||
|
[c, b]
|
||||||
|
), U = k();
|
||||||
|
return _(() => {
|
||||||
|
!U.current && a && b && (U.current = !0, S());
|
||||||
|
}, [S, a, b]), _(() => () => {
|
||||||
|
M.stop();
|
||||||
|
}, [M]), {
|
||||||
|
id: p,
|
||||||
|
canvasId: h,
|
||||||
|
code: b,
|
||||||
|
setCode: q,
|
||||||
|
error: F || P,
|
||||||
|
schedulerError: F,
|
||||||
|
scheduler: M,
|
||||||
|
evalError: P,
|
||||||
|
evaluate: c,
|
||||||
|
activateCode: S,
|
||||||
|
activeCode: x,
|
||||||
|
isDirty: H,
|
||||||
|
pattern: I,
|
||||||
|
started: L,
|
||||||
|
start: f,
|
||||||
|
stop: C,
|
||||||
|
pause: O,
|
||||||
|
togglePlay: async () => {
|
||||||
|
L ? M.pause() : await S();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function we() {
|
||||||
|
return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1);
|
||||||
|
}
|
||||||
|
function $({ type: e }) {
|
||||||
|
return /* @__PURE__ */ l.createElement("svg", {
|
||||||
xmlns: "http://www.w3.org/2000/svg",
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
className: "sc-h-5 sc-w-5",
|
className: "sc-h-5 sc-w-5",
|
||||||
viewBox: "0 0 20 20",
|
viewBox: "0 0 20 20",
|
||||||
fill: "currentColor"
|
fill: "currentColor"
|
||||||
}, {
|
}, {
|
||||||
refresh: /* @__PURE__ */ n.createElement("path", {
|
refresh: /* @__PURE__ */ l.createElement("path", {
|
||||||
fillRule: "evenodd",
|
fillRule: "evenodd",
|
||||||
d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",
|
d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",
|
||||||
clipRule: "evenodd"
|
clipRule: "evenodd"
|
||||||
}),
|
}),
|
||||||
play: /* @__PURE__ */ n.createElement("path", {
|
play: /* @__PURE__ */ l.createElement("path", {
|
||||||
fillRule: "evenodd",
|
fillRule: "evenodd",
|
||||||
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",
|
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",
|
||||||
clipRule: "evenodd"
|
clipRule: "evenodd"
|
||||||
}),
|
}),
|
||||||
pause: /* @__PURE__ */ n.createElement("path", {
|
pause: /* @__PURE__ */ l.createElement("path", {
|
||||||
fillRule: "evenodd",
|
fillRule: "evenodd",
|
||||||
d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",
|
d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",
|
||||||
clipRule: "evenodd"
|
clipRule: "evenodd"
|
||||||
})
|
})
|
||||||
}[e]);
|
}[e]);
|
||||||
}
|
}
|
||||||
function ve({
|
const ye = "_container_3i85k_1", ke = "_header_3i85k_5", _e = "_buttons_3i85k_9", Fe = "_button_3i85k_9", Ce = "_buttonDisabled_3i85k_17", Ne = "_error_3i85k_21", xe = "_body_3i85k_25", E = {
|
||||||
defaultOutput: e,
|
container: ye,
|
||||||
interval: t,
|
header: ke,
|
||||||
getTime: o,
|
buttons: _e,
|
||||||
evalOnMount: a = !1,
|
button: Fe,
|
||||||
initialCode: s = "",
|
buttonDisabled: Ce,
|
||||||
autolink: m = !1,
|
error: Ne,
|
||||||
beforeEval: l,
|
body: xe
|
||||||
afterEval: d,
|
}, Me = () => ce().currentTime;
|
||||||
onEvalError: c,
|
function je({ tune: e, hideOutsideView: r = !1, enableKeyboard: t, withCanvas: a = !1, canvasHeight: o = 200 }) {
|
||||||
onToggle: i
|
|
||||||
}) {
|
|
||||||
const [g, v] = w(), [p, D] = w(), [h, k] = w(s), [y, P] = w(h), [z, _] = w(), [C, H] = w(!1), F = h !== y, { scheduler: u, evaluate: L, start: $, stop: G, pause: J } = I(
|
|
||||||
() => te({
|
|
||||||
interval: t,
|
|
||||||
defaultOutput: e,
|
|
||||||
onSchedulerError: v,
|
|
||||||
onEvalError: (f) => {
|
|
||||||
D(f), c?.(f);
|
|
||||||
},
|
|
||||||
getTime: o,
|
|
||||||
transpiler: re,
|
|
||||||
beforeEval: ({ code: f }) => {
|
|
||||||
k(f), l?.();
|
|
||||||
},
|
|
||||||
afterEval: ({ pattern: f, code: S }) => {
|
|
||||||
P(S), _(f), D(), v(), m && (window.location.hash = "#" + encodeURIComponent(btoa(S))), d?.();
|
|
||||||
},
|
|
||||||
onToggle: (f) => {
|
|
||||||
H(f), i?.(f);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[e, t, o]
|
|
||||||
), M = N(async (f = !0) => L(h, f), [L, h]), V = x();
|
|
||||||
return R(() => {
|
|
||||||
!V.current && a && h && (V.current = !0, M());
|
|
||||||
}, [M, a, h]), R(() => () => {
|
|
||||||
u.stop();
|
|
||||||
}, [u]), {
|
|
||||||
code: h,
|
|
||||||
setCode: k,
|
|
||||||
error: g || p,
|
|
||||||
schedulerError: g,
|
|
||||||
scheduler: u,
|
|
||||||
evalError: p,
|
|
||||||
evaluate: L,
|
|
||||||
activateCode: M,
|
|
||||||
activeCode: y,
|
|
||||||
isDirty: F,
|
|
||||||
pattern: z,
|
|
||||||
started: C,
|
|
||||||
start: $,
|
|
||||||
stop: G,
|
|
||||||
pause: J,
|
|
||||||
togglePlay: async () => {
|
|
||||||
C ? u.pause() : await M();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const be = () => ee().currentTime;
|
|
||||||
function Pe({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) {
|
|
||||||
const {
|
const {
|
||||||
code: s,
|
code: i,
|
||||||
setCode: m,
|
setCode: u,
|
||||||
evaluate: l,
|
evaluate: m,
|
||||||
activateCode: d,
|
activateCode: n,
|
||||||
error: c,
|
error: d,
|
||||||
isDirty: i,
|
isDirty: v,
|
||||||
activeCode: g,
|
activeCode: h,
|
||||||
pattern: v,
|
pattern: p,
|
||||||
started: p,
|
started: F,
|
||||||
scheduler: D,
|
scheduler: A,
|
||||||
togglePlay: h,
|
togglePlay: P,
|
||||||
stop: k
|
stop: D,
|
||||||
} = ve({
|
canvasId: b,
|
||||||
|
id: q
|
||||||
|
} = Ee({
|
||||||
initialCode: e,
|
initialCode: e,
|
||||||
defaultOutput: Z,
|
defaultOutput: ae,
|
||||||
getTime: be
|
getTime: Me,
|
||||||
}), [y, P] = w(), [z, _] = Y({
|
editPattern: (c, f) => c.withContext((C) => ({ ...C, id: f }))
|
||||||
|
});
|
||||||
|
ve({
|
||||||
|
pattern: p,
|
||||||
|
started: a && F,
|
||||||
|
getTime: () => A.now(),
|
||||||
|
onDraw: (c, f) => {
|
||||||
|
const C = document.querySelector("#" + b).getContext("2d");
|
||||||
|
oe({ ctx: C, time: c, haps: f, autorange: 1, fold: 1, playhead: 1 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const [x, V] = w(), [I, R] = se({
|
||||||
threshold: 0.01
|
threshold: 0.01
|
||||||
}), C = x(), H = I(() => ((_ || !t) && (C.current = !0), _ || C.current), [_, t]);
|
}), L = k(), B = K(() => ((R || !r) && (L.current = !0), R || L.current), [R, r]);
|
||||||
return le({
|
pe({
|
||||||
view: y,
|
view: x,
|
||||||
pattern: v,
|
pattern: p,
|
||||||
active: p && !g?.includes("strudel disable-highlighting"),
|
active: F && !h?.includes("strudel disable-highlighting"),
|
||||||
getTime: () => D.getPhase()
|
getTime: () => A.getPhase()
|
||||||
}), K(() => {
|
}), G(() => {
|
||||||
if (a) {
|
if (t) {
|
||||||
const F = async (u) => {
|
const c = async (f) => {
|
||||||
(u.ctrlKey || u.altKey) && (u.code === "Enter" ? (u.preventDefault(), ae(y), await d()) : u.code === "Period" && (k(), u.preventDefault()));
|
(f.ctrlKey || f.altKey) && (f.code === "Enter" ? (f.preventDefault(), de(x), await n()) : f.code === "Period" && (D(), f.preventDefault()));
|
||||||
};
|
};
|
||||||
return window.addEventListener("keydown", F, !0), () => window.removeEventListener("keydown", F, !0);
|
return window.addEventListener("keydown", c, !0), () => window.removeEventListener("keydown", c, !0);
|
||||||
}
|
}
|
||||||
}, [a, v, s, l, k, y]), /* @__PURE__ */ n.createElement("div", {
|
}, [t, p, i, m, D, x]);
|
||||||
className: b.container,
|
const [H, M] = w([]);
|
||||||
ref: z
|
return Ae(
|
||||||
}, /* @__PURE__ */ n.createElement("div", {
|
N((c) => {
|
||||||
className: b.header
|
const { data: f } = c.detail;
|
||||||
}, /* @__PURE__ */ n.createElement("div", {
|
f?.hap?.context?.id === q && M((O) => O.concat([c.detail]).slice(-10));
|
||||||
className: b.buttons
|
}, [])
|
||||||
}, /* @__PURE__ */ n.createElement("button", {
|
), /* @__PURE__ */ l.createElement("div", {
|
||||||
className: B(b.button, p ? "sc-animate-pulse" : ""),
|
className: E.container,
|
||||||
onClick: () => h()
|
ref: I
|
||||||
}, /* @__PURE__ */ n.createElement(q, {
|
}, /* @__PURE__ */ l.createElement("div", {
|
||||||
type: p ? "pause" : "play"
|
className: E.header
|
||||||
})), /* @__PURE__ */ n.createElement("button", {
|
}, /* @__PURE__ */ l.createElement("div", {
|
||||||
className: B(i ? b.button : b.buttonDisabled),
|
className: E.buttons
|
||||||
onClick: () => d()
|
}, /* @__PURE__ */ l.createElement("button", {
|
||||||
}, /* @__PURE__ */ n.createElement(q, {
|
className: W(E.button, F ? "sc-animate-pulse" : ""),
|
||||||
|
onClick: () => P()
|
||||||
|
}, /* @__PURE__ */ l.createElement($, {
|
||||||
|
type: F ? "pause" : "play"
|
||||||
|
})), /* @__PURE__ */ l.createElement("button", {
|
||||||
|
className: W(v ? E.button : E.buttonDisabled),
|
||||||
|
onClick: () => n()
|
||||||
|
}, /* @__PURE__ */ l.createElement($, {
|
||||||
type: "refresh"
|
type: "refresh"
|
||||||
}))), c && /* @__PURE__ */ n.createElement("div", {
|
}))), d && /* @__PURE__ */ l.createElement("div", {
|
||||||
className: b.error
|
className: E.error
|
||||||
}, c.message)), /* @__PURE__ */ n.createElement("div", {
|
}, d.message)), /* @__PURE__ */ l.createElement("div", {
|
||||||
className: b.body
|
className: E.body
|
||||||
}, H && /* @__PURE__ */ n.createElement(ie, {
|
}, B && /* @__PURE__ */ l.createElement(ge, {
|
||||||
value: s,
|
value: i,
|
||||||
onChange: m,
|
onChange: u,
|
||||||
onViewChanged: P
|
onViewChanged: V
|
||||||
})));
|
})), a && /* @__PURE__ */ l.createElement("canvas", {
|
||||||
|
id: b,
|
||||||
|
className: "w-full pointer-events-none",
|
||||||
|
height: o,
|
||||||
|
ref: (c) => {
|
||||||
|
c && c.width !== c.clientWidth && (c.width = c.clientWidth);
|
||||||
}
|
}
|
||||||
function ze(e) {
|
}), !!H.length && /* @__PURE__ */ l.createElement("div", {
|
||||||
return R(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((t) => window.postMessage(t, "*"), []);
|
className: "sc-bg-gray-800 sc-rounded-md sc-p-2"
|
||||||
|
}, H.map(({ message: c }, f) => /* @__PURE__ */ l.createElement("div", {
|
||||||
|
key: f
|
||||||
|
}, c))));
|
||||||
}
|
}
|
||||||
const He = (e) => K(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
|
function Ae(e) {
|
||||||
|
De(ne.key, e);
|
||||||
|
}
|
||||||
|
function De(e, r, t = !1) {
|
||||||
|
_(() => (document.addEventListener(e, r, t), () => {
|
||||||
|
document.removeEventListener(e, r, t);
|
||||||
|
}), [r]);
|
||||||
|
}
|
||||||
|
const Ue = (e) => G(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]);
|
||||||
export {
|
export {
|
||||||
ie as CodeMirror,
|
ge as CodeMirror,
|
||||||
Pe as MiniRepl,
|
je as MiniRepl,
|
||||||
B as cx,
|
W as cx,
|
||||||
ae as flash,
|
de as flash,
|
||||||
le as useHighlighting,
|
pe as useHighlighting,
|
||||||
He as useKeydown,
|
Ue as useKeydown,
|
||||||
ze as usePostMessage,
|
be as usePostMessage,
|
||||||
ve as useStrudel
|
Ee as useStrudel
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:18px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto}
|
.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:18px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}.sc-rounded-md{border-radius:.375rem}.sc-bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}.sc-p-2{padding:.5rem}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()]
|
plugins: [react()],
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/react",
|
"name": "@strudel.cycles/react",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"description": "React components for strudel",
|
"description": "React components for strudel",
|
||||||
"main": "dist/index.cjs.js",
|
"main": "dist/index.cjs.js",
|
||||||
"module": "dist/index.es.js",
|
"module": "dist/index.es.js",
|
||||||
@@ -39,10 +39,10 @@
|
|||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-javascript": "^6.1.1",
|
"@codemirror/lang-javascript": "^6.1.1",
|
||||||
"@strudel.cycles/core": "^0.4.0",
|
"@strudel.cycles/core": "^0.5.0",
|
||||||
"@strudel.cycles/tone": "^0.4.0",
|
"@strudel.cycles/tone": "^0.5.0",
|
||||||
"@strudel.cycles/transpiler": "^0.4.0",
|
"@strudel.cycles/transpiler": "^0.5.0",
|
||||||
"@strudel.cycles/webaudio": "^0.4.0",
|
"@strudel.cycles/webaudio": "^0.5.0",
|
||||||
"@uiw/codemirror-themes": "^4.12.4",
|
"@uiw/codemirror-themes": "^4.12.4",
|
||||||
"@uiw/react-codemirror": "^4.12.4",
|
"@uiw/react-codemirror": "^4.12.4",
|
||||||
"react-hook-inview": "^4.5.0"
|
"react-hook-inview": "^4.5.0"
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ module.exports = {
|
|||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react';
|
import { pianoroll } from '@strudel.cycles/core';
|
||||||
|
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||||
|
import React, { useLayoutEffect, useMemo, useRef, useState, useCallback, useEffect } from 'react';
|
||||||
import { useInView } from 'react-hook-inview';
|
import { useInView } from 'react-hook-inview';
|
||||||
|
import 'tailwindcss/tailwind.css';
|
||||||
import cx from '../cx';
|
import cx from '../cx';
|
||||||
import useHighlighting from '../hooks/useHighlighting.mjs';
|
import useHighlighting from '../hooks/useHighlighting.mjs';
|
||||||
import CodeMirror6, { flash } from './CodeMirror6';
|
import usePatternFrame from '../hooks/usePatternFrame.mjs';
|
||||||
import 'tailwindcss/tailwind.css';
|
|
||||||
import './style.css';
|
|
||||||
import styles from './MiniRepl.module.css';
|
|
||||||
import { Icon } from './Icon';
|
|
||||||
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
|
|
||||||
import useStrudel from '../hooks/useStrudel.mjs';
|
import useStrudel from '../hooks/useStrudel.mjs';
|
||||||
|
import CodeMirror6, { flash } from './CodeMirror6';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
import styles from './MiniRepl.module.css';
|
||||||
|
import './style.css';
|
||||||
|
import { logger } from '@strudel.cycles/core';
|
||||||
|
|
||||||
const getTime = () => getAudioContext().currentTime;
|
const getTime = () => getAudioContext().currentTime;
|
||||||
|
|
||||||
export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }) {
|
export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCanvas = false, canvasHeight = 200 }) {
|
||||||
const {
|
const {
|
||||||
code,
|
code,
|
||||||
setCode,
|
setCode,
|
||||||
@@ -26,11 +29,27 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
|
|||||||
scheduler,
|
scheduler,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
stop,
|
stop,
|
||||||
|
canvasId,
|
||||||
|
id: replId,
|
||||||
} = useStrudel({
|
} = useStrudel({
|
||||||
initialCode: tune,
|
initialCode: tune,
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: webaudioOutput,
|
||||||
getTime,
|
getTime,
|
||||||
|
editPattern: (pat, id) => {
|
||||||
|
return pat.withContext((ctx) => ({ ...ctx, id }));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
usePatternFrame({
|
||||||
|
pattern,
|
||||||
|
started: withCanvas && started,
|
||||||
|
getTime: () => scheduler.now(),
|
||||||
|
onDraw: (time, haps) => {
|
||||||
|
const ctx = document.querySelector('#' + canvasId).getContext('2d');
|
||||||
|
pianoroll({ ctx, time, haps, autorange: 1, fold: 1, playhead: 1 });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/* useEffect(() => {
|
/* useEffect(() => {
|
||||||
init && activateCode();
|
init && activateCode();
|
||||||
}, [init, activateCode]); */
|
}, [init, activateCode]); */
|
||||||
@@ -72,6 +91,20 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
|
|||||||
}
|
}
|
||||||
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
|
}, [enableKeyboard, pattern, code, evaluate, stop, view]);
|
||||||
|
|
||||||
|
const [log, setLog] = useState([]);
|
||||||
|
useLogger(
|
||||||
|
useCallback((e) => {
|
||||||
|
const { data } = e.detail;
|
||||||
|
const logId = data?.hap?.context?.id;
|
||||||
|
// const logId = data?.pattern?.meta?.id;
|
||||||
|
if (logId === replId) {
|
||||||
|
setLog((l) => {
|
||||||
|
return l.concat([e.detail]).slice(-10);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container} ref={ref}>
|
<div className={styles.container} ref={ref}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
@@ -88,6 +121,40 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
|
|||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
|
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
|
||||||
</div>
|
</div>
|
||||||
|
{withCanvas && (
|
||||||
|
<canvas
|
||||||
|
id={canvasId}
|
||||||
|
className="w-full pointer-events-none"
|
||||||
|
height={canvasHeight}
|
||||||
|
ref={(el) => {
|
||||||
|
if (el && el.width !== el.clientWidth) {
|
||||||
|
el.width = el.clientWidth;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></canvas>
|
||||||
|
)}
|
||||||
|
{!!log.length && (
|
||||||
|
<div className="sc-bg-gray-800 sc-rounded-md sc-p-2">
|
||||||
|
{log.map(({ message }, i) => (
|
||||||
|
<div key={i}>{message}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: dedupe
|
||||||
|
function useLogger(onTrigger) {
|
||||||
|
useEvent(logger.key, onTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: dedupe
|
||||||
|
function useEvent(name, onTrigger, useCapture = false) {
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener(name, onTrigger, useCapture);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener(name, onTrigger, useCapture);
|
||||||
|
};
|
||||||
|
}, [onTrigger]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
function useFrame(callback, autostart = false) {
|
||||||
|
const requestRef = useRef();
|
||||||
|
const previousTimeRef = useRef();
|
||||||
|
|
||||||
|
const animate = (time) => {
|
||||||
|
if (previousTimeRef.current !== undefined) {
|
||||||
|
const deltaTime = time - previousTimeRef.current;
|
||||||
|
callback(time, deltaTime);
|
||||||
|
}
|
||||||
|
previousTimeRef.current = time;
|
||||||
|
requestRef.current = requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = () => {
|
||||||
|
requestRef.current = requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
const stop = () => {
|
||||||
|
requestRef.current && cancelAnimationFrame(requestRef.current);
|
||||||
|
delete requestRef.current;
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestRef.current) {
|
||||||
|
stop();
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
}, [callback]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autostart) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
return stop;
|
||||||
|
}, []); // Make sure the effect only runs once
|
||||||
|
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useFrame;
|
||||||
@@ -7,9 +7,7 @@ function useHighlighting({ view, pattern, active, getTime }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (view) {
|
if (view) {
|
||||||
if (pattern && active) {
|
if (pattern && active) {
|
||||||
let frame = requestAnimationFrame(updateHighlights);
|
let frame = requestAnimationFrame(function updateHighlights() {
|
||||||
|
|
||||||
function updateHighlights() {
|
|
||||||
try {
|
try {
|
||||||
const audioTime = getTime();
|
const audioTime = getTime();
|
||||||
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
|
// force min framerate of 10 fps => fixes crash on tab refocus, where lastEnd could be far away
|
||||||
@@ -25,8 +23,7 @@ function useHighlighting({ view, pattern, active, getTime }) {
|
|||||||
view.dispatch({ effects: setHighlights.of([]) });
|
view.dispatch({ effects: setHighlights.of([]) });
|
||||||
}
|
}
|
||||||
frame = requestAnimationFrame(updateHighlights);
|
frame = requestAnimationFrame(updateHighlights);
|
||||||
}
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(frame);
|
cancelAnimationFrame(frame);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import 'tailwindcss/tailwind.css';
|
||||||
|
import useFrame from '../hooks/useFrame.mjs';
|
||||||
|
|
||||||
|
function usePatternFrame({ pattern, started, getTime, onDraw }) {
|
||||||
|
let visibleHaps = useRef([]);
|
||||||
|
let lastFrame = useRef(null);
|
||||||
|
const { start: startFrame, stop: stopFrame } = useFrame(
|
||||||
|
useCallback(() => {
|
||||||
|
const phase = getTime();
|
||||||
|
if (lastFrame.current === null) {
|
||||||
|
lastFrame.current = phase;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const haps = pattern.queryArc(Math.max(lastFrame.current, phase - 1 / 10), phase);
|
||||||
|
const cycles = 4;
|
||||||
|
lastFrame.current = phase;
|
||||||
|
visibleHaps.current = (visibleHaps.current || [])
|
||||||
|
.filter((h) => h.whole.end > phase - cycles) // in frame
|
||||||
|
.concat(haps.filter((h) => h.hasOnset()));
|
||||||
|
onDraw(phase, visibleHaps.current);
|
||||||
|
}, [pattern]),
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (started) {
|
||||||
|
startFrame();
|
||||||
|
} else {
|
||||||
|
visibleHaps.current = [];
|
||||||
|
stopFrame();
|
||||||
|
}
|
||||||
|
}, [started]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default usePatternFrame;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
|
import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { repl } from '@strudel.cycles/core';
|
import { repl } from '@strudel.cycles/core';
|
||||||
import { transpiler } from '@strudel.cycles/transpiler';
|
import { transpiler } from '@strudel.cycles/transpiler';
|
||||||
|
import usePostMessage from './usePostMessage.mjs';
|
||||||
|
|
||||||
function useStrudel({
|
function useStrudel({
|
||||||
defaultOutput,
|
defaultOutput,
|
||||||
@@ -11,14 +12,18 @@ function useStrudel({
|
|||||||
autolink = false,
|
autolink = false,
|
||||||
beforeEval,
|
beforeEval,
|
||||||
afterEval,
|
afterEval,
|
||||||
|
editPattern,
|
||||||
onEvalError,
|
onEvalError,
|
||||||
onToggle,
|
onToggle,
|
||||||
|
canvasId,
|
||||||
}) {
|
}) {
|
||||||
|
const id = useMemo(() => s4(), []);
|
||||||
|
canvasId = canvasId || `canvas-${id}`;
|
||||||
// scheduler
|
// scheduler
|
||||||
const [schedulerError, setSchedulerError] = useState();
|
const [schedulerError, setSchedulerError] = useState();
|
||||||
const [evalError, setEvalError] = useState();
|
const [evalError, setEvalError] = useState();
|
||||||
const [code, setCode] = useState(initialCode);
|
const [code, setCode] = useState(initialCode);
|
||||||
const [activeCode, setActiveCode] = useState(code);
|
const [activeCode, setActiveCode] = useState();
|
||||||
const [pattern, setPattern] = useState();
|
const [pattern, setPattern] = useState();
|
||||||
const [started, setStarted] = useState(false);
|
const [started, setStarted] = useState(false);
|
||||||
const isDirty = code !== activeCode;
|
const isDirty = code !== activeCode;
|
||||||
@@ -40,6 +45,7 @@ function useStrudel({
|
|||||||
setCode(code);
|
setCode(code);
|
||||||
beforeEval?.();
|
beforeEval?.();
|
||||||
},
|
},
|
||||||
|
editPattern: editPattern ? (pat) => editPattern(pat, id) : undefined,
|
||||||
afterEval: ({ pattern: _pattern, code }) => {
|
afterEval: ({ pattern: _pattern, code }) => {
|
||||||
setActiveCode(code);
|
setActiveCode(code);
|
||||||
setPattern(_pattern);
|
setPattern(_pattern);
|
||||||
@@ -57,7 +63,19 @@ function useStrudel({
|
|||||||
}),
|
}),
|
||||||
[defaultOutput, interval, getTime],
|
[defaultOutput, interval, getTime],
|
||||||
);
|
);
|
||||||
const activateCode = useCallback(async (autostart = true) => evaluate(code, autostart), [evaluate, code]);
|
const broadcast = usePostMessage(({ data: { from, type } }) => {
|
||||||
|
if (type === 'start' && from !== id) {
|
||||||
|
// console.log('message', from, type);
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const activateCode = useCallback(
|
||||||
|
async (autostart = true) => {
|
||||||
|
await evaluate(code, autostart);
|
||||||
|
broadcast({ type: 'start', from: id });
|
||||||
|
},
|
||||||
|
[evaluate, code],
|
||||||
|
);
|
||||||
|
|
||||||
const inited = useRef();
|
const inited = useRef();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -83,6 +101,8 @@ function useStrudel({
|
|||||||
};
|
};
|
||||||
const error = schedulerError || evalError;
|
const error = schedulerError || evalError;
|
||||||
return {
|
return {
|
||||||
|
id,
|
||||||
|
canvasId,
|
||||||
code,
|
code,
|
||||||
setCode,
|
setCode,
|
||||||
error,
|
error,
|
||||||
@@ -103,3 +123,9 @@ function useStrudel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default useStrudel;
|
export default useStrudel;
|
||||||
|
|
||||||
|
function s4() {
|
||||||
|
return Math.floor((1 + Math.random()) * 0x10000)
|
||||||
|
.toString(16)
|
||||||
|
.substring(1);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import React from 'react'
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom';
|
||||||
import App from './App'
|
import App from './App';
|
||||||
|
|
||||||
ReactDOM.render(<React.StrictMode><App /></React.StrictMode>,document.getElementById('root'))
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('root'),
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/serial",
|
"name": "@strudel.cycles/serial",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"description": "Webserial API for strudel",
|
"description": "Webserial API for strudel",
|
||||||
"main": "serial.mjs",
|
"main": "serial.mjs",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
+61
-17
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
|
|
||||||
import { Pattern, isPattern } from '@strudel.cycles/core';
|
import { Pattern, isPattern } from '@strudel.cycles/core';
|
||||||
|
|
||||||
var serialWriter;
|
var writeMessage;
|
||||||
var choosing = false;
|
var choosing = false;
|
||||||
|
|
||||||
export async function getWriter(br = 38400) {
|
export async function getWriter(br = 38400) {
|
||||||
@@ -14,17 +14,29 @@ export async function getWriter(br = 38400) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
choosing = true;
|
choosing = true;
|
||||||
if (serialWriter) {
|
if (writeMessage) {
|
||||||
return serialWriter;
|
return writeMessage;
|
||||||
}
|
}
|
||||||
if ('serial' in navigator) {
|
if ('serial' in navigator) {
|
||||||
const port = await navigator.serial.requestPort();
|
const port = await navigator.serial.requestPort();
|
||||||
await port.open({ baudRate: br });
|
await port.open({ baudRate: br });
|
||||||
const textEncoder = new TextEncoderStream();
|
const encoder = new TextEncoder();
|
||||||
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
|
const writer = port.writable.getWriter();
|
||||||
const writer = textEncoder.writable.getWriter();
|
writeMessage = function (message, chk) {
|
||||||
serialWriter = function (message) {
|
const encoded = encoder.encode(message);
|
||||||
writer.write(message);
|
if (!chk) {
|
||||||
|
writer.write(encoded);
|
||||||
|
} else {
|
||||||
|
const bytes = new Uint8Array(4);
|
||||||
|
bytes[0] = 124; // | symbol
|
||||||
|
bytes[1] = (chk >> 8) & 0xff;
|
||||||
|
bytes[2] = chk & 0xff;
|
||||||
|
bytes[3] = 59; // semicolon
|
||||||
|
const withchk = new Uint8Array(encoded.length + 4);
|
||||||
|
withchk.set(encoded);
|
||||||
|
withchk.set(bytes, encoded.length);
|
||||||
|
writer.write(withchk);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw 'Webserial is not available in this browser.';
|
throw 'Webserial is not available in this browser.';
|
||||||
@@ -33,18 +45,41 @@ export async function getWriter(br = 38400) {
|
|||||||
|
|
||||||
const latency = 0.1;
|
const latency = 0.1;
|
||||||
|
|
||||||
Pattern.prototype.serial = function (...args) {
|
// crc16 (CCITT-FALSE) https://gist.github.com/tijnkooijmans/10981093
|
||||||
return this._withHap((hap) => {
|
function crc16(data) {
|
||||||
if (!serialWriter) {
|
const length = data.length;
|
||||||
getWriter(...args);
|
if (length == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var crc = 0xffff;
|
||||||
|
for (var i = 0; i < length; ++i) {
|
||||||
|
crc ^= data.charCodeAt(i) << 8;
|
||||||
|
for (var j = 0; j < 8; ++j) {
|
||||||
|
crc = (crc & 0x8000) > 0 ? (crc << 1) ^ 0x1021 : crc << 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return crc & 0xffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern.prototype.serial = function (br = 38400, sendcrc = false, singlecharids = false) {
|
||||||
|
return this.withHap((hap) => {
|
||||||
|
if (!writeMessage) {
|
||||||
|
getWriter(br);
|
||||||
}
|
}
|
||||||
const onTrigger = (time, hap, currentTime) => {
|
const onTrigger = (time, hap, currentTime) => {
|
||||||
var message = '';
|
var message = '';
|
||||||
|
var chk = 0;
|
||||||
if (typeof hap.value === 'object') {
|
if (typeof hap.value === 'object') {
|
||||||
if ('action' in hap.value) {
|
if ('action' in hap.value) {
|
||||||
message += hap.value['action'] + '(';
|
var action = hap.value['action'];
|
||||||
|
if (singlecharids) {
|
||||||
|
action = action.charAt(0);
|
||||||
|
}
|
||||||
|
message += action + '(';
|
||||||
var first = true;
|
var first = true;
|
||||||
for (const [key, val] of Object.entries(hap.value)) {
|
for (var [key, val] of Object.entries(hap.value)) {
|
||||||
if (key === 'action') {
|
if (key === 'action') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -53,19 +88,28 @@ Pattern.prototype.serial = function (...args) {
|
|||||||
} else {
|
} else {
|
||||||
message += ',';
|
message += ',';
|
||||||
}
|
}
|
||||||
message += `${key}:${val}`;
|
if (singlecharids) {
|
||||||
|
key = key.charAt(0);
|
||||||
|
}
|
||||||
|
message += key + ':' + val;
|
||||||
}
|
}
|
||||||
message += ')';
|
message += ')';
|
||||||
|
if (sendcrc) {
|
||||||
|
chk = crc16(message);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const [key, val] of Object.entries(hap.value)) {
|
for (const [key, val] of Object.entries(hap.value)) {
|
||||||
message += `${key}:${val};`;
|
message += `${key}:${val}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
message = hap.value;
|
message = hap.value;
|
||||||
}
|
}
|
||||||
const offset = (time - currentTime + latency) * 1000;
|
const offset = (time - currentTime + latency) * 1000;
|
||||||
window.setTimeout(serialWriter, offset, message);
|
|
||||||
|
window.setTimeout(function () {
|
||||||
|
writeMessage(message, chk);
|
||||||
|
}, offset);
|
||||||
};
|
};
|
||||||
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
return hap.setContext({ ...hap.context, onTrigger, dominantTrigger: true });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { toMidi } from '@strudel.cycles/core';
|
||||||
|
|
||||||
let loadCache = {};
|
let loadCache = {};
|
||||||
async function loadFont(name) {
|
async function loadFont(name) {
|
||||||
if (loadCache[name]) {
|
if (loadCache[name]) {
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/soundfonts",
|
"name": "@strudel.cycles/soundfonts",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/soundfonts",
|
"name": "@strudel.cycles/soundfonts",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"description": "Soundsfont support for strudel",
|
"description": "Soundsfont support for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.4.0",
|
"@strudel.cycles/core": "^0.5.0",
|
||||||
"@strudel.cycles/webaudio": "^0.4.0",
|
"@strudel.cycles/webaudio": "^0.5.0",
|
||||||
"sfumato": "^0.1.2",
|
"sfumato": "^0.1.2",
|
||||||
"soundfont2": "^0.4.0"
|
"soundfont2": "^0.4.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Pattern } from '@strudel.cycles/core';
|
import { Pattern, getPlayableNoteValue, toMidi } from '@strudel.cycles/core';
|
||||||
|
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||||
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
import { loadSoundfont as _loadSoundfont, startPresetNote } from 'sfumato';
|
||||||
|
|
||||||
Pattern.prototype.soundfont = function (sf, n = 0) {
|
Pattern.prototype.soundfont = function (sf, n = 0) {
|
||||||
|
|||||||
@@ -31,4 +31,4 @@ yields:
|
|||||||
|
|
||||||
## Tonal API
|
## Tonal API
|
||||||
|
|
||||||
See "Tonal API" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
|
See "Tonal API" in the [Strudel Tutorial](https://strudel.tidalcycles.org/learn/tonal)
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
import './tonal.mjs';
|
import './tonal.mjs';
|
||||||
import './voicings.mjs';
|
import './voicings.mjs';
|
||||||
|
|
||||||
|
export * from './tonal.mjs';
|
||||||
|
export * from './voicings.mjs';
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/tonal",
|
"name": "@strudel.cycles/tonal",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/tonal",
|
"name": "@strudel.cycles/tonal",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"description": "Tonal functions for strudel",
|
"description": "Tonal functions for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.4.0",
|
"@strudel.cycles/core": "^0.5.0",
|
||||||
"@tonaljs/tonal": "^4.6.5",
|
"@tonaljs/tonal": "^4.7.2",
|
||||||
"chord-voicings": "^0.0.1",
|
"chord-voicings": "^0.0.1",
|
||||||
"webmidi": "^3.0.21"
|
"webmidi": "^3.0.21"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,27 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
// import { strict as assert } from 'assert';
|
// import { strict as assert } from 'assert';
|
||||||
|
|
||||||
import '../tonal.mjs'; // need to import this to add prototypes
|
import '../tonal.mjs'; // need to import this to add prototypes
|
||||||
import { pure } from '@strudel.cycles/core';
|
import { pure, controls, seq } from '@strudel.cycles/core';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
const { n } = controls;
|
||||||
|
|
||||||
describe('tonal', () => {
|
describe('tonal', () => {
|
||||||
it('Should run tonal functions ', () => {
|
it('Should run tonal functions ', () => {
|
||||||
expect(pure('c3').scale('C major').scaleTranspose(1)._firstCycleValues).toEqual(['D3']);
|
expect(pure('c3').scale('C major').scaleTranspose(1).firstCycleValues).toEqual(['D3']);
|
||||||
|
});
|
||||||
|
it('scale with plain values', () => {
|
||||||
|
expect(
|
||||||
|
seq(0, 1, 2)
|
||||||
|
.scale('C major')
|
||||||
|
.note()
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
|
});
|
||||||
|
it('scale with n values', () => {
|
||||||
|
expect(
|
||||||
|
n(0, 1, 2)
|
||||||
|
.scale('C major')
|
||||||
|
.firstCycleValues.map((h) => h.note),
|
||||||
|
).toEqual(['C3', 'D3', 'E3']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+14
-19
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
import { Note, Interval, Scale } from '@tonaljs/tonal';
|
||||||
import { Pattern, mod } from '@strudel.cycles/core';
|
import { register, _mod } from '@strudel.cycles/core';
|
||||||
|
|
||||||
// transpose note inside scale by offset steps
|
// transpose note inside scale by offset steps
|
||||||
// function scaleOffset(scale: string, offset: number, note: string) {
|
// function scaleOffset(scale: string, offset: number, note: string) {
|
||||||
@@ -29,7 +29,7 @@ function scaleOffset(scale, offset, note) {
|
|||||||
// TODO: find way to do this smarter
|
// TODO: find way to do this smarter
|
||||||
while (Math.abs(i - noteIndex) < Math.abs(offset)) {
|
while (Math.abs(i - noteIndex) < Math.abs(offset)) {
|
||||||
i += direction;
|
i += direction;
|
||||||
const index = mod(i, notes.length);
|
const index = _mod(i, notes.length);
|
||||||
if (direction < 0 && n[0] === 'C') {
|
if (direction < 0 && n[0] === 'C') {
|
||||||
o += direction;
|
o += direction;
|
||||||
}
|
}
|
||||||
@@ -74,8 +74,8 @@ function scaleOffset(scale, offset, note) {
|
|||||||
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()
|
* "c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Pattern.prototype._transpose = function (intervalOrSemitones) {
|
export const transpose = register('transpose', function (intervalOrSemitones, pat) {
|
||||||
return this._withHap((hap) => {
|
return pat.withHap((hap) => {
|
||||||
const interval = !isNaN(Number(intervalOrSemitones))
|
const interval = !isNaN(Number(intervalOrSemitones))
|
||||||
? Interval.fromSemitones(intervalOrSemitones /* as number */)
|
? Interval.fromSemitones(intervalOrSemitones /* as number */)
|
||||||
: String(intervalOrSemitones);
|
: String(intervalOrSemitones);
|
||||||
@@ -87,11 +87,9 @@ Pattern.prototype._transpose = function (intervalOrSemitones) {
|
|||||||
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
|
// tone.js doesn't understand multiple sharps flats e.g. F##3 has to be turned into G3
|
||||||
return hap.withValue(() => Note.simplify(Note.transpose(hap.value, interval)));
|
return hap.withValue(() => Note.simplify(Note.transpose(hap.value, interval)));
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
// example: transpose(3).late(0.2) will be equivalent to compose(transpose(3), late(0.2))
|
// example: transpose(3).late(0.2) will be equivalent to compose(transpose(3), late(0.2))
|
||||||
// TODO: add Pattern.define(name, function, options) that handles all the meta programming stuff
|
|
||||||
// TODO: find out how to patternify this function when it's standalone
|
|
||||||
// e.g. `stack(c3).superimpose(transpose(slowcat(7, 5)))` or
|
// e.g. `stack(c3).superimpose(transpose(slowcat(7, 5)))` or
|
||||||
// or even `stack(c3).superimpose(transpose.slowcat(7, 5))` or
|
// or even `stack(c3).superimpose(transpose.slowcat(7, 5))` or
|
||||||
|
|
||||||
@@ -110,8 +108,8 @@ Pattern.prototype._transpose = function (intervalOrSemitones) {
|
|||||||
* .note()
|
* .note()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
|
export const scaleTranspose = register('scaleTranspose', function (offset /* : number | string */, pat) {
|
||||||
return this._withHap((hap) => {
|
return pat.withHap((hap) => {
|
||||||
if (!hap.context.scale) {
|
if (!hap.context.scale) {
|
||||||
throw new Error('can only use scaleTranspose after .scale');
|
throw new Error('can only use scaleTranspose after .scale');
|
||||||
}
|
}
|
||||||
@@ -120,7 +118,7 @@ Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
|
|||||||
}
|
}
|
||||||
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
|
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
* Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like {@link Pattern#scaleTranspose}.
|
||||||
@@ -141,19 +139,16 @@ Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
|
|||||||
* .note()
|
* .note()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Pattern.prototype._scale = function (scale /* : string */) {
|
export const scale = register('scale', function (scale /* : string */, pat) {
|
||||||
return this._withHap((hap) => {
|
return pat.withHap((hap) => {
|
||||||
let note = hap.value;
|
const isObject = typeof hap.value === 'object';
|
||||||
|
let note = isObject ? hap.value.n : hap.value;
|
||||||
const asNumber = Number(note);
|
const asNumber = Number(note);
|
||||||
if (!isNaN(asNumber)) {
|
if (!isNaN(asNumber)) {
|
||||||
let [tonic, scaleName] = Scale.tokenize(scale);
|
let [tonic, scaleName] = Scale.tokenize(scale);
|
||||||
const { pc, oct = 3 } = Note.get(tonic);
|
const { pc, oct = 3 } = Note.get(tonic);
|
||||||
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
|
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
|
||||||
}
|
}
|
||||||
return hap.withValue(() => note).setContext({ ...hap.context, scale });
|
return hap.withValue(() => (isObject ? { ...hap.value, note } : note)).setContext({ ...hap.context, scale });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
Pattern.prototype.define('transpose', (a, pat) => pat.transpose(a), { composable: true, patternified: true });
|
|
||||||
Pattern.prototype.define('scale', (a, pat) => pat.scale(a), { composable: true, patternified: true });
|
|
||||||
Pattern.prototype.define('scaleTranspose', (a, pat) => pat.scaleTranspose(a), { composable: true, patternified: true });
|
|
||||||
|
|||||||
+62
-38
@@ -4,31 +4,49 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern as _Pattern, stack, Hap, reify } from '@strudel.cycles/core';
|
import { stack, register } from '@strudel.cycles/core';
|
||||||
import _voicings from 'chord-voicings';
|
import _voicings from 'chord-voicings';
|
||||||
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
|
const { dictionaryVoicing, minTopNoteDiff, lefthand } = _voicings.default || _voicings; // parcel module resolution fuckup
|
||||||
|
|
||||||
const getVoicing = (chord, lastVoicing, range = ['F3', 'A4']) =>
|
export const voicingRegistry = {
|
||||||
dictionaryVoicing({
|
lefthand: { dictionary: lefthand, range: ['F3', 'A4'] },
|
||||||
|
};
|
||||||
|
export const setVoicingRange = (name, range) => addVoicings(name, voicingRegistry[name].dictionary, range);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new custom voicing dictionary.
|
||||||
|
*
|
||||||
|
* @name addVoicings
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {string} name identifier for the voicing dictionary
|
||||||
|
* @param {Object} dictionary maps chord symbol to possible voicings
|
||||||
|
* @param {Array} range min, max note
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* addVoicings('cookie', {
|
||||||
|
* 7: ['3M 7m 9M 12P 15P', '7m 10M 13M 16M 19P'],
|
||||||
|
* '^7': ['3M 6M 9M 12P 14M', '7M 10M 13M 16M 19P'],
|
||||||
|
* m7: ['8P 11P 14m 17m 19P', '5P 8P 11P 14m 17m'],
|
||||||
|
* m7b5: ['3m 5d 8P 11P 14m', '5d 8P 11P 14m 17m'],
|
||||||
|
* o7: ['3m 6M 9M 11A 15P'],
|
||||||
|
* '7alt': ['3M 7m 10m 13m 15P'],
|
||||||
|
* '7#11': ['7m 10m 13m 15P 17m'],
|
||||||
|
* }, ['C3', 'C6'])
|
||||||
|
* "<C^7 A7 Dm7 G7>".voicings('cookie').note()
|
||||||
|
*/
|
||||||
|
export const addVoicings = (name, dictionary, range = ['F3', 'A4']) => {
|
||||||
|
Object.assign(voicingRegistry, { [name]: { dictionary, range } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVoicing = (chord, dictionaryName, lastVoicing) => {
|
||||||
|
const { dictionary, range } = voicingRegistry[dictionaryName];
|
||||||
|
return dictionaryVoicing({
|
||||||
chord,
|
chord,
|
||||||
dictionary: lefthand,
|
dictionary,
|
||||||
range,
|
range,
|
||||||
picker: minTopNoteDiff,
|
picker: minTopNoteDiff,
|
||||||
lastVoicing,
|
lastVoicing,
|
||||||
});
|
});
|
||||||
|
|
||||||
const Pattern = _Pattern;
|
|
||||||
|
|
||||||
Pattern.prototype.fmapNested = function (func) {
|
|
||||||
return new Pattern((span) =>
|
|
||||||
this.query(span)
|
|
||||||
.map((event) =>
|
|
||||||
reify(func(event))
|
|
||||||
.query(span)
|
|
||||||
.map((hap) => new Hap(event.whole, event.part, hap.value, hap.context)),
|
|
||||||
)
|
|
||||||
.flat(),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,32 +55,38 @@ Pattern.prototype.fmapNested = function (func) {
|
|||||||
*
|
*
|
||||||
* @name voicings
|
* @name voicings
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @param {range} range note range for possible voicings (optional, defaults to `['F3', 'A4']`)
|
* @param {string} dictionary which voicing dictionary to use.
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* stack("<C^7 A7 Dm7 G7>".voicings(), "<C3 A2 D3 G2>").note()
|
* stack("<C^7 A7 Dm7 G7>".voicings('lefthand'), "<C3 A2 D3 G2>").note()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Pattern.prototype.voicings = function (range) {
|
let lastVoicing; // this now has to be global until another solution is found :-/
|
||||||
let lastVoicing;
|
// it used to be local to the voicings function at evaluation time
|
||||||
if (!range?.length) {
|
// but since register will patternify by default, means that
|
||||||
// allows to pass empty array, if too lazy to specify range
|
// the function is called over and over again, resetting the lastVoicing variables
|
||||||
range = ['F3', 'A4'];
|
export const voicings = register('voicings', function (dictionary, pat) {
|
||||||
}
|
return pat
|
||||||
return this.fmapNested((event) => {
|
.fmap((value) => {
|
||||||
lastVoicing = getVoicing(event.value, lastVoicing, range);
|
lastVoicing = getVoicing(value, dictionary, lastVoicing);
|
||||||
return stack(...lastVoicing)._withContext(() => ({
|
return stack(...lastVoicing);
|
||||||
locations: event.context.locations || [],
|
})
|
||||||
}));
|
.outerJoin();
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
Pattern.prototype._rootNotes = function (octave = 2) {
|
/**
|
||||||
return this.fmap((value) => {
|
* Maps the chords of the incoming pattern to root notes in the given octave.
|
||||||
const [_, root] = value.match(/^([a-gA-G][b#]?).*$/);
|
*
|
||||||
|
* @name rootNotes
|
||||||
|
* @memberof Pattern
|
||||||
|
* @param {octave} octave octave to use
|
||||||
|
* @returns Pattern
|
||||||
|
* @example
|
||||||
|
* "<C^7 A7 Dm7 G7>".rootNotes(2).note()
|
||||||
|
*/
|
||||||
|
export const rootNotes = register('rootNotes', function (octave, pat) {
|
||||||
|
return pat.fmap((value) => {
|
||||||
|
const root = value.match(/^([a-gA-G][b#]?).*$/)[1];
|
||||||
return root + octave;
|
return root + octave;
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
Pattern.prototype.define('voicings', (range, pat) => pat.voicings(range), { composable: true });
|
|
||||||
Pattern.prototype.define('rootNotes', (oct, pat) => pat.rootNotes(oct), { composable: true, patternified: true });
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# @strudel.cycles/tone
|
# @strudel.cycles/tone
|
||||||
|
|
||||||
|
Note: This package still works but is no longer maintained in favor of `@strudel.cycles/webaudio`.
|
||||||
|
|
||||||
This package adds Tone.js functions to strudel Patterns.
|
This package adds Tone.js functions to strudel Patterns.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
@@ -31,11 +33,3 @@ document.getElementById('play').addEventListener('click', async () => {
|
|||||||
|
|
||||||
[open in codesandbox](https://codesandbox.io/s/strudel-tone-example-5ph2te?file=/src/index.js:0-708)
|
[open in codesandbox](https://codesandbox.io/s/strudel-tone-example-5ph2te?file=/src/index.js:0-708)
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
See "Tone API" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
|
|
||||||
|
|
||||||
## Dev Notes
|
|
||||||
|
|
||||||
- `@tonejs/piano` has peer dependency on `webmidi@^2.5.1`! A newer version of webmidi will break.
|
|
||||||
- If you use Tone in another package, make sure to `import { Tone } from '@strudel.cycles/tone`, to ensure you get the same AudioContext.
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/tone",
|
"name": "@strudel.cycles/tone",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/tone",
|
"name": "@strudel.cycles/tone",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"description": "Tone.js API for strudel",
|
"description": "Tone.js API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.4.0",
|
"@strudel.cycles/core": "^0.5.0",
|
||||||
"tone": "^14.7.77"
|
"tone": "^14.7.77"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ describe('tone', () => {
|
|||||||
// const s = synth().chain(out()); // TODO: mock audio context?
|
// const s = synth().chain(out()); // TODO: mock audio context?
|
||||||
// assert.deepStrictEqual(s, new Tone.Synth().chain(out()));
|
// assert.deepStrictEqual(s, new Tone.Synth().chain(out()));
|
||||||
const s = {};
|
const s = {};
|
||||||
expect(pure('c3').tone(s)._firstCycleValues).toEqual(['c3']);
|
expect(pure('c3').tone(s).firstCycleValues).toEqual(['c3']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,18 +4,15 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern } from '@strudel.cycles/core';
|
import { register } from '@strudel.cycles/core';
|
||||||
import * as _Tone from 'tone';
|
import * as _Tone from 'tone';
|
||||||
|
|
||||||
// import Tone from here, to make sure to get the same AudioContext
|
// import Tone from here, to make sure to get the same AudioContext
|
||||||
export const Tone = _Tone;
|
export const Tone = _Tone;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
AutoFilter,
|
|
||||||
Destination,
|
|
||||||
Filter,
|
Filter,
|
||||||
Gain,
|
Gain,
|
||||||
isNote,
|
|
||||||
Synth,
|
Synth,
|
||||||
PolySynth,
|
PolySynth,
|
||||||
MembraneSynth,
|
MembraneSynth,
|
||||||
@@ -49,8 +46,8 @@ export const getDefaultSynth = () => {
|
|||||||
// https://www.charlie-roberts.com/gibberish/playground/
|
// https://www.charlie-roberts.com/gibberish/playground/
|
||||||
|
|
||||||
// with this function, you can play the pattern with any tone synth
|
// with this function, you can play the pattern with any tone synth
|
||||||
Pattern.prototype.tone = function (instrument) {
|
export const tone = register('tone', function (instrument, pat) {
|
||||||
return this.onTrigger((time, hap) => {
|
return pat.onTrigger((time, hap) => {
|
||||||
let note;
|
let note;
|
||||||
let velocity = hap.context?.velocity ?? 0.75;
|
let velocity = hap.context?.velocity ?? 0.75;
|
||||||
if (instrument instanceof PluckSynth) {
|
if (instrument instanceof PluckSynth) {
|
||||||
@@ -74,9 +71,7 @@ Pattern.prototype.tone = function (instrument) {
|
|||||||
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
|
instrument.triggerAttackRelease(note, hap.duration.valueOf(), time, velocity);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
Pattern.prototype.define('tone', (type, pat) => pat.tone(type), { composable: true, patternified: false });
|
|
||||||
|
|
||||||
// synth helpers
|
// synth helpers
|
||||||
export const amsynth = (options) => new AMSynth(options);
|
export const amsynth = (options) => new AMSynth(options);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/transpiler",
|
"name": "@strudel.cycles/transpiler",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
"description": "Transpiler for strudel user code. Converts syntactically correct but semantically meaningless JS into evaluatable strudel code.",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.4.0",
|
"@strudel.cycles/core": "^0.5.0",
|
||||||
"acorn": "^8.8.1",
|
"acorn": "^8.8.1",
|
||||||
"escodegen": "^2.0.0",
|
"escodegen": "^2.0.0",
|
||||||
"estree-walker": "^3.0.1"
|
"estree-walker": "^3.0.1"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import escodegen from 'escodegen';
|
import escodegen from 'escodegen';
|
||||||
import { parse } from 'acorn';
|
import { parse } from 'acorn';
|
||||||
import { walk } from 'estree-walker';
|
import { walk } from 'estree-walker';
|
||||||
import { isNote } from '@strudel.cycles/core';
|
import { isNoteWithOctave } from '@strudel.cycles/core';
|
||||||
|
|
||||||
export function transpiler(input, options = {}) {
|
export function transpiler(input, options = {}) {
|
||||||
const { wrapAsync = false, addReturn = true, simpleLocs = false } = options;
|
const { wrapAsync = false, addReturn = true, simpleLocs = false } = options;
|
||||||
@@ -25,11 +25,11 @@ export function transpiler(input, options = {}) {
|
|||||||
this.skip();
|
this.skip();
|
||||||
return this.replace(miniWithLocation(value, node, simpleLocs));
|
return this.replace(miniWithLocation(value, node, simpleLocs));
|
||||||
}
|
}
|
||||||
if (node.type === 'Identifier' && isNote(node.name)) {
|
// TODO: remove pseudo note variables?
|
||||||
|
if (node.type === 'Identifier' && isNoteWithOctave(node.name)) {
|
||||||
this.skip();
|
this.skip();
|
||||||
return this.replace({ type: 'Literal', value: node.name });
|
return this.replace({ type: 'Literal', value: node.name });
|
||||||
}
|
}
|
||||||
// TODO:
|
|
||||||
},
|
},
|
||||||
leave(node, parent, prop, index) {},
|
leave(node, parent, prop, index) {},
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/webaudio",
|
"name": "@strudel.cycles/webaudio",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/webaudio",
|
"name": "@strudel.cycles/webaudio",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"description": "Web Audio helpers for Strudel",
|
"description": "Web Audio helpers for Strudel",
|
||||||
"main": "dist/index.cjs.js",
|
"main": "index.mjs",
|
||||||
"module": "dist/index.es.js",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"directories": {
|
"directories": {
|
||||||
"example": "examples"
|
"example": "examples"
|
||||||
@@ -31,6 +30,6 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "^0.4.0"
|
"@strudel.cycles/core": "^0.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
import { toMidi, fromMidi } from '@strudel.cycles/core/util.mjs';
|
||||||
|
// import './feedbackdelay.mjs';
|
||||||
|
// import './reverb.mjs';
|
||||||
|
|
||||||
|
// TODO: reuse as much logic as possible between webaudio.mjs and render.mjs
|
||||||
|
|
||||||
|
const logger = console.log;
|
||||||
|
|
||||||
|
export const splitSN = (s, n) => {
|
||||||
|
if (!s.includes(':')) {
|
||||||
|
return [s, n];
|
||||||
|
}
|
||||||
|
let [s2, n2] = s.split(':');
|
||||||
|
if (isNaN(Number(n2))) {
|
||||||
|
return [s, n];
|
||||||
|
}
|
||||||
|
return [s2, n2];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOscillator = (ac, { s, freq, t, duration, release }) => {
|
||||||
|
// make oscillator
|
||||||
|
const o = ac.createOscillator();
|
||||||
|
o.type = s || 'triangle';
|
||||||
|
o.frequency.value = Number(freq);
|
||||||
|
o.start(t);
|
||||||
|
o.stop(t + duration + release);
|
||||||
|
return o;
|
||||||
|
};
|
||||||
|
const getADSR = (ac, attack, decay, sustain, release, velocity, begin, end) => {
|
||||||
|
const gainNode = ac.createGain();
|
||||||
|
gainNode.gain.setValueAtTime(0, begin);
|
||||||
|
gainNode.gain.linearRampToValueAtTime(velocity, begin + attack); // attack
|
||||||
|
gainNode.gain.linearRampToValueAtTime(sustain * velocity, begin + attack + decay); // sustain start
|
||||||
|
gainNode.gain.setValueAtTime(sustain * velocity, end); // sustain end
|
||||||
|
gainNode.gain.linearRampToValueAtTime(0, end + release); // release
|
||||||
|
// for some reason, using exponential ramping creates little cracklings
|
||||||
|
/* let t = begin;
|
||||||
|
gainNode.gain.setValueAtTime(0, t);
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(velocity, (t += attack));
|
||||||
|
const sustainGain = Math.max(sustain * velocity, 0.001);
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(sustainGain, (t += decay));
|
||||||
|
if (end - begin < attack + decay) {
|
||||||
|
gainNode.gain.cancelAndHoldAtTime(end);
|
||||||
|
} else {
|
||||||
|
gainNode.gain.setValueAtTime(sustainGain, end);
|
||||||
|
}
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.001, end + release); // release */
|
||||||
|
return gainNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function gainNode(ac, value) {
|
||||||
|
const node = ac.createGain();
|
||||||
|
node.gain.value = value;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* let delays = {};
|
||||||
|
function getDelay(ac, orbit, delaytime, delayfeedback, t) {
|
||||||
|
if (!delays[orbit]) {
|
||||||
|
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
||||||
|
dly.start?.(t); // for some reason, this throws when audion extension is installed..
|
||||||
|
dly.connect(ac.destination);
|
||||||
|
delays[orbit] = dly;
|
||||||
|
}
|
||||||
|
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
|
||||||
|
delays[orbit].feedback.value !== delayfeedback && delays[orbit].feedback.setValueAtTime(delayfeedback, t);
|
||||||
|
return delays[orbit];
|
||||||
|
}
|
||||||
|
|
||||||
|
let reverbs = {};
|
||||||
|
function getReverb(ac, orbit, duration = 2) {
|
||||||
|
if (!reverbs[orbit]) {
|
||||||
|
const reverb = ac.createReverb(duration);
|
||||||
|
reverb.connect(ac.destination);
|
||||||
|
reverbs[orbit] = reverb;
|
||||||
|
}
|
||||||
|
if (reverbs[orbit].duration !== duration) {
|
||||||
|
reverbs[orbit] = reverbs[orbit].setDuration(duration);
|
||||||
|
reverbs[orbit].duration = duration;
|
||||||
|
}
|
||||||
|
return reverbs[orbit];
|
||||||
|
} */
|
||||||
|
|
||||||
|
function effectSend(input, effect, wet) {
|
||||||
|
const send = gainNode(wet);
|
||||||
|
input.connect(send);
|
||||||
|
send.connect(effect);
|
||||||
|
return send;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFilter = (ac, type, frequency, Q) => {
|
||||||
|
const filter = ac.createBiquadFilter();
|
||||||
|
filter.type = type;
|
||||||
|
filter.frequency.value = frequency;
|
||||||
|
filter.Q.value = Q;
|
||||||
|
return filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
// export const webaudioOutput = async (t, hap, ct, cps) => {
|
||||||
|
export const renderHap = async (ac, hap, deadline) => {
|
||||||
|
const hapDuration = hap.duration;
|
||||||
|
/* if (isNote(hap.value)) {
|
||||||
|
// supports primitive hap values that look like notes
|
||||||
|
hap.value = { note: hap.value };
|
||||||
|
} */
|
||||||
|
if (typeof hap.value !== 'object') {
|
||||||
|
logger(
|
||||||
|
`hap.value "${hap.value}" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
|
||||||
|
'error',
|
||||||
|
);
|
||||||
|
/* throw new Error(
|
||||||
|
`hap.value "${hap.value}"" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
|
||||||
|
); */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// calculate correct time (tone.js workaround)
|
||||||
|
let t = ac.currentTime + deadline;
|
||||||
|
// destructure value
|
||||||
|
let {
|
||||||
|
freq,
|
||||||
|
s,
|
||||||
|
bank,
|
||||||
|
sf,
|
||||||
|
clip = 0, // if 1, samples will be cut off when the hap ends
|
||||||
|
n = 0,
|
||||||
|
note,
|
||||||
|
gain = 0.8,
|
||||||
|
// low pass
|
||||||
|
lpf,
|
||||||
|
cutoff = lpf,
|
||||||
|
lpq = 1,
|
||||||
|
resonance = lpq,
|
||||||
|
// high pass
|
||||||
|
hpf,
|
||||||
|
hcutoff = hpf,
|
||||||
|
hpq = 1,
|
||||||
|
hresonance = hpq,
|
||||||
|
// band pass
|
||||||
|
bpf,
|
||||||
|
bandf = bpf,
|
||||||
|
bpq = 1,
|
||||||
|
bandq = bpq,
|
||||||
|
//
|
||||||
|
coarse,
|
||||||
|
crush,
|
||||||
|
shape,
|
||||||
|
pan,
|
||||||
|
speed = 1, // sample playback speed
|
||||||
|
begin = 0,
|
||||||
|
end = 1,
|
||||||
|
vowel,
|
||||||
|
delay = 0,
|
||||||
|
delayfeedback = 0.5,
|
||||||
|
delaytime = 0.25,
|
||||||
|
unit,
|
||||||
|
nudge = 0, // TODO: is this in seconds?
|
||||||
|
cut,
|
||||||
|
loop,
|
||||||
|
orbit = 1,
|
||||||
|
room,
|
||||||
|
size = 2,
|
||||||
|
roomsize = size,
|
||||||
|
} = hap.value;
|
||||||
|
const { velocity = 1 } = hap.context;
|
||||||
|
gain *= velocity; // legacy fix for velocity
|
||||||
|
// the chain will hold all audio nodes that connect to each other
|
||||||
|
const chain = [];
|
||||||
|
if (bank && s) {
|
||||||
|
s = `${bank}_${s}`;
|
||||||
|
}
|
||||||
|
if (typeof s === 'string') {
|
||||||
|
[s, n] = splitSN(s, n);
|
||||||
|
}
|
||||||
|
if (typeof note === 'string') {
|
||||||
|
[note, n] = splitSN(note, n);
|
||||||
|
}
|
||||||
|
if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) {
|
||||||
|
// destructure adsr here, because the default should be different for synths and samples
|
||||||
|
const { attack = 0.001, decay = 0.05, sustain = 0.6, release = 0.01 } = hap.value;
|
||||||
|
// with synths, n and note are the same thing
|
||||||
|
n = note || n || 36;
|
||||||
|
if (typeof n === 'string') {
|
||||||
|
n = toMidi(n); // e.g. c3 => 48
|
||||||
|
}
|
||||||
|
// get frequency
|
||||||
|
if (!freq && typeof n === 'number') {
|
||||||
|
freq = fromMidi(n); // + 48);
|
||||||
|
}
|
||||||
|
// make oscillator
|
||||||
|
const o = getOscillator(ac, { t, s, freq, duration: hapDuration, release });
|
||||||
|
chain.push(o);
|
||||||
|
// level down oscillators as they are really loud compared to samples i've tested
|
||||||
|
chain.push(gainNode(ac, 0.3));
|
||||||
|
// TODO: make adsr work with samples without pops
|
||||||
|
// envelope
|
||||||
|
const adsr = getADSR(ac, attack, decay, sustain, release, 1, t, t + hapDuration);
|
||||||
|
chain.push(adsr);
|
||||||
|
} else {
|
||||||
|
// TODO: refactor sampler to node
|
||||||
|
/*
|
||||||
|
// destructure adsr here, because the default should be different for synths and samples
|
||||||
|
const { attack = 0.001, decay = 0.001, sustain = 1, release = 0.001 } = hap.value;
|
||||||
|
// load sample
|
||||||
|
if (speed === 0) {
|
||||||
|
// no playback
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!s) {
|
||||||
|
console.warn('no sample specified');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const soundfont = getSoundfontKey(s);
|
||||||
|
let bufferSource;
|
||||||
|
|
||||||
|
if (soundfont) {
|
||||||
|
// is soundfont
|
||||||
|
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac, freq);
|
||||||
|
} else {
|
||||||
|
// is sample from loaded samples(..)
|
||||||
|
bufferSource = await getSampleBufferSource(s, n, note, speed, freq);
|
||||||
|
}
|
||||||
|
// asny stuff above took too long?
|
||||||
|
if (ac.currentTime > t) {
|
||||||
|
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
|
||||||
|
// console.warn('sample still loading:', s, n);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!bufferSource) {
|
||||||
|
console.warn('no buffer source');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value;
|
||||||
|
if (unit === 'c') {
|
||||||
|
// are there other units?
|
||||||
|
bufferSource.playbackRate.value = bufferSource.playbackRate.value * bufferSource.buffer.duration;
|
||||||
|
}
|
||||||
|
let duration = soundfont || clip ? hapDuration : bufferSource.buffer.duration / bufferSource.playbackRate.value;
|
||||||
|
// "The computation of the offset into the sound is performed using the sound buffer's natural sample rate,
|
||||||
|
// rather than the current playback rate, so even if the sound is playing at twice its normal speed,
|
||||||
|
// the midway point through a 10-second audio buffer is still 5."
|
||||||
|
const offset = begin * duration * bufferSource.playbackRate.value;
|
||||||
|
duration = (end - begin) * duration;
|
||||||
|
if (loop) {
|
||||||
|
bufferSource.loop = true;
|
||||||
|
bufferSource.loopStart = offset;
|
||||||
|
bufferSource.loopEnd = offset + duration;
|
||||||
|
duration = loop * duration;
|
||||||
|
}
|
||||||
|
t += nudge;
|
||||||
|
|
||||||
|
bufferSource.start(t, offset);
|
||||||
|
if (cut !== undefined) {
|
||||||
|
cutGroups[cut]?.stop(t); // fade out?
|
||||||
|
cutGroups[cut] = bufferSource;
|
||||||
|
}
|
||||||
|
chain.push(bufferSource);
|
||||||
|
bufferSource.stop(t + duration + release);
|
||||||
|
const adsr = getADSR(ac, attack, decay, sustain, release, 1, t, t + duration);
|
||||||
|
chain.push(adsr); */
|
||||||
|
}
|
||||||
|
|
||||||
|
// gain stage
|
||||||
|
chain.push(gainNode(ac, gain));
|
||||||
|
|
||||||
|
// filters
|
||||||
|
cutoff !== undefined && chain.push(getFilter(ac, 'lowpass', cutoff, resonance));
|
||||||
|
hcutoff !== undefined && chain.push(getFilter(ac, 'highpass', hcutoff, hresonance));
|
||||||
|
bandf !== undefined && chain.push(getFilter(ac, 'bandpass', bandf, bandq));
|
||||||
|
vowel !== undefined && chain.push(ac.createVowelFilter(vowel));
|
||||||
|
|
||||||
|
// effects
|
||||||
|
/* coarse !== undefined && chain.push(getWorklet(ac, 'coarse-processor', { coarse }));
|
||||||
|
crush !== undefined && chain.push(getWorklet(ac, 'crush-processor', { crush }));
|
||||||
|
shape !== undefined && chain.push(getWorklet(ac, 'shape-processor', { shape })); */
|
||||||
|
|
||||||
|
// panning
|
||||||
|
if (pan !== undefined) {
|
||||||
|
const panner = ac.createStereoPanner();
|
||||||
|
panner.pan.value = 2 * pan - 1;
|
||||||
|
chain.push(panner);
|
||||||
|
}
|
||||||
|
|
||||||
|
// last gain
|
||||||
|
const post = gainNode(ac, 1);
|
||||||
|
chain.push(post);
|
||||||
|
// post.connect(getDestination());
|
||||||
|
post.connect(ac.destination);
|
||||||
|
|
||||||
|
// TODO: refactor delay / reverb for node: extending nodes does not work..
|
||||||
|
// delay
|
||||||
|
/* let delaySend;
|
||||||
|
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||||
|
const delyNode = getDelay(ac, orbit, delaytime, delayfeedback, t);
|
||||||
|
delaySend = effectSend(post, delyNode, delay);
|
||||||
|
}
|
||||||
|
// reverb
|
||||||
|
let reverbSend;
|
||||||
|
if (room > 0 && roomsize > 0) {
|
||||||
|
const reverbNode = getReverb(ac, orbit, roomsize);
|
||||||
|
reverbSend = effectSend(post, reverbNode, room);
|
||||||
|
} */
|
||||||
|
|
||||||
|
// connect chain elements together
|
||||||
|
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||||
|
|
||||||
|
// disconnect all nodes when source node has ended:
|
||||||
|
chain[0].onended = () =>
|
||||||
|
chain
|
||||||
|
.concat([
|
||||||
|
/*delaySend , reverbSend */
|
||||||
|
])
|
||||||
|
.forEach((n) => n?.disconnect());
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { logger } from '@strudel.cycles/core';
|
import { logger, toMidi, valueToMidi } from '@strudel.cycles/core';
|
||||||
|
import { getAudioContext } from './index.mjs';
|
||||||
|
|
||||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||||
@@ -19,9 +20,12 @@ function humanFileSize(bytes, si) {
|
|||||||
return bytes.toFixed(1) + ' ' + units[u];
|
return bytes.toFixed(1) + ' ' + units[u];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSampleBufferSource = async (s, n, note, speed) => {
|
export const getSampleBufferSource = async (s, n, note, speed, freq) => {
|
||||||
let transpose = 0;
|
let transpose = 0;
|
||||||
let midi = typeof note === 'string' ? toMidi(note) : note || 36;
|
if (freq !== undefined && note !== undefined) {
|
||||||
|
logger('[sampler] hap has note and freq. ignoring note', 'warning');
|
||||||
|
}
|
||||||
|
let midi = valueToMidi({ freq, note }, 36);
|
||||||
transpose = midi - 36; // C3 is middle C
|
transpose = midi - 36; // C3 is middle C
|
||||||
|
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
|||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
|
|
||||||
import * as strudel from '@strudel.cycles/core';
|
import * as strudel from '@strudel.cycles/core';
|
||||||
import { fromMidi, logger, toMidi } from '@strudel.cycles/core';
|
import { fromMidi, logger, toMidi } from '@strudel.cycles/core';
|
||||||
import './feedbackdelay.mjs';
|
import './feedbackdelay.mjs';
|
||||||
@@ -157,7 +156,7 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
|
|||||||
if (!delays[orbit]) {
|
if (!delays[orbit]) {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
||||||
dly.start(t);
|
dly.start?.(t); // for some reason, this throws when audion extension is installed..
|
||||||
dly.connect(getDestination());
|
dly.connect(getDestination());
|
||||||
delays[orbit] = dly;
|
delays[orbit] = dly;
|
||||||
}
|
}
|
||||||
@@ -196,9 +195,14 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
|||||||
hap.value = { note: hap.value };
|
hap.value = { note: hap.value };
|
||||||
} */
|
} */
|
||||||
if (typeof hap.value !== 'object') {
|
if (typeof hap.value !== 'object') {
|
||||||
throw new Error(
|
logger(
|
||||||
`hap.value ${hap.value} is not supported by webaudio output. Hint: append .note() or .s() to the end`,
|
`hap.value "${hap.value}" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
|
||||||
|
'error',
|
||||||
);
|
);
|
||||||
|
/* throw new Error(
|
||||||
|
`hap.value "${hap.value}"" is not supported by webaudio output. Hint: append .note() or .s() to the end`,
|
||||||
|
); */
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// calculate correct time (tone.js workaround)
|
// calculate correct time (tone.js workaround)
|
||||||
let t = ac.currentTime + deadline;
|
let t = ac.currentTime + deadline;
|
||||||
@@ -212,12 +216,22 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
|||||||
n = 0,
|
n = 0,
|
||||||
note,
|
note,
|
||||||
gain = 0.8,
|
gain = 0.8,
|
||||||
cutoff,
|
// low pass
|
||||||
resonance = 1,
|
lpf,
|
||||||
hcutoff,
|
cutoff = lpf,
|
||||||
hresonance = 1,
|
lpq = 1,
|
||||||
bandf,
|
resonance = lpq,
|
||||||
bandq = 1,
|
// high pass
|
||||||
|
hpf,
|
||||||
|
hcutoff = hpf,
|
||||||
|
hpq = 1,
|
||||||
|
hresonance = hpq,
|
||||||
|
// band pass
|
||||||
|
bpf,
|
||||||
|
bandf = bpf,
|
||||||
|
bpq = 1,
|
||||||
|
bandq = bpq,
|
||||||
|
//
|
||||||
coarse,
|
coarse,
|
||||||
crush,
|
crush,
|
||||||
shape,
|
shape,
|
||||||
@@ -289,10 +303,10 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
|||||||
|
|
||||||
if (soundfont) {
|
if (soundfont) {
|
||||||
// is soundfont
|
// is soundfont
|
||||||
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
|
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac, freq);
|
||||||
} else {
|
} else {
|
||||||
// is sample from loaded samples(..)
|
// is sample from loaded samples(..)
|
||||||
bufferSource = await getSampleBufferSource(s, n, note, speed);
|
bufferSource = await getSampleBufferSource(s, n, note, speed, freq);
|
||||||
}
|
}
|
||||||
// asny stuff above took too long?
|
// asny stuff above took too long?
|
||||||
if (ac.currentTime > t) {
|
if (ac.currentTime > t) {
|
||||||
@@ -382,7 +396,7 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
|||||||
|
|
||||||
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps);
|
export const webaudioOutputTrigger = (t, hap, ct, cps) => webaudioOutput(hap, t - ct, hap.duration / cps);
|
||||||
|
|
||||||
Pattern.prototype.out = function () {
|
Pattern.prototype.webaudio = function () {
|
||||||
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
|
// TODO: refactor (t, hap, ct, cps) to (hap, deadline, duration) ?
|
||||||
return this.onTrigger(webaudioOutputTrigger);
|
return this.onTrigger(webaudioOutputTrigger);
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/webdirt",
|
"name": "@strudel.cycles/webdirt",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user