mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-21 20:55:12 -04:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97a38a6f59 | |||
| 986de6a73b | |||
| 1750db2f1c | |||
| eadcf25775 | |||
| 1d2c4fd771 | |||
| 92bf43e3a6 | |||
| 9dd4385bbf | |||
| 274378df42 | |||
| 5ec217ea21 | |||
| d2375f89ee |
@@ -9,13 +9,6 @@ out/**
|
|||||||
postcss.config.js
|
postcss.config.js
|
||||||
postcss.config.cjs
|
postcss.config.cjs
|
||||||
tailwind.config.js
|
tailwind.config.js
|
||||||
tailwind.config.cjs
|
|
||||||
vite.config.js
|
vite.config.js
|
||||||
/**/dist/**/*
|
/**/dist/**/*
|
||||||
!**/*.mjs
|
!**/*.mjs
|
||||||
**/*.tsx
|
|
||||||
**/*.ts
|
|
||||||
**/*.json
|
|
||||||
**/dev-dist
|
|
||||||
**/dist
|
|
||||||
/src-tauri/target/**/*
|
|
||||||
+2
-3
@@ -9,9 +9,8 @@
|
|||||||
"ecmaVersion": "latest",
|
"ecmaVersion": "latest",
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"plugins": ["import"],
|
"plugins": [],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-unused-vars": ["warn", { "destructuredArrayIgnorePattern": ".", "ignoreRestSiblings": false }],
|
"no-unused-vars": ["warn", { "destructuredArrayIgnorePattern": ".", "ignoreRestSiblings": false }]
|
||||||
"import/no-extraneous-dependencies": ["error", {"devDependencies": true}]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,18 +22,15 @@ jobs:
|
|||||||
url: ${{ steps.deployment.outputs.page_url }}
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 7
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 16
|
||||||
cache: "pnpm"
|
cache: "npm"
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pnpm install
|
run: npm ci && cd repl && npm ci && cd ../tutorial && npm ci
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build
|
run: npm run build
|
||||||
|
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v2
|
uses: actions/configure-pages@v2
|
||||||
@@ -42,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: "./website/dist"
|
path: "./out"
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
name: Tauri Builder
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
platform: [macos-latest, ubuntu-latest, windows-latest]
|
|
||||||
include:
|
|
||||||
- os: ubuntu-latest
|
|
||||||
rust_target: x86_64-unknown-linux-gnu
|
|
||||||
- os: macos-latest
|
|
||||||
rust_target: x86_64-apple-darwin
|
|
||||||
- os: macos-latest
|
|
||||||
rust_target: aarch64-apple-darwin
|
|
||||||
- os: windows-latest
|
|
||||||
rust_target: x86_64-pc-windows-msvc
|
|
||||||
runs-on: ${{ matrix.platform }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8.6.2
|
|
||||||
|
|
||||||
- name: Node.js setup
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: latest
|
|
||||||
# node-version-file: '.nvmrc'
|
|
||||||
|
|
||||||
- name: Install Rust (Stable)
|
|
||||||
run:
|
|
||||||
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
|
||||||
|
|
||||||
- name: Install dependencies (ubuntu only)
|
|
||||||
if: matrix.platform == 'ubuntu-latest'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
|
||||||
|
|
||||||
- name: Install app dependencies from lockfile and build web
|
|
||||||
run: pnpm install
|
|
||||||
|
|
||||||
- name: Build the app
|
|
||||||
uses: tauri-apps/tauri-action@v0
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
# tauri-action replaces \_\_VERSION\_\_ with the app version
|
|
||||||
tagName: ${{ github.ref_name }}
|
|
||||||
releaseName: "Strudel v__VERSION__"
|
|
||||||
releaseBody: |
|
|
||||||
See the assets to download this version and install.
|
|
||||||
releaseDraft: true
|
|
||||||
prerelease: false
|
|
||||||
@@ -10,15 +10,10 @@ jobs:
|
|||||||
node-version: [18]
|
node-version: [18]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: pnpm/action-setup@v2
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
version: 7
|
node-version: ${{ matrix.node-version }}
|
||||||
- uses: actions/setup-node@v3
|
cache: 'npm'
|
||||||
with:
|
- run: npm install
|
||||||
node-version: ${{ matrix.node-version }}
|
- run: npm test
|
||||||
cache: 'pnpm'
|
|
||||||
- run: pnpm install
|
|
||||||
- run: pnpm run format-check
|
|
||||||
- run: pnpm run lint
|
|
||||||
- run: pnpm test
|
|
||||||
|
|||||||
+1
-7
@@ -37,10 +37,4 @@ talk/public/EmuSP12
|
|||||||
talk/public/samples
|
talk/public/samples
|
||||||
server/samples/old
|
server/samples/old
|
||||||
repl/stats.html
|
repl/stats.html
|
||||||
coverage
|
coverage
|
||||||
public/icons/apple-splash-*
|
|
||||||
dev-dist
|
|
||||||
Dirt-Samples
|
|
||||||
tidal-drum-machines
|
|
||||||
webaudiofontdata
|
|
||||||
src-tauri/target
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
*.md
|
|
||||||
*.json
|
|
||||||
*.yml
|
|
||||||
*.
|
|
||||||
**/out
|
|
||||||
**/dist
|
|
||||||
packages/mini/krill-parser.js
|
|
||||||
packages/xen/tunejs.js
|
|
||||||
paper
|
|
||||||
pnpm-lock.yaml
|
|
||||||
pnpm-workspace.yaml
|
|
||||||
**/dev-dist
|
|
||||||
+15
-14
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"jsxSingleQuote": false,
|
"jsxSingleQuote": false,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"bracketSameLine": false,
|
"jsxBracketSameLine": false,
|
||||||
"arrowParens": "always",
|
"arrowParens": "always",
|
||||||
"proseWrap": "preserve",
|
"proseWrap": "preserve",
|
||||||
"htmlWhitespaceSensitivity": "css",
|
"htmlWhitespaceSensitivity": "css",
|
||||||
"endOfLine": "lf"
|
"endOfLine": "lf"
|
||||||
}
|
}
|
||||||
|
|
||||||
Vendored
+1
-2
@@ -5,6 +5,5 @@
|
|||||||
],
|
],
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
+19
-47
@@ -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 glanced through the
|
If you have any questions about strudel, make sure you've read the
|
||||||
[docs](https://strudel.tidalcycles.org/learn/) to find out if it answers your question.
|
[tutorial](https://strudel.tidalcycles.org/tutorial/) 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,10 +29,12 @@ 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 Docs
|
## Improve the Tutorial
|
||||||
|
|
||||||
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/workshop/getting-started/),
|
If you find some weak spots in the [tutorial](https://strudel.tidalcycles.org/),
|
||||||
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
|
you are welcome to improve them by editing [this file](https://github.com/tidalcycles/strudel/blob/main/tutorial/tutorial.mdx).
|
||||||
|
|
||||||
|
This will even work without setting up a development environment, only a github account is required.
|
||||||
|
|
||||||
## Propose a Feature
|
## Propose a Feature
|
||||||
|
|
||||||
@@ -58,22 +60,23 @@ To fix a bug that has been reported,
|
|||||||
## Write Tests
|
## Write Tests
|
||||||
|
|
||||||
There are still many tests that have not been written yet! Reading and writing tests is a great opportunity to get familiar with the codebase.
|
There are still many tests that have not been written yet! Reading and writing tests is a great opportunity to get familiar with the codebase.
|
||||||
You can find the tests in each package in the `test` folder. To run all tests, run `pnpm test` from the root folder.
|
You can find the tests in each package in the `test` folder. To run all tests, run `npm test` from the root folder.
|
||||||
|
|
||||||
## Project Setup
|
## Project Setup
|
||||||
|
|
||||||
To get the project up and running for development, make sure you have installed:
|
To get the project up and running for development, make sure you have installed:
|
||||||
|
|
||||||
- [git](https://git-scm.com/)
|
- git
|
||||||
- [node](https://nodejs.org/en/) >= 18
|
- node, preferably v16
|
||||||
- [pnpm](https://pnpm.io/) (`npm i pnpm -g`)
|
|
||||||
|
|
||||||
then, do the following:
|
then, do the following:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/tidalcycles/strudel.git && cd strudel
|
git clone https://github.com/tidalcycles/strudel.git && cd strudel
|
||||||
pnpm i # install at root to symlink packages
|
npm i # install at root to symlink packages
|
||||||
pnpm start # start repl
|
npx lerna bootstrap # install all dependencies in packages
|
||||||
|
cd repl && npm i # install repl dependencies
|
||||||
|
npm run start # start repl
|
||||||
```
|
```
|
||||||
|
|
||||||
Those commands might look slightly different for your OS.
|
Those commands might look slightly different for your OS.
|
||||||
@@ -82,10 +85,6 @@ Please report any problems you've had with the setup instructions!
|
|||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
To make sure the code changes only where it should, we are using prettier to unify the code style.
|
To make sure the code changes only where it should, we are using prettier to unify the code style.
|
||||||
|
|
||||||
- You can format all files at once by running `pnpm prettier` from the project root
|
|
||||||
- Run `pnpm format-check` from the project root to check if all files are well formatted
|
|
||||||
|
|
||||||
If you use VSCode, you can
|
If you use VSCode, you can
|
||||||
|
|
||||||
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||||
@@ -93,29 +92,12 @@ If you use VSCode, you can
|
|||||||
3. Choose "Configure Default Formatter..."
|
3. Choose "Configure Default Formatter..."
|
||||||
4. Select prettier
|
4. Select prettier
|
||||||
|
|
||||||
## ESLint
|
|
||||||
|
|
||||||
To prevent unwanted runtime errors, this project uses [eslint](https://eslint.org/).
|
|
||||||
|
|
||||||
- You can check for lint errors by running `pnpm lint`
|
|
||||||
|
|
||||||
There are also eslint extensions / plugins for most editors.
|
|
||||||
|
|
||||||
## Running Tests
|
|
||||||
|
|
||||||
- Run all tests with `pnpm test`
|
|
||||||
- Run all tests with UI using `pnpm test-ui`
|
|
||||||
|
|
||||||
## Running all CI Checks
|
|
||||||
|
|
||||||
When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests.
|
|
||||||
You can run the same check with `pnpm check`
|
|
||||||
|
|
||||||
## Package Workflow
|
## Package Workflow
|
||||||
|
|
||||||
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
|
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
|
||||||
When you run `pnpm i` on the root folder, [pnpm workspaces](https://pnpm.io/workspaces) will install all dependencies of all subpackages. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
|
When you run `npm i` on the root folder, [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces) will symlink all packages
|
||||||
allowing to develop multiple packages at the same time.
|
in the `node_modules` folder. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
|
||||||
|
which allows developing multiple packages at the same time
|
||||||
|
|
||||||
## Package Publishing
|
## Package Publishing
|
||||||
|
|
||||||
@@ -123,25 +105,15 @@ To publish all packages that have been changed since the last release, run:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm login
|
npm login
|
||||||
|
npx lerna publish
|
||||||
# this will increment all the versions in package.json files of non private packages to selected versions
|
|
||||||
npx lerna version --no-private
|
|
||||||
|
|
||||||
# publish all packages inside /packages using pnpm! don't use lerna to publish!!
|
|
||||||
pnpm --filter "./packages/**" publish --dry-run
|
|
||||||
|
|
||||||
# the last command was only a dry-run, make sure everything looks ok, if yes, run the same command without flag
|
|
||||||
```
|
```
|
||||||
|
|
||||||
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
|
|
||||||
Important: Always publish with `pnpm`, as `npm` does not support overriding main files in `publishConfig`, which is done in all the packages.
|
|
||||||
|
|
||||||
### New Packages
|
### New Packages
|
||||||
|
|
||||||
To add a new package, you have to publish it manually the first time, using:
|
To add a new package, you have to publish it manually the first time, using:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd packages/<package-name> && pnpm publish --access public
|
cd packages/<package-name> && npm publish --access public
|
||||||
```
|
```
|
||||||
|
|
||||||
## Have Fun
|
## Have Fun
|
||||||
|
|||||||
@@ -5,17 +5,16 @@
|
|||||||
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/>
|
||||||
- Docs: <https://strudel.tidalcycles.org/learn/>
|
- Tutorial: <https://strudel.tidalcycles.org/tutorial/>
|
||||||
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
||||||
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
|
||||||
|
|
||||||
## Running Locally
|
## Running Locally
|
||||||
|
|
||||||
After cloning the project, you can run the REPL locally:
|
After cloning the project, you can run the REPL locally:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run setup
|
npm run setup
|
||||||
pnpm run repl
|
npm run repl
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using Strudel In Your Project
|
## Using Strudel In Your Project
|
||||||
@@ -24,8 +23,8 @@ There are multiple npm packages you can use to use strudel, or only parts of it,
|
|||||||
|
|
||||||
- [`core`](./packages/core/): tidal pattern engine
|
- [`core`](./packages/core/): tidal pattern engine
|
||||||
- [`mini`](./packages/mini): mini notation parser + core binding
|
- [`mini`](./packages/mini): mini notation parser + core binding
|
||||||
- [`transpiler`](./packages/transpiler): user code transpiler
|
- [`eval`](./packages/eval): user code evaluator. syntax sugar + highlighting
|
||||||
- [`webaudio`](./packages/webaudio): webaudio output
|
- [`tone`](./packages/tone): bindings for Tone.js instruments and effects
|
||||||
- [`osc`](./packages/osc): bindings to communicate via OSC
|
- [`osc`](./packages/osc): bindings to communicate via OSC
|
||||||
- [`midi`](./packages/midi): webmidi bindings
|
- [`midi`](./packages/midi): webmidi bindings
|
||||||
- [`serial`](./packages/serial): webserial bindings
|
- [`serial`](./packages/serial): webserial bindings
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
// this barrel export is currently only used to find undocumented exports
|
|
||||||
export * from './packages/core/index.mjs';
|
|
||||||
export * from './packages/csound/index.mjs';
|
|
||||||
export * from './packages/embed/index.mjs';
|
|
||||||
export * from './packages/desktopbridge/index.mjs';
|
|
||||||
export * from './packages/midi/index.mjs';
|
|
||||||
export * from './packages/mini/index.mjs';
|
|
||||||
export * from './packages/osc/index.mjs';
|
|
||||||
export * from './packages/react/index.mjs';
|
|
||||||
export * from './packages/serial/index.mjs';
|
|
||||||
export * from './packages/soundfonts/index.mjs';
|
|
||||||
export * from './packages/tonal/index.mjs';
|
|
||||||
export * from './packages/transpiler/index.mjs';
|
|
||||||
export * from './packages/webaudio/index.mjs';
|
|
||||||
export * from './packages/xen/index.mjs';
|
|
||||||
+1
-3
@@ -2,7 +2,5 @@
|
|||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"version": "independent",
|
"version": "independent"
|
||||||
"npmClient": "pnpm",
|
|
||||||
"useWorkspaces": true
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
# my-patterns
|
|
||||||
|
|
||||||
This directory can be used to save your own patterns, which then get
|
|
||||||
made into a pattern swatch.
|
|
||||||
|
|
||||||
Example: <https://felixroos.github.io/strudel/swatch/>
|
|
||||||
|
|
||||||
## deploy
|
|
||||||
|
|
||||||
### 1. fork the [strudel repo on github](https://github.com/tidalcycles/strudel.git)
|
|
||||||
|
|
||||||
### 2. clone your fork to your machine `git clone https://github.com/<your-username>/strudel.git strudel && cd strudel`
|
|
||||||
|
|
||||||
### 3. create a separate branch like `git branch patternuary && git checkout patternuary`
|
|
||||||
|
|
||||||
### 4. save one or more .txt files in the my-patterns folder
|
|
||||||
|
|
||||||
### 5. edit `website/public/CNAME` to contain `<your-username>.github.io/strudel`
|
|
||||||
|
|
||||||
### 6. edit `website/astro.config.mjs` to use site: `https://<your-username>.github.io` and base `/strudel`, like this
|
|
||||||
|
|
||||||
```js
|
|
||||||
const site = 'https://<your-username>.github.io';
|
|
||||||
const base = '/strudel';
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. commit & push the changes
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git add . && git commit -m "site config" && git push --set-upstream origin
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8. deploy to github pages
|
|
||||||
|
|
||||||
- go to settings -> pages and select "Github Actions" as source
|
|
||||||
- go to settings -> environments -> github-pages and press the edit button next to `main` and type in `patternuary` (under "Deployment branches")
|
|
||||||
- go to Actions -> `Build and Deploy` and click `Run workflow` with branch `patternuary`
|
|
||||||
|
|
||||||
### 9. 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).
|
|
||||||
|
|
||||||
### 10. optional: automatic deployment
|
|
||||||
|
|
||||||
If you want to automatically deploy your site on push, go to `deploy.yml` and change `workflow_dispatch` to `push`.
|
|
||||||
|
|
||||||
## running locally
|
|
||||||
|
|
||||||
- install dependencies with `npm run setup`
|
|
||||||
- run dev server with `npm run repl` and open `http://localhost:3000/strudel/swatch/`
|
|
||||||
|
|
||||||
## tests fail?
|
|
||||||
|
|
||||||
Your tests might fail if the code does not follow prettiers format.
|
|
||||||
In that case, run `npm run codeformat`. To disable that, remove `npm run format-check` from `test.yml`
|
|
||||||
|
|
||||||
## updating your fork
|
|
||||||
|
|
||||||
To update your fork, you can pull the main branch and merge it into your `patternuary` branch.
|
|
||||||
Generated
+20352
File diff suppressed because it is too large
Load Diff
+24
-40
@@ -1,32 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/monorepo",
|
"name": "@strudel.cycles/monorepo",
|
||||||
"version": "0.5.0",
|
"version": "0.0.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Port of tidalcycles to javascript",
|
"description": "Port of tidalcycles to javascript",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"setup": "pnpm i",
|
"pretest": "cd tutorial && npm run jsdoc-json",
|
||||||
"pretest": "npm run jsdoc-json",
|
"test": "npm run lint && vitest run --version",
|
||||||
"prebuild": "npm run jsdoc-json",
|
"test-ui": "vitest --ui",
|
||||||
"prestart": "npm run jsdoc-json",
|
"test-coverage": "vitest --coverage",
|
||||||
"test": "npm run pretest && vitest run --version",
|
"bootstrap": "lerna bootstrap",
|
||||||
"test-ui": "npm run pretest && vitest --ui",
|
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
|
||||||
"test-coverage": "npm run pretest && vitest --coverage",
|
"snapshot": "vitest run -u --silent",
|
||||||
"snapshot": "npm run pretest && vitest run -u --silent",
|
"repl": "cd repl && npm run dev",
|
||||||
"repl": "npm run prestart && cd website && npm run dev",
|
|
||||||
"start": "npm run prestart && cd website && npm run dev",
|
|
||||||
"dev": "npm run prestart && cd website && npm run dev",
|
|
||||||
"build": "npm run prebuild && cd website && npm run build",
|
|
||||||
"preview": "cd website && npm run preview",
|
|
||||||
"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",
|
||||||
|
"preview": "npx serve ./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",
|
"lint": "npx eslint . --ext mjs,js --quiet"
|
||||||
"codeformat": "prettier --write .",
|
|
||||||
"format-check": "prettier --check .",
|
|
||||||
"report-undocumented": "npm run jsdoc-json && node undocumented.mjs > undocumented.json",
|
|
||||||
"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": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
@@ -44,29 +40,17 @@
|
|||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"homepage": "https://strudel.tidalcycles.org",
|
||||||
"dependencies": {
|
|
||||||
"@strudel.cycles/core": "workspace:*",
|
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
|
||||||
"@strudel.cycles/tonal": "workspace:*",
|
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
|
||||||
"@strudel.cycles/xen": "workspace:*",
|
|
||||||
"acorn": "^8.8.1",
|
|
||||||
"dependency-tree": "^9.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.4.0",
|
"@vitest/ui": "^0.21.1",
|
||||||
"@vitest/ui": "^0.28.0",
|
"c8": "^7.12.0",
|
||||||
"canvas": "^2.11.2",
|
"eslint": "^8.28.0",
|
||||||
"eslint": "^8.39.0",
|
|
||||||
"eslint-plugin-import": "^2.27.5",
|
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"jsdoc": "^4.0.2",
|
"gh-pages": "^4.0.0",
|
||||||
|
"jsdoc": "^3.6.10",
|
||||||
"jsdoc-json": "^2.0.2",
|
"jsdoc-json": "^2.0.2",
|
||||||
"jsdoc-to-markdown": "^8.0.0",
|
"jsdoc-to-markdown": "^7.1.1",
|
||||||
"lerna": "^6.6.1",
|
"lerna": "^4.0.0",
|
||||||
"prettier": "^2.8.8",
|
|
||||||
"rollup-plugin-visualizer": "^5.8.1",
|
"rollup-plugin-visualizer": "^5.8.1",
|
||||||
"vitest": "^0.33.0"
|
"vitest": "^0.21.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# @strudel/codemirror
|
|
||||||
|
|
||||||
This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it.
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { defaultKeymap } from '@codemirror/commands';
|
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
|
||||||
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
||||||
import { EditorState } from '@codemirror/state';
|
|
||||||
import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from '@codemirror/view';
|
|
||||||
import { Drawer, repl } from '@strudel.cycles/core';
|
|
||||||
import { flashField, flash } from './flash.mjs';
|
|
||||||
import { highlightExtension, highlightMiniLocations } from './highlight.mjs';
|
|
||||||
import { oneDark } from './themes/one-dark';
|
|
||||||
|
|
||||||
// https://codemirror.net/docs/guide/
|
|
||||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
|
|
||||||
let state = EditorState.create({
|
|
||||||
doc: initialCode,
|
|
||||||
extensions: [
|
|
||||||
theme,
|
|
||||||
javascript(),
|
|
||||||
lineNumbers(),
|
|
||||||
highlightExtension,
|
|
||||||
highlightActiveLineGutter(),
|
|
||||||
syntaxHighlighting(defaultHighlightStyle),
|
|
||||||
keymap.of(defaultKeymap),
|
|
||||||
flashField,
|
|
||||||
EditorView.updateListener.of((v) => onChange(v)),
|
|
||||||
keymap.of([
|
|
||||||
{
|
|
||||||
key: 'Ctrl-Enter',
|
|
||||||
run: () => onEvaluate(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Ctrl-.',
|
|
||||||
run: () => onStop(),
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
return new EditorView({
|
|
||||||
state,
|
|
||||||
parent: root,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export class StrudelMirror {
|
|
||||||
constructor(options) {
|
|
||||||
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
|
|
||||||
this.code = initialCode;
|
|
||||||
|
|
||||||
this.drawer = new Drawer((haps, time) => {
|
|
||||||
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
|
|
||||||
this.highlight(currentFrame, time);
|
|
||||||
onDraw?.(haps, time, currentFrame);
|
|
||||||
}, drawTime);
|
|
||||||
|
|
||||||
const prebaked = prebake();
|
|
||||||
prebaked.then(async () => {
|
|
||||||
if (!onDraw) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { scheduler, evaluate } = await this.repl;
|
|
||||||
// draw first frame instantly
|
|
||||||
prebaked.then(async () => {
|
|
||||||
await evaluate(this.code, false);
|
|
||||||
this.drawer.invalidate(scheduler);
|
|
||||||
onDraw?.(this.drawer.visibleHaps, 0, []);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.repl = repl({
|
|
||||||
...replOptions,
|
|
||||||
onToggle: async (started) => {
|
|
||||||
replOptions?.onToggle?.(started);
|
|
||||||
const { scheduler } = await this.repl;
|
|
||||||
if (started) {
|
|
||||||
this.drawer.start(scheduler);
|
|
||||||
} else {
|
|
||||||
this.drawer.stop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeEval: async () => {
|
|
||||||
await prebaked;
|
|
||||||
},
|
|
||||||
afterEval: (options) => {
|
|
||||||
replOptions?.afterEval?.(options);
|
|
||||||
this.drawer.invalidate();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.editor = initEditor({
|
|
||||||
root,
|
|
||||||
initialCode,
|
|
||||||
onChange: (v) => {
|
|
||||||
this.code = v.state.doc.toString();
|
|
||||||
},
|
|
||||||
onEvaluate: () => this.evaluate(),
|
|
||||||
onStop: () => this.stop(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async evaluate() {
|
|
||||||
const { evaluate } = await this.repl;
|
|
||||||
this.flash();
|
|
||||||
await evaluate(this.code);
|
|
||||||
}
|
|
||||||
async stop() {
|
|
||||||
const { scheduler } = await this.repl;
|
|
||||||
scheduler.stop();
|
|
||||||
}
|
|
||||||
flash(ms) {
|
|
||||||
flash(this.editor, ms);
|
|
||||||
}
|
|
||||||
highlight(haps, time) {
|
|
||||||
highlightMiniLocations(this.editor.view, time, haps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { StateEffect, StateField } from '@codemirror/state';
|
|
||||||
import { Decoration, EditorView } from '@codemirror/view';
|
|
||||||
|
|
||||||
export const setFlash = StateEffect.define();
|
|
||||||
export const flashField = StateField.define({
|
|
||||||
create() {
|
|
||||||
return Decoration.none;
|
|
||||||
},
|
|
||||||
update(flash, tr) {
|
|
||||||
try {
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(setFlash)) {
|
|
||||||
if (e.value && tr.newDoc.length > 0) {
|
|
||||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
|
||||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
|
||||||
} else {
|
|
||||||
flash = Decoration.set([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return flash;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('flash error', err);
|
|
||||||
return flash;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
provide: (f) => EditorView.decorations.from(f),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const flash = (view, ms = 200) => {
|
|
||||||
view.dispatch({ effects: setFlash.of(true) });
|
|
||||||
setTimeout(() => {
|
|
||||||
view.dispatch({ effects: setFlash.of(false) });
|
|
||||||
}, ms);
|
|
||||||
};
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
|
||||||
import { Decoration, EditorView } from '@codemirror/view';
|
|
||||||
|
|
||||||
export const setMiniLocations = StateEffect.define();
|
|
||||||
export const showMiniLocations = StateEffect.define();
|
|
||||||
export const updateMiniLocations = (view, locations) => {
|
|
||||||
view.dispatch({ effects: setMiniLocations.of(locations) });
|
|
||||||
};
|
|
||||||
export const highlightMiniLocations = (view, atTime, haps) => {
|
|
||||||
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
|
|
||||||
};
|
|
||||||
|
|
||||||
const miniLocations = StateField.define({
|
|
||||||
create() {
|
|
||||||
return Decoration.none;
|
|
||||||
},
|
|
||||||
update(locations, tr) {
|
|
||||||
if (tr.docChanged) {
|
|
||||||
locations = locations.map(tr.changes);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(setMiniLocations)) {
|
|
||||||
// this is called on eval, with the mini locations obtained from the transpiler
|
|
||||||
// codemirror will automatically remap the marks when the document is edited
|
|
||||||
// create a mark for each mini location, adding the range to the spec to find it later
|
|
||||||
const marks = e.value
|
|
||||||
.filter(([from]) => from < tr.newDoc.length)
|
|
||||||
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
|
|
||||||
.map(
|
|
||||||
(range) =>
|
|
||||||
Decoration.mark({
|
|
||||||
id: range.join(':'),
|
|
||||||
// this green is only to verify that the decoration moves when the document is edited
|
|
||||||
// it will be removed later, so the mark is not visible by default
|
|
||||||
attributes: { style: `background-color: #00CA2880` },
|
|
||||||
}).range(...range), // -> Decoration
|
|
||||||
);
|
|
||||||
|
|
||||||
locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<Decoration>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return locations;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const visibleMiniLocations = StateField.define({
|
|
||||||
create() {
|
|
||||||
return { atTime: 0, haps: new Map() };
|
|
||||||
},
|
|
||||||
update(visible, tr) {
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(showMiniLocations)) {
|
|
||||||
// this is called every frame to show the locations that are currently active
|
|
||||||
// we can NOT create new marks because the context.locations haven't changed since eval time
|
|
||||||
// this is why we need to find a way to update the existing decorations, showing the ones that have an active range
|
|
||||||
const haps = new Map();
|
|
||||||
for (let hap of e.value.haps) {
|
|
||||||
for (let { start, end } of hap.context.locations) {
|
|
||||||
let id = `${start}:${end}`;
|
|
||||||
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
|
|
||||||
haps.set(id, hap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visible = { atTime: e.value.atTime, haps };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return visible;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// // Derive the set of decorations from the miniLocations and visibleLocations
|
|
||||||
const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
|
|
||||||
const iterator = state.field(miniLocations).iter();
|
|
||||||
const { haps } = state.field(visibleMiniLocations);
|
|
||||||
const builder = new RangeSetBuilder();
|
|
||||||
|
|
||||||
while (iterator.value) {
|
|
||||||
const {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
value: {
|
|
||||||
spec: { id },
|
|
||||||
},
|
|
||||||
} = iterator;
|
|
||||||
|
|
||||||
if (haps.has(id)) {
|
|
||||||
const hap = haps.get(id);
|
|
||||||
const color = hap.context.color ?? 'var(--foreground)';
|
|
||||||
// Get explicit channels for color values
|
|
||||||
/*
|
|
||||||
const swatch = document.createElement('div');
|
|
||||||
swatch.style.color = color;
|
|
||||||
document.body.appendChild(swatch);
|
|
||||||
let channels = getComputedStyle(swatch)
|
|
||||||
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
|
|
||||||
.slice(1)
|
|
||||||
.map((c) => parseFloat(c || 1));
|
|
||||||
document.body.removeChild(swatch);
|
|
||||||
|
|
||||||
// Get percentage of event
|
|
||||||
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
|
|
||||||
channels[3] *= percent;
|
|
||||||
*/
|
|
||||||
|
|
||||||
builder.add(
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
Decoration.mark({
|
|
||||||
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
|
|
||||||
attributes: { style: `outline: solid 2px ${color}` },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
iterator.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.finish();
|
|
||||||
});
|
|
||||||
|
|
||||||
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from './codemirror.mjs';
|
|
||||||
export * from './highlight.mjs';
|
|
||||||
export * from './flash.mjs';
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@strudel/codemirror",
|
|
||||||
"version": "0.9.0",
|
|
||||||
"description": "Codemirror Extensions for Strudel",
|
|
||||||
"main": "index.mjs",
|
|
||||||
"publishConfig": {
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"module": "dist/index.mjs"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "vite build",
|
|
||||||
"prepublishOnly": "npm run build"
|
|
||||||
},
|
|
||||||
"type": "module",
|
|
||||||
"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": {
|
|
||||||
"@codemirror/commands": "^6.2.4",
|
|
||||||
"@codemirror/lang-javascript": "^6.1.7",
|
|
||||||
"@codemirror/language": "^6.6.0",
|
|
||||||
"@codemirror/state": "^6.2.0",
|
|
||||||
"@codemirror/view": "^6.10.0",
|
|
||||||
"@lezer/highlight": "^1.1.4",
|
|
||||||
"@strudel.cycles/core": "workspace:*"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"vite": "^4.3.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-139
@@ -1,139 +0,0 @@
|
|||||||
import { EditorView } from '@codemirror/view';
|
|
||||||
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
||||||
import { tags as t } from '@lezer/highlight';
|
|
||||||
|
|
||||||
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
|
|
||||||
|
|
||||||
const chalky = '#e5c07b',
|
|
||||||
coral = '#e06c75',
|
|
||||||
cyan = '#56b6c2',
|
|
||||||
invalid = '#ffffff',
|
|
||||||
ivory = '#abb2bf',
|
|
||||||
stone = '#7d8799', // Brightened compared to original to increase contrast
|
|
||||||
malibu = '#61afef',
|
|
||||||
sage = '#98c379',
|
|
||||||
whiskey = '#d19a66',
|
|
||||||
violet = '#c678dd',
|
|
||||||
darkBackground = '#21252b',
|
|
||||||
highlightBackground = '#2c313a',
|
|
||||||
background = '#282c34',
|
|
||||||
tooltipBackground = '#353a42',
|
|
||||||
selection = '#3E4451',
|
|
||||||
cursor = '#528bff';
|
|
||||||
|
|
||||||
/// The colors used in the theme, as CSS color strings.
|
|
||||||
export const color = {
|
|
||||||
chalky,
|
|
||||||
coral,
|
|
||||||
cyan,
|
|
||||||
invalid,
|
|
||||||
ivory,
|
|
||||||
stone,
|
|
||||||
malibu,
|
|
||||||
sage,
|
|
||||||
whiskey,
|
|
||||||
violet,
|
|
||||||
darkBackground,
|
|
||||||
highlightBackground,
|
|
||||||
background,
|
|
||||||
tooltipBackground,
|
|
||||||
selection,
|
|
||||||
cursor,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The editor theme styles for One Dark.
|
|
||||||
export const oneDarkTheme = EditorView.theme(
|
|
||||||
{
|
|
||||||
'&': {
|
|
||||||
color: ivory,
|
|
||||||
backgroundColor: background,
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-content': {
|
|
||||||
caretColor: cursor,
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
|
|
||||||
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
|
|
||||||
{ backgroundColor: selection },
|
|
||||||
|
|
||||||
'.cm-panels': { backgroundColor: darkBackground, color: ivory },
|
|
||||||
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
|
||||||
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
|
||||||
|
|
||||||
'.cm-searchMatch': {
|
|
||||||
backgroundColor: '#72a1ff59',
|
|
||||||
outline: '1px solid #457dff',
|
|
||||||
},
|
|
||||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
|
||||||
backgroundColor: '#6199ff2f',
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
|
|
||||||
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
|
||||||
|
|
||||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
|
||||||
backgroundColor: '#bad0f847',
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-gutters': {
|
|
||||||
backgroundColor: background,
|
|
||||||
color: stone,
|
|
||||||
border: 'none',
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-activeLineGutter': {
|
|
||||||
backgroundColor: highlightBackground,
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-foldPlaceholder': {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
color: '#ddd',
|
|
||||||
},
|
|
||||||
|
|
||||||
'.cm-tooltip': {
|
|
||||||
border: 'none',
|
|
||||||
backgroundColor: tooltipBackground,
|
|
||||||
},
|
|
||||||
'.cm-tooltip .cm-tooltip-arrow:before': {
|
|
||||||
borderTopColor: 'transparent',
|
|
||||||
borderBottomColor: 'transparent',
|
|
||||||
},
|
|
||||||
'.cm-tooltip .cm-tooltip-arrow:after': {
|
|
||||||
borderTopColor: tooltipBackground,
|
|
||||||
borderBottomColor: tooltipBackground,
|
|
||||||
},
|
|
||||||
'.cm-tooltip-autocomplete': {
|
|
||||||
'& > ul > li[aria-selected]': {
|
|
||||||
backgroundColor: highlightBackground,
|
|
||||||
color: ivory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ dark: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
/// The highlighting style for code in the One Dark theme.
|
|
||||||
export const oneDarkHighlightStyle = HighlightStyle.define([
|
|
||||||
{ tag: t.keyword, color: violet },
|
|
||||||
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
|
|
||||||
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
|
|
||||||
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
|
|
||||||
{ tag: [t.definition(t.name), t.separator], color: ivory },
|
|
||||||
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
|
|
||||||
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
|
|
||||||
{ tag: [t.meta, t.comment], color: stone },
|
|
||||||
{ tag: t.strong, fontWeight: 'bold' },
|
|
||||||
{ tag: t.emphasis, fontStyle: 'italic' },
|
|
||||||
{ tag: t.strikethrough, textDecoration: 'line-through' },
|
|
||||||
{ tag: t.link, color: stone, textDecoration: 'underline' },
|
|
||||||
{ tag: t.heading, fontWeight: 'bold', color: coral },
|
|
||||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
|
|
||||||
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
|
|
||||||
{ tag: t.invalid, color: invalid },
|
|
||||||
]);
|
|
||||||
|
|
||||||
/// Extension to enable the One Dark theme (both the editor theme and
|
|
||||||
/// the highlight style).
|
|
||||||
export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import { dependencies } from './package.json';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [],
|
|
||||||
build: {
|
|
||||||
lib: {
|
|
||||||
entry: resolve(__dirname, 'index.mjs'),
|
|
||||||
formats: ['es', 'cjs'],
|
|
||||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
external: [...Object.keys(dependencies)],
|
|
||||||
},
|
|
||||||
target: 'esnext',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -33,7 +33,6 @@ b: 3/2 - 7/4
|
|||||||
c: 7/4 - 2
|
c: 7/4 - 2
|
||||||
```
|
```
|
||||||
|
|
||||||
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-forked-9ywhv7?file=/src/index.js).
|
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-qmz6qr?file=/src/index.js).
|
||||||
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/canvas.html)
|
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/package-examples/packages/core/examples/canvas.html)
|
||||||
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/vanilla.html)
|
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/package-examples/packages/core/examples/metro.html)
|
||||||
- [open minimal vite example](./examples/vite-vanilla-repl/)
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
|
||||||
import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
|
|
||||||
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, angle = 0, fill = 'darkseagreen' } = f.value;
|
|
||||||
w *= ww;
|
|
||||||
h *= wh;
|
|
||||||
if (r !== undefined && angle !== undefined) {
|
|
||||||
const radians = angle * 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, angle, r, fill, smear } = createParams('x', 'y', 'w', 'h', 'angle', '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);
|
|
||||||
});
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
+365
-795
File diff suppressed because it is too large
Load Diff
+19
-25
@@ -8,51 +8,46 @@ import createClock from './zyklus.mjs';
|
|||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
|
|
||||||
export class Cyclist {
|
export class Cyclist {
|
||||||
|
worker;
|
||||||
|
pattern;
|
||||||
|
started = false;
|
||||||
|
cps = 1; // TODO
|
||||||
|
getTime;
|
||||||
|
phase = 0;
|
||||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||||
this.started = false;
|
this.getTime = getTime;
|
||||||
this.cps = 1;
|
|
||||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
|
||||||
this.lastBegin = 0; // query begin of last tick
|
|
||||||
this.lastEnd = 0; // query end of last tick
|
|
||||||
this.getTime = getTime; // get absolute time
|
|
||||||
this.onToggle = onToggle;
|
this.onToggle = onToggle;
|
||||||
this.latency = latency; // fixed trigger time offset
|
this.latency = latency;
|
||||||
const round = (x) => Math.round(x * 1000) / 1000;
|
const round = (x) => Math.round(x * 1000) / 1000;
|
||||||
this.clock = createClock(
|
this.clock = createClock(
|
||||||
getTime,
|
getTime,
|
||||||
// called slightly before each cycle
|
|
||||||
(phase, duration, tick) => {
|
(phase, duration, tick) => {
|
||||||
if (tick === 0) {
|
if (tick === 0) {
|
||||||
this.origin = phase;
|
this.origin = phase;
|
||||||
}
|
}
|
||||||
|
const begin = round(phase - this.origin);
|
||||||
|
this.phase = begin - latency;
|
||||||
|
const end = round(begin + duration);
|
||||||
|
const time = getTime();
|
||||||
try {
|
try {
|
||||||
const time = getTime();
|
const haps = this.pattern.queryArc(begin, end); // get Haps
|
||||||
const begin = this.lastEnd;
|
|
||||||
this.lastBegin = begin;
|
|
||||||
const end = round(begin + duration * this.cps);
|
|
||||||
this.lastEnd = end;
|
|
||||||
const haps = this.pattern.queryArc(begin, end);
|
|
||||||
const tickdeadline = phase - time; // time left till phase begins
|
|
||||||
this.lastTick = time + tickdeadline;
|
|
||||||
|
|
||||||
haps.forEach((hap) => {
|
haps.forEach((hap) => {
|
||||||
if (hap.part.begin.equals(hap.whole.begin)) {
|
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||||
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
|
const deadline = hap.whole.begin + this.origin - time + latency;
|
||||||
const duration = hap.duration / this.cps;
|
const duration = hap.duration * 1;
|
||||||
onTrigger?.(hap, deadline, duration, this.cps);
|
onTrigger?.(hap, deadline, duration);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger(`[cyclist] error: ${e.message}`);
|
logger(`[cyclist] error: ${e.message}`);
|
||||||
onError?.(e);
|
onError?.(e);
|
||||||
}
|
}
|
||||||
},
|
}, // called slightly before each cycle
|
||||||
interval, // duration of each cycle
|
interval, // duration of each cycle
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
now() {
|
getPhase() {
|
||||||
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
|
return this.getTime() - this.origin - this.latency;
|
||||||
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
|
|
||||||
}
|
}
|
||||||
setStarted(v) {
|
setStarted(v) {
|
||||||
this.started = v;
|
this.started = v;
|
||||||
@@ -74,7 +69,6 @@ export class Cyclist {
|
|||||||
stop() {
|
stop() {
|
||||||
logger('[cyclist] stop');
|
logger('[cyclist] stop');
|
||||||
this.clock.stop();
|
this.clock.stop();
|
||||||
this.lastEnd = 0;
|
|
||||||
this.setStarted(false);
|
this.setStarted(false);
|
||||||
}
|
}
|
||||||
setPattern(pat, autostart = false) {
|
setPattern(pat, autostart = false) {
|
||||||
|
|||||||
+11
-119
@@ -1,37 +1,29 @@
|
|||||||
/*
|
/*
|
||||||
draw.mjs - <short description TODO>
|
draw.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/draw.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
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, State, TimeSpan } from './index.mjs';
|
import pattern from './pattern.mjs';
|
||||||
|
const Pattern = pattern.Pattern;
|
||||||
|
import { getTime } from './time.mjs';
|
||||||
|
import { State } from './state.mjs';
|
||||||
|
import { TimeSpan } from './timespan.mjs';
|
||||||
|
|
||||||
export const getDrawContext = (id = 'test-canvas') => {
|
export const getDrawContext = (id = 'test-canvas') => {
|
||||||
let canvas = document.querySelector('#' + id);
|
let canvas = document.querySelector('#' + id);
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
const scale = 2; // 2 = crisp on retina screens
|
|
||||||
canvas = document.createElement('canvas');
|
canvas = document.createElement('canvas');
|
||||||
canvas.id = id;
|
canvas.id = id;
|
||||||
canvas.width = window.innerWidth * scale;
|
canvas.width = window.innerWidth;
|
||||||
canvas.height = window.innerHeight * scale;
|
canvas.height = window.innerHeight;
|
||||||
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
|
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0;z-index:5';
|
||||||
document.body.prepend(canvas);
|
document.body.prepend(canvas);
|
||||||
let timeout;
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
timeout && clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
canvas.width = window.innerWidth * scale;
|
|
||||||
canvas.height = window.innerHeight * scale;
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return canvas.getContext('2d');
|
return canvas.getContext('2d');
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
|
Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (window.strudelAnimation) {
|
if (window.strudelAnimation) {
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
}
|
}
|
||||||
@@ -63,7 +55,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
|
|||||||
|
|
||||||
export const cleanupDraw = (clearScreen = true) => {
|
export const cleanupDraw = (clearScreen = true) => {
|
||||||
const ctx = getDrawContext();
|
const ctx = getDrawContext();
|
||||||
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
|
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||||
if (window.strudelAnimation) {
|
if (window.strudelAnimation) {
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
}
|
}
|
||||||
@@ -71,103 +63,3 @@ export const cleanupDraw = (clearScreen = true) => {
|
|||||||
clearInterval(window.strudelScheduler);
|
clearInterval(window.strudelScheduler);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.onPaint = function (onPaint) {
|
|
||||||
// this is evil! TODO: add pattern.context
|
|
||||||
this.context = { onPaint };
|
|
||||||
return this;
|
|
||||||
};
|
|
||||||
|
|
||||||
// const round = (x) => Math.round(x * 1000) / 1000;
|
|
||||||
|
|
||||||
// encapsulates starting and stopping animation frames
|
|
||||||
export class Framer {
|
|
||||||
constructor(onFrame, onError) {
|
|
||||||
this.onFrame = onFrame;
|
|
||||||
this.onError = onError;
|
|
||||||
}
|
|
||||||
start() {
|
|
||||||
const self = this;
|
|
||||||
let frame = requestAnimationFrame(function updateHighlights(time) {
|
|
||||||
try {
|
|
||||||
self.onFrame(time);
|
|
||||||
} catch (err) {
|
|
||||||
self.onError(err);
|
|
||||||
}
|
|
||||||
frame = requestAnimationFrame(updateHighlights);
|
|
||||||
});
|
|
||||||
self.cancel = () => {
|
|
||||||
cancelAnimationFrame(frame);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
stop() {
|
|
||||||
if (this.cancel) {
|
|
||||||
this.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// syncs animation frames to a cyclist scheduler
|
|
||||||
// see vite-vanilla-repl-cm6 for an example
|
|
||||||
export class Drawer {
|
|
||||||
constructor(onDraw, drawTime) {
|
|
||||||
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
|
|
||||||
lookbehind = Math.abs(lookbehind);
|
|
||||||
this.visibleHaps = [];
|
|
||||||
this.lastFrame = null;
|
|
||||||
this.drawTime = drawTime;
|
|
||||||
this.framer = new Framer(
|
|
||||||
() => {
|
|
||||||
if (!this.scheduler) {
|
|
||||||
console.warn('Drawer: no scheduler');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// calculate current frame time (think right side of screen for pianoroll)
|
|
||||||
const phase = this.scheduler.now() + lookahead;
|
|
||||||
// first frame just captures the phase
|
|
||||||
if (this.lastFrame === null) {
|
|
||||||
this.lastFrame = phase;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// query haps from last frame till now. take last 100ms max
|
|
||||||
const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase);
|
|
||||||
this.lastFrame = phase;
|
|
||||||
this.visibleHaps = (this.visibleHaps || [])
|
|
||||||
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
|
|
||||||
.filter((h) => h.whole.end >= phase - lookbehind - lookahead)
|
|
||||||
// add new haps with onset (think right edge bars scrolling in)
|
|
||||||
.concat(haps.filter((h) => h.hasOnset()));
|
|
||||||
const time = phase - lookahead;
|
|
||||||
onDraw(this.visibleHaps, time, this);
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
console.warn('draw error', err);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
invalidate(scheduler = this.scheduler) {
|
|
||||||
if (!scheduler) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.scheduler = scheduler;
|
|
||||||
const t = scheduler.now();
|
|
||||||
let [_, lookahead] = this.drawTime;
|
|
||||||
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
|
|
||||||
// remove all future haps
|
|
||||||
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
|
|
||||||
// query future haps
|
|
||||||
const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query..
|
|
||||||
// append future haps
|
|
||||||
this.visibleHaps = this.visibleHaps.concat(futureHaps);
|
|
||||||
}
|
|
||||||
start(scheduler) {
|
|
||||||
this.scheduler = scheduler;
|
|
||||||
this.invalidate();
|
|
||||||
this.framer.start();
|
|
||||||
}
|
|
||||||
stop() {
|
|
||||||
if (this.framer) {
|
|
||||||
this.framer.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+30
-96
@@ -1,91 +1,48 @@
|
|||||||
/*
|
/*
|
||||||
euclid.mjs - Bjorklund/Euclidean/Diaspora rhythms
|
euclid.mjs - <short description TODO>
|
||||||
Copyright (C) 2023 Rohan Drape and strudel contributors
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs>
|
||||||
|
|
||||||
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, register, silence } from './pattern.mjs';
|
import pattern from './pattern.mjs';
|
||||||
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
|
const { Pattern, timeCat } = pattern;
|
||||||
|
import bjork from 'bjork';
|
||||||
|
import { rotate } from './util.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
|
|
||||||
const left = function (n, x) {
|
const euclid = (pulses, steps, rotation = 0) => {
|
||||||
const [ons, offs] = n;
|
const b = bjork(steps, pulses);
|
||||||
const [xs, ys] = x;
|
if (rotation) {
|
||||||
const [_xs, __xs] = splitAt(offs, xs);
|
return rotate(b, -rotation);
|
||||||
return [
|
}
|
||||||
[offs, ons - offs],
|
return b;
|
||||||
[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)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export 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
|
* Euclidian rhythms are rhythms obtained using the greatest common divisor of two numbers.
|
||||||
* divisor of two numbers. They were described in 2004 by Godfried
|
* They were described in 2004 by Godfried Toussaint, a canadian computer scientist.
|
||||||
* Toussaint, a canadian computer scientist. Euclidian rhythms are
|
* Euclidian rhythms are really useful for computer/algorithmic music because they can accurately
|
||||||
* really useful for computer/algorithmic music because they can
|
* describe a large number of rhythms used in the most important music world traditions.
|
||||||
* 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").euclidRot(3,5,2)
|
* note("c3").euclid(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.
|
||||||
@@ -115,57 +72,34 @@ export const bjork = function (ons, steps) {
|
|||||||
* @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").euclidRot(7,16,14)
|
* note("c3").euclid(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").euclidRot(11,24,14)
|
* note("c3").euclid(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").euclidRot(13,24,5)
|
* note("c3").euclid(13,24,5)
|
||||||
*/
|
*/
|
||||||
|
Pattern.prototype.euclid = function (pulses, steps, rotation = 0) {
|
||||||
const _euclidRot = function (pulses, steps, rotation) {
|
return this.struct(euclid(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,
|
* Similar to `.euclid`, but each pulse is held until the next pulse, so there will be no gaps.
|
||||||
* 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 _euclidLegato = function (pulses, steps, rotation, pat) {
|
const bin_pat = euclid(pulses, steps, rotation);
|
||||||
if (pulses < 1) {
|
const firstOne = bin_pat.indexOf(1);
|
||||||
return silence;
|
const gapless = rotate(bin_pat, firstOne)
|
||||||
}
|
|
||||||
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 pat.struct(timeCat(...gapless));
|
return this.struct(timeCat(...gapless)).late(Fraction(firstOne).div(steps));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const euclidLegato = register(['euclidLegato'], function (pulses, steps, pat) {
|
export default euclid;
|
||||||
return _euclidLegato(pulses, steps, 0, pat);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
|
||||||
return _euclidLegato(pulses, steps, rotation, pat);
|
|
||||||
});
|
|
||||||
|
|||||||
+14
-16
@@ -1,12 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
evaluate.mjs - <short description TODO>
|
evaluate.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/evaluate.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
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 } from './index.mjs';
|
import pattern from './pattern.mjs';
|
||||||
|
const { isPattern, Pattern } = pattern;
|
||||||
|
|
||||||
|
let scoped = false;
|
||||||
export const evalScope = async (...args) => {
|
export const evalScope = async (...args) => {
|
||||||
|
if (scoped) {
|
||||||
|
console.warn('evalScope was called more than once.');
|
||||||
|
}
|
||||||
|
scoped = true;
|
||||||
const results = await Promise.allSettled(args);
|
const results = await Promise.allSettled(args);
|
||||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||||
results.forEach((result, i) => {
|
results.forEach((result, i) => {
|
||||||
@@ -14,14 +20,7 @@ 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);
|
Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap());
|
||||||
// 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 = {}) {
|
||||||
@@ -37,12 +36,11 @@ function safeEval(str, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const evaluate = async (code, transpiler) => {
|
export const evaluate = async (code, transpiler) => {
|
||||||
let meta = {};
|
if (!scoped) {
|
||||||
|
await evalScope(); // at least scope Pattern.prototype.boostrap
|
||||||
|
}
|
||||||
if (transpiler) {
|
if (transpiler) {
|
||||||
// transform syntactically correct js code to semantically usable code
|
code = transpiler(code); // transform syntactically correct js code to semantically usable code
|
||||||
const transpiled = transpiler(code);
|
|
||||||
code = transpiled.output;
|
|
||||||
meta = transpiled;
|
|
||||||
}
|
}
|
||||||
// if no transpiler is given, we expect a single instruction (!wrapExpression)
|
// if no transpiler is given, we expect a single instruction (!wrapExpression)
|
||||||
const options = { wrapExpression: !!transpiler };
|
const options = { wrapExpression: !!transpiler };
|
||||||
@@ -52,5 +50,5 @@ export const evaluate = async (code, transpiler) => {
|
|||||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||||
}
|
}
|
||||||
return { mode: 'javascript', pattern: evaluated, meta };
|
return { mode: 'javascript', pattern: evaluated };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="text"
|
id="text"
|
||||||
value="seq('a', ['b', 'c'])"
|
value="cat('a', 'b')"
|
||||||
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
|
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
/>
|
/>
|
||||||
<div id="output"></div>
|
<div id="output"></div>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8');
|
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.0.2');
|
||||||
Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
|
Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
|
||||||
const input = document.getElementById('text');
|
const input = document.getElementById('text');
|
||||||
const getEvents = () => {
|
const getEvents = () => {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="text"
|
id="text"
|
||||||
value="seq('tomato', 'indigo', ['white', 'steelblue']).fast(4)"
|
value="cat('orange', 'indigo')"
|
||||||
style="width: 100%; font-size: 2em; background: black; color: white; outline: none; position: absolute; top: 0"
|
style="width: 100%; font-size: 2em; background: black; color: white; outline: none; position: absolute; top: 0"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
/>
|
/>
|
||||||
<canvas id="canvas"></canvas>
|
<canvas id="canvas"></canvas>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8');
|
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.0.2');
|
||||||
// this adds all strudel functions to the global scope, to be used by eval
|
// this adds all strudel functions to the global scope, to be used by eval
|
||||||
Object.assign(window, strudel);
|
Object.assign(window, strudel);
|
||||||
// setup elements
|
// setup elements
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<div style="position: absolute; bottom: 0; right: 0; padding: 4px; width: 100vw; display: flex">
|
||||||
|
<button id="start" style="font-size: 2em">start</button>
|
||||||
|
<button id="stop" style="font-size: 2em">stop</button>
|
||||||
|
<button id="slower" style="font-size: 2em">slower</button>
|
||||||
|
<button id="faster" style="font-size: 2em">faster</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
style="font-size: 2em; background: #052b49; color: #fff; height: 100%; width: 100%; outline: none; border: 0"
|
||||||
|
id="text"
|
||||||
|
spellcheck="false"
|
||||||
|
>
|
||||||
|
Loading...</textarea
|
||||||
|
>
|
||||||
|
<script type="module">
|
||||||
|
document.body.style = 'margin: 0';
|
||||||
|
import * as strudel from '@strudel.cycles/core';
|
||||||
|
|
||||||
|
const { cat, State, TimeSpan, Scheduler, getPlayableNoteValue, getFreq } = strudel;
|
||||||
|
Object.assign(window, strudel); // add strudel to eval scope
|
||||||
|
|
||||||
|
const ctx = new AudioContext();
|
||||||
|
const scheduler = new Scheduler({
|
||||||
|
// audioContext: getAudioContext(),
|
||||||
|
interval: 0.1,
|
||||||
|
onTrigger: (hap, time, duration) => {
|
||||||
|
const freq = getFrequency(hap);
|
||||||
|
const osc = ctx.createOscillator();
|
||||||
|
const gain = 0.2;
|
||||||
|
osc.frequency.value = freq;
|
||||||
|
osc.type = 'triangle';
|
||||||
|
const onset = ctx.currentTime + time;
|
||||||
|
const offset = onset + duration;
|
||||||
|
const attack = 0.05;
|
||||||
|
const release = 0.05;
|
||||||
|
osc.start(onset);
|
||||||
|
osc.stop(offset + release);
|
||||||
|
|
||||||
|
const g = ctx.createGain();
|
||||||
|
g.gain.setValueAtTime(gain, onset);
|
||||||
|
g.gain.linearRampToValueAtTime(gain, onset + attack);
|
||||||
|
g.gain.setValueAtTime(gain, offset - release);
|
||||||
|
g.gain.linearRampToValueAtTime(0, offset);
|
||||||
|
osc.connect(g);
|
||||||
|
g.connect(ctx.destination);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let initialCode = `stack('c4','e4',cat('g4','a4','b4','a4'))
|
||||||
|
.add(cat(0,1,2,3,4,3,2,1).slow(8))
|
||||||
|
.fast(2)
|
||||||
|
.cps(tri.range(1,8).slow(32))`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const base64 = decodeURIComponent(window.location.href.split('#')[1]);
|
||||||
|
initialCode = atob(base64);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('failed to decode', err);
|
||||||
|
}
|
||||||
|
const input = document.getElementById('text');
|
||||||
|
input.value = initialCode;
|
||||||
|
const evaluate = () => {
|
||||||
|
try {
|
||||||
|
const pattern = eval(input.value);
|
||||||
|
scheduler.setPattern(pattern);
|
||||||
|
window.location.hash = '#' + encodeURIComponent(btoa(input.value)); // update url hash
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
evaluate();
|
||||||
|
input.addEventListener('input', () => evaluate());
|
||||||
|
document.getElementById('start').addEventListener('click', async () => {
|
||||||
|
await ctx.resume();
|
||||||
|
scheduler.start();
|
||||||
|
});
|
||||||
|
document.getElementById('stop').addEventListener('click', () => scheduler.stop());
|
||||||
|
document.getElementById('slower').addEventListener('click', () => scheduler.setCps(scheduler.cps - 0.1));
|
||||||
|
document.getElementById('faster').addEventListener('click', () => scheduler.setCps(scheduler.cps + 0.1));
|
||||||
|
</script>
|
||||||
|
<!--
|
||||||
|
sequence(1,2).mul(55/2) // frequencies
|
||||||
|
.mul(slowcat(1,2))
|
||||||
|
.mul(slowcat(1,3/2,4/3,5/3).slow(8))
|
||||||
|
.fast(3)
|
||||||
|
.freq()
|
||||||
|
.velocity(.5)
|
||||||
|
.s('sawtooth')
|
||||||
|
.cutoff(800)
|
||||||
|
.out()
|
||||||
|
-->
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="text"
|
||||||
|
value="seq('c3','eb3','g3').note().s('sawtooth').out()"
|
||||||
|
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
|
||||||
|
spellcheck="false"
|
||||||
|
/>
|
||||||
|
<button id="start">play</button>
|
||||||
|
<div id="output"></div>
|
||||||
|
<script type="module">
|
||||||
|
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@latest');
|
||||||
|
|
||||||
|
const controls = await import('https://cdn.skypack.dev/@strudel.cycles/core@latest/controls.mjs');
|
||||||
|
const { getAudioContext, Scheduler } = await import('https://cdn.skypack.dev/@strudel.cycles/webaudio@latest');
|
||||||
|
let scheduler;
|
||||||
|
|
||||||
|
const audioContext = getAudioContext();
|
||||||
|
const latency = 0.2;
|
||||||
|
|
||||||
|
Object.assign(window, strudel);
|
||||||
|
Object.assign(window, controls.default);
|
||||||
|
scheduler = new Scheduler({
|
||||||
|
audioContext,
|
||||||
|
interval: 0.1,
|
||||||
|
latency,
|
||||||
|
onEvent: (hap) => {
|
||||||
|
if (!hap.context.onTrigger) {
|
||||||
|
console.warn('no output chosen. use one of .out() .webdirt() .osc()');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let started;
|
||||||
|
document.getElementById('start').addEventListener('click', async () => {
|
||||||
|
const code = document.getElementById('text').value;
|
||||||
|
const pattern = eval(code);
|
||||||
|
const events = pattern._firstCycleValues;
|
||||||
|
console.log(code, '->', events);
|
||||||
|
scheduler.setPattern(pattern);
|
||||||
|
if (!started) {
|
||||||
|
scheduler.start();
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -6,36 +6,46 @@
|
|||||||
<title>Buildless Vanilla Strudel REPL</title>
|
<title>Buildless Vanilla Strudel REPL</title>
|
||||||
</head>
|
</head>
|
||||||
<body style="margin: 0; background: #222">
|
<body style="margin: 0; background: #222">
|
||||||
<div style="display: grid; height: 100vh; grid-template-rows: 32px auto">
|
<div style="display: grid; height: 100vh">
|
||||||
<button id="start" style="width: 100vw; height: 32px">evaluate</button>
|
|
||||||
<textarea
|
<textarea
|
||||||
id="text"
|
id="text"
|
||||||
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
|
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</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>
|
<div id="output"></div>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.6.8';
|
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.3.2';
|
||||||
import { mini } from 'https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0';
|
import { mini } from 'https://cdn.skypack.dev/@strudel.cycles/mini@0.3.2';
|
||||||
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.6.0';
|
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.3.2';
|
||||||
import {
|
import { getAudioContext, webaudioOutput } from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.3.3';
|
||||||
getAudioContext,
|
|
||||||
webaudioOutput,
|
|
||||||
initAudioOnFirstClick,
|
|
||||||
} from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0';
|
|
||||||
|
|
||||||
initAudioOnFirstClick();
|
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
const input = document.getElementById('text');
|
const input = document.getElementById('text');
|
||||||
input.innerHTML = getTune();
|
input.innerHTML = getTune();
|
||||||
|
|
||||||
evalScope(
|
evalScope(
|
||||||
controls,
|
controls,
|
||||||
import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8'),
|
import('https://cdn.skypack.dev/@strudel.cycles/core@0.3.2'),
|
||||||
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0'),
|
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.3.2'),
|
||||||
import('https://cdn.skypack.dev/@strudel.cycles/tonal@0.6.0'),
|
import('https://cdn.skypack.dev/@strudel.cycles/tonal@0.3.3'),
|
||||||
import('https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0'),
|
import('https://cdn.skypack.dev/@strudel.cycles/webaudio@0.3.3'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { evaluate } = repl({
|
const { evaluate } = repl({
|
||||||
@@ -43,7 +53,10 @@
|
|||||||
getTime: () => ctx.currentTime,
|
getTime: () => ctx.currentTime,
|
||||||
transpiler,
|
transpiler,
|
||||||
});
|
});
|
||||||
document.getElementById('start').addEventListener('click', () => evaluate(input.value));
|
document.getElementById('start').addEventListener('click', () => {
|
||||||
|
ctx.resume();
|
||||||
|
evaluate(input.value);
|
||||||
|
});
|
||||||
|
|
||||||
function getTune() {
|
function getTune() {
|
||||||
return `await samples('github:tidalcycles/Dirt-Samples/master')
|
return `await samples('github:tidalcycles/Dirt-Samples/master')
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# vite-vanilla-repl-cm6
|
|
||||||
|
|
||||||
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm i
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Vite Vanilla Strudel REPL</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<nav>
|
|
||||||
<button id="play">eval</button>
|
|
||||||
<button id="stop">stop</button>
|
|
||||||
</nav>
|
|
||||||
<div class="container">
|
|
||||||
<div id="editor"></div>
|
|
||||||
<div id="output"></div>
|
|
||||||
</div>
|
|
||||||
<canvas id="roll"></canvas>
|
|
||||||
</main>
|
|
||||||
<script type="module" src="./main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { StrudelMirror } from '@strudel/codemirror';
|
|
||||||
import { funk42 } from './tunes';
|
|
||||||
import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
|
|
||||||
import './style.css';
|
|
||||||
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
|
||||||
import { transpiler } from '@strudel.cycles/transpiler';
|
|
||||||
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
|
|
||||||
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
|
|
||||||
|
|
||||||
// init canvas
|
|
||||||
const canvas = document.getElementById('roll');
|
|
||||||
canvas.width = canvas.width * 2;
|
|
||||||
canvas.height = canvas.height * 2;
|
|
||||||
const drawContext = canvas.getContext('2d');
|
|
||||||
const drawTime = [-2, 2]; // time window of drawn haps
|
|
||||||
|
|
||||||
const editor = new StrudelMirror({
|
|
||||||
defaultOutput: webaudioOutput,
|
|
||||||
getTime: () => getAudioContext().currentTime,
|
|
||||||
transpiler,
|
|
||||||
root: document.getElementById('editor'),
|
|
||||||
initialCode: funk42,
|
|
||||||
drawTime,
|
|
||||||
onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }),
|
|
||||||
prebake: async () => {
|
|
||||||
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
|
|
||||||
const loadModules = evalScope(
|
|
||||||
controls,
|
|
||||||
import('@strudel.cycles/core'),
|
|
||||||
import('@strudel.cycles/mini'),
|
|
||||||
import('@strudel.cycles/tonal'),
|
|
||||||
import('@strudel.cycles/webaudio'),
|
|
||||||
);
|
|
||||||
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('play').addEventListener('click', () => editor.evaluate());
|
|
||||||
document.getElementById('stop').addEventListener('click', () => editor.stop());
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "vite-vanilla-repl-cm6",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"vite": "^4.3.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@strudel/codemirror": "workspace:*",
|
|
||||||
"@strudel.cycles/core": "workspace:*",
|
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
|
||||||
"@strudel.cycles/soundfonts": "workspace:*",
|
|
||||||
"@strudel.cycles/tonal": "workspace:*",
|
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
|
||||||
"@strudel.cycles/webaudio": "workspace:*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
body,
|
|
||||||
html {
|
|
||||||
margin: 0;
|
|
||||||
height: 100%;
|
|
||||||
background: #282c34;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
flex-grow: 1;
|
|
||||||
max-height: 100%;
|
|
||||||
position: relative;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#editor {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-editor {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#roll {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
|
|
||||||
await samples('github:felixroos/samples/main')
|
|
||||||
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
|
||||||
|
|
||||||
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
|
|
||||||
// make it 22 edo
|
|
||||||
.fmap(v => Math.pow(2,v/22))
|
|
||||||
// mess with the base frequency
|
|
||||||
.mul("<300 [300@3 200]>/8").freq()
|
|
||||||
.layer(
|
|
||||||
// chords
|
|
||||||
x=>x.div(freq(2)).s("flute").euclidLegato("<3 2>",8)
|
|
||||||
.shape(.4).lpf(sine.range(800,4000).slow(8)),
|
|
||||||
// adlibs
|
|
||||||
x=>x.arp("{0 3 2 [1 3]}%1.5")
|
|
||||||
.s('xylo').mul(freq(2))
|
|
||||||
.delay(.5).delayfeedback(.4).juxBy(.5, rev)
|
|
||||||
.hpf(sine.range(200,3000).slow(8)),
|
|
||||||
// bass
|
|
||||||
x=>x.arp("[0 [2 1?]](5,8)").s('sawtooth').div(freq(4))
|
|
||||||
.lpf(sine.range(400,2000).slow(8)).lpq(8).shape(.4)
|
|
||||||
.off(1/8, x=>x.mul(freq(2)).degradeBy(.5)).gain(.3)
|
|
||||||
).clip(1).release(.2)
|
|
||||||
.stack(
|
|
||||||
// drums
|
|
||||||
s("bd sd:<2 1>, [~ hh]*2, [~ rim]").bank('RolandTR909')
|
|
||||||
.off(1/8, x=>x.speed(2).gain(.4)).sometimes(ply(2)).gain(.8)
|
|
||||||
.mask("<0@4 1@12>/4")
|
|
||||||
.reset("<x@15 [x(3,8) x*[4 8]]>")
|
|
||||||
// wait for it...
|
|
||||||
).fast(2/3)
|
|
||||||
//.crush(6) // remove "//" if you dare`;
|
|
||||||
|
|
||||||
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
|
|
||||||
|
|
||||||
await samples('github:felixroos/samples/main')
|
|
||||||
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
|
||||||
|
|
||||||
addVoicings('hip', {
|
|
||||||
m11: ['2M 3m 4P 7m'],
|
|
||||||
'^7#11': ['3M 4A 5P 7M'],
|
|
||||||
}, ['C4', 'C6'])
|
|
||||||
|
|
||||||
stack(
|
|
||||||
stack(
|
|
||||||
"<Bbm11 A^7#11>/2".voicings('hip').note()
|
|
||||||
.s("gm_epiano1:2")
|
|
||||||
.arp("[<[0 1 2 3] [3 2 1 0]> ~@5]/2")
|
|
||||||
.release(2).late(.25).lpf(2000),
|
|
||||||
"<Bb1 A1>/2".note().s('gm_acoustic_bass'),
|
|
||||||
n("<0 2 3>(3,8)".off(1/8, add(4)))
|
|
||||||
.scale("<Bb4:minor A4:lydian>/2")
|
|
||||||
.s('gm_electric_guitar_jazz')
|
|
||||||
.decay(sine.range(.05, .2).slow(32)).sustain(0)
|
|
||||||
.delay(.5).lpf(sine.range(100,5000).slow(64))
|
|
||||||
.gain(.7).room(.5).pan(sine.range(0,1).slow(11))
|
|
||||||
).add(perlin.range(0,.25).note()),
|
|
||||||
stack(
|
|
||||||
s("bd:1(3,8) rim").bank('RolandTR707').slow(2).room("<0 <.1 .6>>")
|
|
||||||
.when("<0@7 1>",x=>x.echoWith(3, .0625, (x,i) => x.speed(1+i*.24))),
|
|
||||||
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.late("[0 .05]*2").late(12)
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const funk42 = `// froos - how to funk in 42 lines of code
|
|
||||||
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
|
|
||||||
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
|
|
||||||
|
|
||||||
await samples('github:felixroos/samples/main')
|
|
||||||
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
|
||||||
|
|
||||||
setcps(.5)
|
|
||||||
|
|
||||||
let drums = stack(
|
|
||||||
s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"),
|
|
||||||
s("hh*4").begin(.2).release(.02).end(.25).release(.02)
|
|
||||||
.gain(.3).bank('RolandTR707').late(.02).room(.5),
|
|
||||||
//s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02)
|
|
||||||
).fast(2)
|
|
||||||
|
|
||||||
let wurli = note(\`<
|
|
||||||
[[a2,g3,[b3 c4],e4] ~ [g3,c4,e4](3,8)@4 ~@2]!3
|
|
||||||
[[e2,e3,a3,b3,e4]@3 [e2,e3,ab3,b3,e4]@5]>\`)
|
|
||||||
.s("gm_epiano1:5").decay(.2).sustain("<[1 0@7]!3 1>")
|
|
||||||
.gain("<[.8@2 .4@14]!3 .7>").room(.3)
|
|
||||||
|
|
||||||
let organ = note("<[~@3 [a3,d4,f#4]@2 [[a3,c4,e4]@2 ~] ~@2]!3 ~>".add(12))
|
|
||||||
.s("gm_percussive_organ:2").gain(.6).lpf(1800).pan(.2).room(.3);
|
|
||||||
|
|
||||||
let clav = note(\`<
|
|
||||||
[~@3 a2 [g3,[b3 c4],e4]@2 ~ a2 [g3,b3,e4] ~@2 [g3,c4,e4] ~@4]!3
|
|
||||||
[~@3 e3 [[a3 b3],c3,e3]@2 ~ e2 [e3,a3]@3 [b3,e3] ~@2 [b3,e3]@2]>\`)
|
|
||||||
.s("gm_clavinet:1").decay("<.25!3 [.25 .4]>").sustain(0)
|
|
||||||
.gain(.7).pan(.8).room(.2);
|
|
||||||
|
|
||||||
let bass = note(\`<
|
|
||||||
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
|
|
||||||
[a1 [~ [g2 a2]] [e3 d3] [c3 [g3 a3]]]
|
|
||||||
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
|
|
||||||
[e2@6 e1@5 e1 [[d2 e3] g1]@4]
|
|
||||||
>\`).s("gm_electric_bass_pick:1").release(.1)
|
|
||||||
|
|
||||||
stack(
|
|
||||||
drums
|
|
||||||
,wurli
|
|
||||||
,organ
|
|
||||||
,clav
|
|
||||||
,bass
|
|
||||||
)`;
|
|
||||||
@@ -6,3 +6,5 @@ This folder demonstrates how to set up a strudel repl using vite and vanilla JS.
|
|||||||
npm i
|
npm i
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
or view it [live on githack](https://rawcdn.githack.com/tidalcycles/strudel/5fb36acb046ead7cd6ad3cd10f532e7f585f536a/packages/core/examples/vite-vanilla-repl/dist/index.html)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { controls, repl, evalScope } from '@strudel.cycles/core';
|
import { controls, repl, evalScope, setStringParser } from '@strudel.cycles/core';
|
||||||
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
import { mini } from '@strudel.cycles/mini';
|
||||||
import { transpiler } from '@strudel.cycles/transpiler';
|
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||||
|
// import { transpiler } from '@strudel.cycles/transpiler';
|
||||||
import tune from './tune.mjs';
|
import tune from './tune.mjs';
|
||||||
|
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
const input = document.getElementById('text');
|
const input = document.getElementById('text');
|
||||||
input.innerHTML = tune;
|
input.innerHTML = tune;
|
||||||
initAudioOnFirstClick();
|
|
||||||
|
|
||||||
evalScope(
|
evalScope(
|
||||||
controls,
|
controls,
|
||||||
@@ -16,13 +16,14 @@ evalScope(
|
|||||||
import('@strudel.cycles/tonal'),
|
import('@strudel.cycles/tonal'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setStringParser(mini)
|
||||||
|
|
||||||
const { evaluate } = repl({
|
const { evaluate } = repl({
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: webaudioOutput,
|
||||||
getTime: () => ctx.currentTime,
|
getTime: () => ctx.currentTime,
|
||||||
transpiler,
|
// transpiler,
|
||||||
});
|
});
|
||||||
document.getElementById('start').addEventListener('click', () => {
|
document.getElementById('start').addEventListener('click', () => {
|
||||||
ctx.resume();
|
ctx.resume();
|
||||||
console.log('eval', input.value);
|
|
||||||
evaluate(input.value);
|
evaluate(input.value);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,885 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-vanilla-repl",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "vite-vanilla-repl",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-RY2fVI8O0iFUNvZirXaQ1vMvK0xhCcl0gqRj74Z6yEiO1zAUa7hbsdwZM1kzqbxHK7LFyMizipfXT3JME+12Hw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-+BoyIm4I8uJmH/QDIH0fu7MG0AEx9OXEDXnqptXCwKOlOqZiS4iraH1Nr7/ObLMokW3sOCeBNyD68ATcV9b9Ag==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-Cu3SC84oyzzhrK/YyN4iEVy2jZu5t2fz66HEOShHURcjSkOSAVL8C/gfUT+lDJxkVHpg8GZ10DD0rMHRPqMFaQ==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/android-arm": "0.15.13",
|
||||||
|
"@esbuild/linux-loong64": "0.15.13",
|
||||||
|
"esbuild-android-64": "0.15.13",
|
||||||
|
"esbuild-android-arm64": "0.15.13",
|
||||||
|
"esbuild-darwin-64": "0.15.13",
|
||||||
|
"esbuild-darwin-arm64": "0.15.13",
|
||||||
|
"esbuild-freebsd-64": "0.15.13",
|
||||||
|
"esbuild-freebsd-arm64": "0.15.13",
|
||||||
|
"esbuild-linux-32": "0.15.13",
|
||||||
|
"esbuild-linux-64": "0.15.13",
|
||||||
|
"esbuild-linux-arm": "0.15.13",
|
||||||
|
"esbuild-linux-arm64": "0.15.13",
|
||||||
|
"esbuild-linux-mips64le": "0.15.13",
|
||||||
|
"esbuild-linux-ppc64le": "0.15.13",
|
||||||
|
"esbuild-linux-riscv64": "0.15.13",
|
||||||
|
"esbuild-linux-s390x": "0.15.13",
|
||||||
|
"esbuild-netbsd-64": "0.15.13",
|
||||||
|
"esbuild-openbsd-64": "0.15.13",
|
||||||
|
"esbuild-sunos-64": "0.15.13",
|
||||||
|
"esbuild-windows-32": "0.15.13",
|
||||||
|
"esbuild-windows-64": "0.15.13",
|
||||||
|
"esbuild-windows-arm64": "0.15.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-android-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-yRorukXBlokwTip+Sy4MYskLhJsO0Kn0/Fj43s1krVblfwP+hMD37a4Wmg139GEsMLl+vh8WXp2mq/cTA9J97g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-android-arm64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-TKzyymLD6PiVeyYa4c5wdPw87BeAiTXNtK6amWUcXZxkV51gOk5u5qzmDaYSwiWeecSNHamFsaFjLoi32QR5/w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-darwin-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-WAx7c2DaOS6CrRcoYCgXgkXDliLnFv3pQLV6GeW1YcGEZq2Gnl8s9Pg7ahValZkpOa0iE/ojRVQ87sbUhF1Cbg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-darwin-arm64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-U6jFsPfSSxC3V1CLiQqwvDuj3GGrtQNB3P3nNC3+q99EKf94UGpsG9l4CQ83zBs1NHrk1rtCSYT0+KfK5LsD8A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-freebsd-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-whItJgDiOXaDG/idy75qqevIpZjnReZkMGCgQaBWZuKHoElDJC1rh7MpoUgupMcdfOd+PgdEwNQW9DAE6i8wyA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-freebsd-arm64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-6pCSWt8mLUbPtygv7cufV0sZLeylaMwS5Fznj6Rsx9G2AJJsAjQ9ifA+0rQEIg7DwJmi9it+WjzNTEAzzdoM3Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-linux-32": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-VbZdWOEdrJiYApm2kkxoTOgsoCO1krBZ3quHdYk3g3ivWaMwNIVPIfEE0f0XQQ0u5pJtBsnk2/7OPiCFIPOe/w==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-linux-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-rXmnArVNio6yANSqDQlIO4WiP+Cv7+9EuAHNnag7rByAqFVuRusLbGi2697A5dFPNXoO//IiogVwi3AdcfPC6A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-linux-arm": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-Ac6LpfmJO8WhCMQmO253xX2IU2B3wPDbl4IvR0hnqcPrdfCaUa2j/lLMGTjmQ4W5JsJIdHEdW12dG8lFS0MbxQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-linux-arm64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-alEMGU4Z+d17U7KQQw2IV8tQycO6T+rOrgW8OS22Ua25x6kHxoG6Ngry6Aq6uranC+pNWNMB6aHFPh7aTQdORQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-linux-mips64le": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-47PgmyYEu+yN5rD/MbwS6DxP2FSGPo4Uxg5LwIdxTiyGC2XKwHhHyW7YYEDlSuXLQXEdTO7mYe8zQ74czP7W8A==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-linux-ppc64le": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-z6n28h2+PC1Ayle9DjKoBRcx/4cxHoOa2e689e2aDJSaKug3jXcQw7mM+GLg+9ydYoNzj8QxNL8ihOv/OnezhA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-linux-riscv64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-+Lu4zuuXuQhgLUGyZloWCqTslcCAjMZH1k3Xc9MSEJEpEFdpsSU0sRDXAnk18FKOfEjhu4YMGaykx9xjtpA6ow==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-linux-s390x": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-BMeXRljruf7J0TMxD5CIXS65y7puiZkAh+s4XFV9qy16SxOuMhxhVIXYLnbdfLrsYGFzx7U9mcdpFWkkvy/Uag==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-netbsd-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-EHj9QZOTel581JPj7UO3xYbltFTYnHy+SIqJVq6yd3KkCrsHRbapiPb0Lx3EOOtybBEE9EyqbmfW1NlSDsSzvQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-openbsd-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-nkuDlIjF/sfUhfx8SKq0+U+Fgx5K9JcPq1mUodnxI0x4kBdCv46rOGWbuJ6eof2n3wdoCLccOoJAbg9ba/bT2w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-sunos-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-jVeu2GfxZQ++6lRdY43CS0Tm/r4WuQQ0Pdsrxbw+aOrHQPHV0+LNOLnvbN28M7BSUGnJnHkHm2HozGgNGyeIRw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-windows-32": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-XoF2iBf0wnqo16SDq+aDGi/+QbaLFpkiRarPVssMh9KYbFNCqPLlGAWwDvxEVz+ywX6Si37J2AKm+AXq1kC0JA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-windows-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-Et6htEfGycjDrtqb2ng6nT+baesZPYQIW+HUEHK4D1ncggNrDNk3yoboYQ5KtiVrw/JaDMNttz8rrPubV/fvPQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild-windows-arm64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-3bv7tqntThQC9SWLRouMDmZnlOukBhOCTlkzNqzGCmrkCJI7io5LLjwJBOVY6kOUlIvdxbooNZwjtBvj+7uuVg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/has": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-core-module": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"has": "^1.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||||
|
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-parse": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/picocolors": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/postcss": {
|
||||||
|
"version": "8.4.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
|
||||||
|
"integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.4",
|
||||||
|
"picocolors": "^1.0.0",
|
||||||
|
"source-map-js": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resolve": {
|
||||||
|
"version": "1.22.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||||
|
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"is-core-module": "^2.9.0",
|
||||||
|
"path-parse": "^1.0.7",
|
||||||
|
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"resolve": "bin/resolve"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup": {
|
||||||
|
"version": "2.79.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
|
||||||
|
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"rollup": "dist/bin/rollup"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/supports-preserve-symlinks-flag": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "^0.15.9",
|
||||||
|
"postcss": "^8.4.18",
|
||||||
|
"resolve": "^1.22.1",
|
||||||
|
"rollup": "^2.79.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite": "bin/vite.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.18.0 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"less": "*",
|
||||||
|
"sass": "*",
|
||||||
|
"stylus": "*",
|
||||||
|
"sugarss": "*",
|
||||||
|
"terser": "^5.4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"less": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"stylus": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sugarss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"terser": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@esbuild/android-arm": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-RY2fVI8O0iFUNvZirXaQ1vMvK0xhCcl0gqRj74Z6yEiO1zAUa7hbsdwZM1kzqbxHK7LFyMizipfXT3JME+12Hw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/linux-loong64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-+BoyIm4I8uJmH/QDIH0fu7MG0AEx9OXEDXnqptXCwKOlOqZiS4iraH1Nr7/ObLMokW3sOCeBNyD68ATcV9b9Ag==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-Cu3SC84oyzzhrK/YyN4iEVy2jZu5t2fz66HEOShHURcjSkOSAVL8C/gfUT+lDJxkVHpg8GZ10DD0rMHRPqMFaQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@esbuild/android-arm": "0.15.13",
|
||||||
|
"@esbuild/linux-loong64": "0.15.13",
|
||||||
|
"esbuild-android-64": "0.15.13",
|
||||||
|
"esbuild-android-arm64": "0.15.13",
|
||||||
|
"esbuild-darwin-64": "0.15.13",
|
||||||
|
"esbuild-darwin-arm64": "0.15.13",
|
||||||
|
"esbuild-freebsd-64": "0.15.13",
|
||||||
|
"esbuild-freebsd-arm64": "0.15.13",
|
||||||
|
"esbuild-linux-32": "0.15.13",
|
||||||
|
"esbuild-linux-64": "0.15.13",
|
||||||
|
"esbuild-linux-arm": "0.15.13",
|
||||||
|
"esbuild-linux-arm64": "0.15.13",
|
||||||
|
"esbuild-linux-mips64le": "0.15.13",
|
||||||
|
"esbuild-linux-ppc64le": "0.15.13",
|
||||||
|
"esbuild-linux-riscv64": "0.15.13",
|
||||||
|
"esbuild-linux-s390x": "0.15.13",
|
||||||
|
"esbuild-netbsd-64": "0.15.13",
|
||||||
|
"esbuild-openbsd-64": "0.15.13",
|
||||||
|
"esbuild-sunos-64": "0.15.13",
|
||||||
|
"esbuild-windows-32": "0.15.13",
|
||||||
|
"esbuild-windows-64": "0.15.13",
|
||||||
|
"esbuild-windows-arm64": "0.15.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"esbuild-android-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-yRorukXBlokwTip+Sy4MYskLhJsO0Kn0/Fj43s1krVblfwP+hMD37a4Wmg139GEsMLl+vh8WXp2mq/cTA9J97g==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-android-arm64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-TKzyymLD6PiVeyYa4c5wdPw87BeAiTXNtK6amWUcXZxkV51gOk5u5qzmDaYSwiWeecSNHamFsaFjLoi32QR5/w==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-darwin-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-WAx7c2DaOS6CrRcoYCgXgkXDliLnFv3pQLV6GeW1YcGEZq2Gnl8s9Pg7ahValZkpOa0iE/ojRVQ87sbUhF1Cbg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-darwin-arm64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-U6jFsPfSSxC3V1CLiQqwvDuj3GGrtQNB3P3nNC3+q99EKf94UGpsG9l4CQ83zBs1NHrk1rtCSYT0+KfK5LsD8A==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-freebsd-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-whItJgDiOXaDG/idy75qqevIpZjnReZkMGCgQaBWZuKHoElDJC1rh7MpoUgupMcdfOd+PgdEwNQW9DAE6i8wyA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-freebsd-arm64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-6pCSWt8mLUbPtygv7cufV0sZLeylaMwS5Fznj6Rsx9G2AJJsAjQ9ifA+0rQEIg7DwJmi9it+WjzNTEAzzdoM3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-32": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-VbZdWOEdrJiYApm2kkxoTOgsoCO1krBZ3quHdYk3g3ivWaMwNIVPIfEE0f0XQQ0u5pJtBsnk2/7OPiCFIPOe/w==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-rXmnArVNio6yANSqDQlIO4WiP+Cv7+9EuAHNnag7rByAqFVuRusLbGi2697A5dFPNXoO//IiogVwi3AdcfPC6A==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-arm": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-Ac6LpfmJO8WhCMQmO253xX2IU2B3wPDbl4IvR0hnqcPrdfCaUa2j/lLMGTjmQ4W5JsJIdHEdW12dG8lFS0MbxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-arm64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-alEMGU4Z+d17U7KQQw2IV8tQycO6T+rOrgW8OS22Ua25x6kHxoG6Ngry6Aq6uranC+pNWNMB6aHFPh7aTQdORQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-mips64le": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-47PgmyYEu+yN5rD/MbwS6DxP2FSGPo4Uxg5LwIdxTiyGC2XKwHhHyW7YYEDlSuXLQXEdTO7mYe8zQ74czP7W8A==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-ppc64le": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-z6n28h2+PC1Ayle9DjKoBRcx/4cxHoOa2e689e2aDJSaKug3jXcQw7mM+GLg+9ydYoNzj8QxNL8ihOv/OnezhA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-riscv64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-+Lu4zuuXuQhgLUGyZloWCqTslcCAjMZH1k3Xc9MSEJEpEFdpsSU0sRDXAnk18FKOfEjhu4YMGaykx9xjtpA6ow==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-s390x": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-BMeXRljruf7J0TMxD5CIXS65y7puiZkAh+s4XFV9qy16SxOuMhxhVIXYLnbdfLrsYGFzx7U9mcdpFWkkvy/Uag==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-netbsd-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-EHj9QZOTel581JPj7UO3xYbltFTYnHy+SIqJVq6yd3KkCrsHRbapiPb0Lx3EOOtybBEE9EyqbmfW1NlSDsSzvQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-openbsd-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-nkuDlIjF/sfUhfx8SKq0+U+Fgx5K9JcPq1mUodnxI0x4kBdCv46rOGWbuJ6eof2n3wdoCLccOoJAbg9ba/bT2w==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-sunos-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-jVeu2GfxZQ++6lRdY43CS0Tm/r4WuQQ0Pdsrxbw+aOrHQPHV0+LNOLnvbN28M7BSUGnJnHkHm2HozGgNGyeIRw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-windows-32": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-XoF2iBf0wnqo16SDq+aDGi/+QbaLFpkiRarPVssMh9KYbFNCqPLlGAWwDvxEVz+ywX6Si37J2AKm+AXq1kC0JA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-windows-64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-Et6htEfGycjDrtqb2ng6nT+baesZPYQIW+HUEHK4D1ncggNrDNk3yoboYQ5KtiVrw/JaDMNttz8rrPubV/fvPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-windows-arm64": {
|
||||||
|
"version": "0.15.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.13.tgz",
|
||||||
|
"integrity": "sha512-3bv7tqntThQC9SWLRouMDmZnlOukBhOCTlkzNqzGCmrkCJI7io5LLjwJBOVY6kOUlIvdxbooNZwjtBvj+7uuVg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"function-bind": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"has": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"function-bind": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-core-module": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"has": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nanoid": {
|
||||||
|
"version": "3.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
|
||||||
|
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"path-parse": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"picocolors": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"postcss": {
|
||||||
|
"version": "8.4.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz",
|
||||||
|
"integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"nanoid": "^3.3.4",
|
||||||
|
"picocolors": "^1.0.0",
|
||||||
|
"source-map-js": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resolve": {
|
||||||
|
"version": "1.22.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||||
|
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-core-module": "^2.9.0",
|
||||||
|
"path-parse": "^1.0.7",
|
||||||
|
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rollup": {
|
||||||
|
"version": "2.79.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
|
||||||
|
"integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source-map-js": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"supports-preserve-symlinks-flag": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"esbuild": "^0.15.9",
|
||||||
|
"fsevents": "~2.3.2",
|
||||||
|
"postcss": "^8.4.18",
|
||||||
|
"resolve": "^1.22.1",
|
||||||
|
"rollup": "^2.79.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,13 +10,6 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.3.3"
|
"vite": "^3.2.0"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@strudel.cycles/core": "workspace:*",
|
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
|
||||||
"@strudel.cycles/tonal": "workspace:*"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default `await samples('github:tidalcycles/Dirt-Samples/master')
|
/* export default `await samples('github:tidalcycles/Dirt-Samples/master')
|
||||||
|
|
||||||
stack(
|
stack(
|
||||||
// amen
|
// amen
|
||||||
@@ -29,3 +29,11 @@ stack(
|
|||||||
,
|
,
|
||||||
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
|
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
|
||||||
).reset("<x@7 x(5,8)>")`;
|
).reset("<x@7 x(5,8)>")`;
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default `stack(
|
||||||
|
n("c3 [eb3,g3]")
|
||||||
|
.delay("<0 .5>")
|
||||||
|
.delaytime(".16 | .33")
|
||||||
|
.delayfeedback(".6 | .8")
|
||||||
|
).sometimes(x=>x.speed("-1"))`;
|
||||||
|
|||||||
@@ -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>
|
||||||
+5
-45
@@ -32,11 +32,7 @@ export class Hap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get duration() {
|
get duration() {
|
||||||
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
|
return this.whole.end.sub(this.whole.begin);
|
||||||
}
|
|
||||||
|
|
||||||
get endClipped() {
|
|
||||||
return this.whole.begin.add(this.duration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wholeOrPart() {
|
wholeOrPart() {
|
||||||
@@ -83,33 +79,10 @@ export class Hap {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
show(compact = false) {
|
show() {
|
||||||
const value =
|
return (
|
||||||
typeof this.value === 'object'
|
'(' + (this.whole == undefined ? '~' : this.whole.show()) + ', ' + this.part.show() + ', ' + this.value + ')'
|
||||||
? 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) {
|
||||||
@@ -130,19 +103,6 @@ export class Hap {
|
|||||||
setContext(context) {
|
setContext(context) {
|
||||||
return new Hap(this.whole, this.part, this.value, context);
|
return new Hap(this.whole, this.part, this.value, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureObjectValue() {
|
|
||||||
/* if (isNote(hap.value)) {
|
|
||||||
// supports primitive hap values that look like notes
|
|
||||||
hap.value = { note: hap.value };
|
|
||||||
} */
|
|
||||||
if (typeof this.value !== 'object') {
|
|
||||||
throw new Error(
|
|
||||||
`expected hap.value to be an object, but got "${this.value}". Hint: append .note() or .s() to the end`,
|
|
||||||
'error',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Hap;
|
export default Hap;
|
||||||
|
|||||||
+28
-22
@@ -4,30 +4,36 @@ 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 controls from './controls.mjs';
|
|
||||||
export * from './euclid.mjs';
|
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
|
|
||||||
|
import drawLine from './drawLine.mjs';
|
||||||
|
import gist from './gist.js';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
export { Fraction, controls };
|
// Fraction: Fraction, drawLine: drawline, gist: gist, logger: logger,
|
||||||
export * from './hap.mjs';
|
|
||||||
export * from './pattern.mjs';
|
import controls from './controls.mjs';
|
||||||
export * from './signal.mjs';
|
import pattern from './pattern.mjs';
|
||||||
export * from './state.mjs';
|
|
||||||
export * from './timespan.mjs';
|
import * as draw from './draw.mjs';
|
||||||
export * from './util.mjs';
|
import * as euclid from './euclid.mjs';
|
||||||
export * from './speak.mjs';
|
import * as evaluate from './evaluate.mjs';
|
||||||
export * from './evaluate.mjs';
|
import * as hap from './hap.mjs';
|
||||||
export * from './repl.mjs';
|
import * as repl from './repl.mjs';
|
||||||
export * from './cyclist.mjs';
|
import * as pianoroll from './pianoroll.mjs';
|
||||||
export * from './logger.mjs';
|
import * as signal from './signal.mjs';
|
||||||
export * from './time.mjs';
|
import * as speak from './speak.mjs';
|
||||||
export * from './draw.mjs';
|
import * as state from './state.mjs';
|
||||||
export * from './animate.mjs';
|
import * as time from './time.mjs';
|
||||||
export * from './pianoroll.mjs';
|
import * as timespan from './timespan.mjs';
|
||||||
export * from './spiral.mjs';
|
import * as ui from './ui.mjs';
|
||||||
export * from './ui.mjs';
|
import * as util from './util.mjs';
|
||||||
export { default as drawLine } from './drawLine.mjs';
|
|
||||||
export { default as gist } from './gist.js';
|
const core = { drawLine, gist, logger, Fraction, ...controls, ...pattern, ...draw, ...euclid,
|
||||||
|
...evaluate, ...hap, ...repl, ...pianoroll, ...signal, ...speak,
|
||||||
|
...state, ...time, ...timespan, ...ui, ...util
|
||||||
|
};
|
||||||
|
export default core;
|
||||||
|
|
||||||
// below won't work with runtime.mjs (json import fails)
|
// below won't work with runtime.mjs (json import fails)
|
||||||
/* import * as p from './package.json';
|
/* import * as p from './package.json';
|
||||||
export const version = p.version; */
|
export const version = p.version; */
|
||||||
|
|||||||
@@ -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 document !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
if (typeof CustomEvent !== 'undefined') {
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
new CustomEvent(logKey, {
|
new CustomEvent(logKey, {
|
||||||
detail: {
|
detail: {
|
||||||
|
|||||||
Generated
+1639
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/core",
|
"name": "@strudel.cycles/core",
|
||||||
"version": "0.9.0",
|
"version": "0.4.1",
|
||||||
"description": "Port of Tidal Cycles to JavaScript",
|
"description": "Port of Tidal Cycles to JavaScript",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"publishConfig": {
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"module": "dist/index.mjs"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest run",
|
"test": "vitest run"
|
||||||
"build": "vite build",
|
|
||||||
"prepublishOnly": "pnpm build"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -31,11 +25,8 @@
|
|||||||
},
|
},
|
||||||
"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"
|
||||||
"devDependencies": {
|
|
||||||
"vite": "^4.3.3",
|
|
||||||
"vitest": "^0.33.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1017
-1311
File diff suppressed because it is too large
Load Diff
+94
-187
@@ -1,66 +1,28 @@
|
|||||||
/*
|
/*
|
||||||
pianoroll.mjs - <short description TODO>
|
pianoroll.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pianoroll.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/pianoroll.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
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, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
import pattern from './pattern.mjs';
|
||||||
|
const { Pattern } = pattern;
|
||||||
|
|
||||||
|
import { toMidi } from './util.mjs';
|
||||||
|
import { getDrawContext } from './draw.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 } = e;
|
let value = typeof e.value === 'object' ? e.value.note ?? e.value.n : e.value;
|
||||||
if (typeof e.value !== 'object') {
|
if (typeof value === 'string') {
|
||||||
value = { value };
|
value = toMidi(value);
|
||||||
}
|
|
||||||
let { note, n, freq, s } = value;
|
|
||||||
if (freq) {
|
|
||||||
return freqToMidi(freq);
|
|
||||||
}
|
|
||||||
note = note ?? n;
|
|
||||||
if (typeof note === 'string') {
|
|
||||||
return noteToMidi(note);
|
|
||||||
}
|
|
||||||
if (typeof note === 'number') {
|
|
||||||
return note;
|
|
||||||
}
|
|
||||||
if (s) {
|
|
||||||
return '_' + s;
|
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.pianoroll = function (options = {}) {
|
Pattern.prototype.pianoroll = function ({
|
||||||
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
|
|
||||||
|
|
||||||
let from = -cycles * playhead;
|
|
||||||
let to = cycles * (1 - playhead);
|
|
||||||
|
|
||||||
this.draw(
|
|
||||||
(ctx, haps, t) => {
|
|
||||||
const inFrame = (event) =>
|
|
||||||
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
|
|
||||||
pianoroll({
|
|
||||||
...options,
|
|
||||||
time: t,
|
|
||||||
ctx,
|
|
||||||
haps: haps.filter(inFrame),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: from - overscan,
|
|
||||||
to: to + overscan,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
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,
|
cycles = 4,
|
||||||
playhead = 0.5,
|
playhead = 0.5,
|
||||||
|
overscan = 1,
|
||||||
flipTime = 0,
|
flipTime = 0,
|
||||||
flipValues = 0,
|
flipValues = 0,
|
||||||
hideNegative = false,
|
hideNegative = false,
|
||||||
@@ -78,16 +40,8 @@ export function pianoroll({
|
|||||||
timeframe: timeframeProp,
|
timeframe: timeframeProp,
|
||||||
fold = 0,
|
fold = 0,
|
||||||
vertical = 0,
|
vertical = 0,
|
||||||
labels = false,
|
|
||||||
fill = 1,
|
|
||||||
fillActive = false,
|
|
||||||
strokeActive = true,
|
|
||||||
stroke,
|
|
||||||
hideInactive = 0,
|
|
||||||
colorizeInactive = 1,
|
|
||||||
fontFamily,
|
|
||||||
ctx,
|
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
const ctx = getDrawContext();
|
||||||
const w = ctx.canvas.width;
|
const w = ctx.canvas.width;
|
||||||
const h = ctx.canvas.height;
|
const h = ctx.canvas.height;
|
||||||
let from = -cycles * playhead;
|
let from = -cycles * playhead;
|
||||||
@@ -98,6 +52,9 @@ export function pianoroll({
|
|||||||
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
|
||||||
@@ -109,136 +66,86 @@ export function pianoroll({
|
|||||||
flipTime && timeRange.reverse();
|
flipTime && timeRange.reverse();
|
||||||
flipValues && valueRange.reverse();
|
flipValues && valueRange.reverse();
|
||||||
|
|
||||||
// onQuery
|
this.draw(
|
||||||
const { min, max, values } = haps.reduce(
|
(ctx, events, t) => {
|
||||||
({ min, max, values }, e) => {
|
ctx.fillStyle = background;
|
||||||
const v = getValue(e);
|
ctx.globalAlpha = 1; // reset!
|
||||||
return {
|
if (!smear) {
|
||||||
min: v < min ? v : min,
|
ctx.clearRect(0, 0, w, h);
|
||||||
max: v > max ? v : max,
|
ctx.fillRect(0, 0, w, h);
|
||||||
values: values.includes(v) ? values : [...values, v],
|
}
|
||||||
};
|
const inFrame = (event) =>
|
||||||
|
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
|
||||||
|
events.filter(inFrame).forEach((event) => {
|
||||||
|
const isActive = event.whole.begin <= t && event.whole.end > t;
|
||||||
|
ctx.fillStyle = event.context?.color || inactive;
|
||||||
|
ctx.strokeStyle = event.context?.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(t / 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();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: from - overscan,
|
||||||
|
to: to + overscan,
|
||||||
|
onQuery: (events) => {
|
||||||
|
const { min, max, values } = events.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);
|
||||||
|
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ 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);
|
|
||||||
}
|
|
||||||
haps.forEach((event) => {
|
|
||||||
const isActive = event.whole.begin <= time && event.endClipped > time;
|
|
||||||
let strokeCurrent = stroke ?? (strokeActive && isActive);
|
|
||||||
let fillCurrent = (!isActive && fill) || (isActive && fillActive);
|
|
||||||
if (hideInactive && !isActive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let color = event.value?.color || event.context?.color;
|
|
||||||
active = color || active;
|
|
||||||
inactive = colorizeInactive ? color || inactive : inactive;
|
|
||||||
color = isActive ? active : inactive;
|
|
||||||
ctx.fillStyle = fillCurrent ? color : 'transparent';
|
|
||||||
ctx.strokeStyle = color;
|
|
||||||
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
|
|
||||||
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
|
|
||||||
const timePx = scale(timeProgress, ...timeRange);
|
|
||||||
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
|
||||||
const value = getValue(event);
|
|
||||||
const valueProgress = fold
|
|
||||||
? foldValues.indexOf(value) / foldValues.length
|
|
||||||
: (Number(value) - minMidi) / valueExtent;
|
|
||||||
const valuePx = scale(valueProgress, ...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
|
|
||||||
];
|
|
||||||
}
|
|
||||||
/* const xFactor = Math.sin(performance.now() / 500) + 1;
|
|
||||||
coords[0] *= xFactor; */
|
|
||||||
|
|
||||||
if (strokeCurrent) {
|
|
||||||
ctx.strokeRect(...coords);
|
|
||||||
}
|
|
||||||
if (fillCurrent) {
|
|
||||||
ctx.fillRect(...coords);
|
|
||||||
}
|
|
||||||
//ctx.ellipse(...ellipseFromRect(...coords))
|
|
||||||
if (labels) {
|
|
||||||
const defaultLabel = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
|
|
||||||
const { label: inactiveLabel, activeLabel } = event.value;
|
|
||||||
const customLabel = isActive ? activeLabel || inactiveLabel : inactiveLabel;
|
|
||||||
const label = customLabel ?? defaultLabel;
|
|
||||||
let measure = vertical ? durationPx : barThickness * 0.75;
|
|
||||||
ctx.font = `${measure}px ${fontFamily || 'monospace'}`;
|
|
||||||
// font color
|
|
||||||
ctx.fillStyle = /* isActive && */ !fillCurrent ? color : 'black';
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
ctx.fillText(label, ...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;
|
return this;
|
||||||
}
|
|
||||||
|
|
||||||
export function getDrawOptions(drawTime, options = {}) {
|
|
||||||
let [lookbehind, lookahead] = drawTime;
|
|
||||||
lookbehind = Math.abs(lookbehind);
|
|
||||||
const cycles = lookahead + lookbehind;
|
|
||||||
const playhead = lookbehind / cycles;
|
|
||||||
return { fold: 1, ...options, cycles, playhead };
|
|
||||||
}
|
|
||||||
|
|
||||||
Pattern.prototype.punchcard = function (options) {
|
|
||||||
return this.onPaint((ctx, time, haps, drawTime, paintOptions = {}) =>
|
|
||||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) }),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.wordfall = function (options) {
|
|
||||||
return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options });
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Pattern.prototype.pianoroll = function (options) {
|
|
||||||
return this.onPaint((ctx, time, haps, drawTime) =>
|
|
||||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),
|
|
||||||
);
|
|
||||||
}; */
|
|
||||||
|
|
||||||
export function drawPianoroll(options) {
|
|
||||||
const { drawTime, ...rest } = options;
|
|
||||||
pianoroll({ ...getDrawOptions(drawTime), ...rest });
|
|
||||||
}
|
|
||||||
|
|||||||
+20
-57
@@ -2,8 +2,6 @@ import { Cyclist } from './cyclist.mjs';
|
|||||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { setTime } from './time.mjs';
|
import { setTime } from './time.mjs';
|
||||||
import { evalScope } from './evaluate.mjs';
|
|
||||||
import { register } from './pattern.mjs';
|
|
||||||
|
|
||||||
export function repl({
|
export function repl({
|
||||||
interval,
|
interval,
|
||||||
@@ -15,30 +13,38 @@ export function repl({
|
|||||||
getTime,
|
getTime,
|
||||||
transpiler,
|
transpiler,
|
||||||
onToggle,
|
onToggle,
|
||||||
editPattern,
|
|
||||||
}) {
|
}) {
|
||||||
const scheduler = new Cyclist({
|
const scheduler = new Cyclist({
|
||||||
interval,
|
interval,
|
||||||
onTrigger: getTrigger({ defaultOutput, getTime }),
|
onTrigger: async (hap, deadline, duration) => {
|
||||||
|
try {
|
||||||
|
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||||
|
await defaultOutput(hap, deadline, duration);
|
||||||
|
}
|
||||||
|
if (hap.context.onTrigger) {
|
||||||
|
const cps = 1;
|
||||||
|
// call signature of output / onTrigger is different...
|
||||||
|
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
onError: onSchedulerError,
|
onError: onSchedulerError,
|
||||||
getTime,
|
getTime,
|
||||||
onToggle,
|
onToggle,
|
||||||
});
|
});
|
||||||
const setPattern = (pattern, autostart = true) => {
|
setTime(() => scheduler.getPhase()); // TODO: refactor?
|
||||||
pattern = editPattern?.(pattern) || pattern;
|
|
||||||
scheduler.setPattern(pattern, autostart);
|
|
||||||
};
|
|
||||||
setTime(() => scheduler.now()); // TODO: refactor?
|
|
||||||
const evaluate = async (code, autostart = true) => {
|
const evaluate = async (code, autostart = true) => {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error('no code to evaluate');
|
throw new Error('no code to evaluate');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await beforeEval?.({ code });
|
beforeEval?.({ code });
|
||||||
let { pattern, meta } = await _evaluate(code, transpiler);
|
const { pattern } = await _evaluate(code, transpiler);
|
||||||
logger(`[eval] code updated`);
|
logger(`[eval] code updated`);
|
||||||
setPattern(pattern, autostart);
|
scheduler.setPattern(pattern, autostart);
|
||||||
afterEval?.({ code, pattern, meta });
|
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}`);
|
||||||
@@ -49,48 +55,5 @@ export function repl({
|
|||||||
const stop = () => scheduler.stop();
|
const stop = () => scheduler.stop();
|
||||||
const start = () => scheduler.start();
|
const start = () => scheduler.start();
|
||||||
const pause = () => scheduler.pause();
|
const pause = () => scheduler.pause();
|
||||||
const setCps = (cps) => scheduler.setCps(cps);
|
return { scheduler, evaluate, start, stop, pause };
|
||||||
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
|
||||||
|
|
||||||
// the following functions use the cps value, which is why they are defined here..
|
|
||||||
const loopAt = register('loopAt', (cycles, pat) => {
|
|
||||||
return pat.loopAtCps(cycles, scheduler.cps);
|
|
||||||
});
|
|
||||||
|
|
||||||
const fit = register('fit', (pat) =>
|
|
||||||
pat.withHap((hap) =>
|
|
||||||
hap.withValue((v) => ({
|
|
||||||
...v,
|
|
||||||
speed: scheduler.cps / hap.whole.duration, // overwrite speed completely?
|
|
||||||
unit: 'c',
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
evalScope({
|
|
||||||
loopAt,
|
|
||||||
fit,
|
|
||||||
setCps,
|
|
||||||
setcps: setCps,
|
|
||||||
setCpm,
|
|
||||||
setcpm: setCpm,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTrigger =
|
|
||||||
({ getTime, defaultOutput }) =>
|
|
||||||
async (hap, deadline, duration, cps) => {
|
|
||||||
try {
|
|
||||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
|
||||||
await defaultOutput(hap, deadline, duration, cps);
|
|
||||||
}
|
|
||||||
if (hap.context.onTrigger) {
|
|
||||||
// call signature of output / onTrigger is different...
|
|
||||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger(`[cyclist] error: ${err.message}`, 'error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
+85
-58
@@ -5,7 +5,8 @@ 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, register } from './pattern.mjs';
|
import pattern from './pattern.mjs';
|
||||||
|
const { Pattern, fastcat, reify, silence, stack, isPattern } = pattern;
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
import { id } from './util.mjs';
|
import { id } from './util.mjs';
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ export const isaw2 = isaw.toBipolar();
|
|||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
|
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
|
||||||
* @example
|
* @example
|
||||||
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
||||||
*
|
*
|
||||||
@@ -114,14 +115,6 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* A discrete pattern of numbers from 0 to n-1
|
|
||||||
* @example
|
|
||||||
* run(4).scale('C4 major').note()
|
|
||||||
* // "0 1 2 3".scale('C4 major').note()
|
|
||||||
*/
|
|
||||||
export const run = (n) => saw.range(0, n).floor().segment(n);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A continuous pattern of random numbers, between 0 and 1.
|
* A continuous pattern of random numbers, between 0 and 1.
|
||||||
*
|
*
|
||||||
@@ -266,9 +259,9 @@ export const perlinWith = (pat) => {
|
|||||||
*/
|
*/
|
||||||
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
||||||
|
|
||||||
export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
|
Pattern.prototype._degradeByWith = function (withPat, x) {
|
||||||
pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
|
return this.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.
|
||||||
@@ -284,9 +277,9 @@ export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
|
|||||||
* @example
|
* @example
|
||||||
* s("[hh?0.2]*8")
|
* s("[hh?0.2]*8")
|
||||||
*/
|
*/
|
||||||
export const degradeBy = register('degradeBy', function (x, pat) {
|
Pattern.prototype._degradeBy = function (x) {
|
||||||
return pat._degradeByWith(rand, x);
|
return this._degradeByWith(rand, x);
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -300,7 +293,9 @@ export const degradeBy = register('degradeBy', function (x, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* s("[hh?]*8")
|
* s("[hh?]*8")
|
||||||
*/
|
*/
|
||||||
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
Pattern.prototype.degrade = function () {
|
||||||
|
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.
|
||||||
@@ -315,14 +310,26 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*8").undegradeBy(0.2)
|
* s("hh*8").undegradeBy(0.2)
|
||||||
*/
|
*/
|
||||||
export const undegradeBy = register('undegradeBy', function (x, pat) {
|
Pattern.prototype._undegradeBy = function (x) {
|
||||||
return pat._degradeByWith(
|
return this._degradeByWith(
|
||||||
rand.fmap((r) => 1 - r),
|
rand.fmap((r) => 1 - r),
|
||||||
x,
|
x,
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
Pattern.prototype.undegrade = function () {
|
||||||
|
return this._undegradeBy(0.5);
|
||||||
|
};
|
||||||
|
|
||||||
|
Pattern.prototype._sometimesBy = function (x, func) {
|
||||||
|
return stack(this._degradeBy(x), func(this._undegradeBy(1 - x)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}; */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -337,12 +344,24 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
|||||||
* @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) {
|
||||||
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
const pat = this;
|
||||||
return reify(patx)
|
return reify(patx)
|
||||||
.fmap((x) => stack(pat._degradeBy(x), func(pat._undegradeBy(1 - x))))
|
.fmap((x) => pat._sometimesBy(x, func))
|
||||||
.innerJoin();
|
.innerJoin();
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// why does this exist? it is identical to sometimesBy
|
||||||
|
Pattern.prototype._sometimesByPre = function (x, func) {
|
||||||
|
return stack(this._degradeBy(x), func(this).undegradeBy(1 - x));
|
||||||
|
};
|
||||||
|
|
||||||
|
Pattern.prototype.sometimesByPre = function (patx, func) {
|
||||||
|
const pat = this;
|
||||||
|
return reify(patx)
|
||||||
|
.fmap((x) => pat._sometimesByPre(x, func))
|
||||||
|
.innerJoin();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -355,9 +374,20 @@ export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*4").sometimes(x=>x.speed("0.5"))
|
* s("hh*4").sometimes(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
export const sometimes = register('sometimes', function (func, pat) {
|
Pattern.prototype.sometimes = function (func) {
|
||||||
return pat._sometimesBy(0.5, func);
|
return this._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)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -372,17 +402,12 @@ export const sometimes = register('sometimes', function (func, pat) {
|
|||||||
* @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) {
|
||||||
export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
|
const pat = this;
|
||||||
return reify(patx)
|
return reify(patx)
|
||||||
.fmap((x) =>
|
.fmap((x) => pat._someCyclesBy(x, func))
|
||||||
stack(
|
|
||||||
pat._degradeByWith(rand._segment(1), x),
|
|
||||||
func(pat._degradeByWith(rand.fmap((r) => 1 - r)._segment(1), 1 - x)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.innerJoin();
|
.innerJoin();
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -394,9 +419,9 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat)
|
|||||||
* @example
|
* @example
|
||||||
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
|
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
export const someCycles = register('someCycles', function (func, pat) {
|
Pattern.prototype.someCycles = function (func) {
|
||||||
return pat._someCyclesBy(0.5, func);
|
return this._someCyclesBy(0.5, func);
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -408,9 +433,9 @@ export const someCycles = register('someCycles', function (func, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*8").often(x=>x.speed("0.5"))
|
* s("hh*8").often(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
export const often = register('often', function (func, pat) {
|
Pattern.prototype.often = function (func) {
|
||||||
return pat.sometimesBy(0.75, func);
|
return this.sometimesBy(0.75, func);
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -422,9 +447,9 @@ export const often = register('often', function (func, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*8").rarely(x=>x.speed("0.5"))
|
* s("hh*8").rarely(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
export const rarely = register('rarely', function (func, pat) {
|
Pattern.prototype.rarely = function (func) {
|
||||||
return pat.sometimesBy(0.25, func);
|
return this.sometimesBy(0.25, func);
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -436,9 +461,9 @@ export const rarely = register('rarely', function (func, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*8").almostNever(x=>x.speed("0.5"))
|
* s("hh*8").almostNever(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
export const almostNever = register('almostNever', function (func, pat) {
|
Pattern.prototype.almostNever = function (func) {
|
||||||
return pat.sometimesBy(0.1, func);
|
return this.sometimesBy(0.1, func);
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -450,9 +475,9 @@ export const almostNever = register('almostNever', function (func, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*8").almostAlways(x=>x.speed("0.5"))
|
* s("hh*8").almostAlways(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
export const almostAlways = register('almostAlways', function (func, pat) {
|
Pattern.prototype.almostAlways = function (func) {
|
||||||
return pat.sometimesBy(0.9, func);
|
return this.sometimesBy(0.9, func);
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -464,9 +489,9 @@ export const almostAlways = register('almostAlways', function (func, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*8").never(x=>x.speed("0.5"))
|
* s("hh*8").never(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
export const never = register('never', function (_, pat) {
|
Pattern.prototype.never = function (func) {
|
||||||
return pat;
|
return this;
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -478,6 +503,8 @@ export const never = register('never', function (_, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* s("hh*8").always(x=>x.speed("0.5"))
|
* s("hh*8").always(x=>x.speed("0.5"))
|
||||||
*/
|
*/
|
||||||
export const always = register('always', function (func, pat) {
|
Pattern.prototype.always = function (func) {
|
||||||
return func(pat);
|
return func(this);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
Pattern.prototype.patternified.push('degradeBy', 'undegradeBy');
|
||||||
|
|||||||
+11
-6
@@ -4,7 +4,8 @@ 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 { register } from './index.mjs';
|
import pattern from './pattern.mjs';
|
||||||
|
const {Pattern, patternify2, reify} = pattern;
|
||||||
|
|
||||||
let synth;
|
let synth;
|
||||||
try {
|
try {
|
||||||
@@ -16,7 +17,7 @@ try {
|
|||||||
let allVoices = synth?.getVoices();
|
let allVoices = synth?.getVoices();
|
||||||
// console.log('voices', allVoices);
|
// console.log('voices', allVoices);
|
||||||
|
|
||||||
function triggerSpeech(words, lang, voice) {
|
function speak(words, lang, voice) {
|
||||||
synth.cancel();
|
synth.cancel();
|
||||||
const utterance = new SpeechSynthesisUtterance(words);
|
const utterance = new SpeechSynthesisUtterance(words);
|
||||||
utterance.lang = lang;
|
utterance.lang = lang;
|
||||||
@@ -31,8 +32,12 @@ function triggerSpeech(words, lang, voice) {
|
|||||||
speechSynthesis.speak(utterance);
|
speechSynthesis.speak(utterance);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const speak = register('speak', function (lang, voice, pat) {
|
Pattern.prototype._speak = function (lang, voice) {
|
||||||
return pat.onTrigger((_, hap) => {
|
return this.onTrigger((_, hap) => {
|
||||||
triggerSpeech(hap.value, lang, voice);
|
speak(hap.value, lang, voice);
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
Pattern.prototype.speak = function (lang, voice) {
|
||||||
|
return patternify2(Pattern.prototype._speak)(reify(lang), reify(voice), this);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
import { Pattern } from './index.mjs';
|
|
||||||
|
|
||||||
// polar coords -> xy
|
|
||||||
function fromPolar(angle, radius, cx, cy) {
|
|
||||||
const radians = ((angle - 90) * Math.PI) / 180;
|
|
||||||
return [cx + Math.cos(radians) * radius, cy + Math.sin(radians) * radius];
|
|
||||||
}
|
|
||||||
|
|
||||||
const xyOnSpiral = (angle, margin, cx, cy, rotate = 0) => fromPolar((angle + rotate) * 360, margin * angle, cx, cy); // TODO: logSpiral
|
|
||||||
|
|
||||||
// draw spiral / segment of spiral
|
|
||||||
function spiralSegment(options) {
|
|
||||||
let {
|
|
||||||
ctx,
|
|
||||||
from = 0,
|
|
||||||
to = 3,
|
|
||||||
margin = 50,
|
|
||||||
cx = 100,
|
|
||||||
cy = 100,
|
|
||||||
rotate = 0,
|
|
||||||
thickness = margin / 2,
|
|
||||||
color = '#0000ff30',
|
|
||||||
cap = 'round',
|
|
||||||
stretch = 1,
|
|
||||||
fromOpacity = 1,
|
|
||||||
toOpacity = 1,
|
|
||||||
} = options;
|
|
||||||
from *= stretch;
|
|
||||||
to *= stretch;
|
|
||||||
rotate *= stretch;
|
|
||||||
ctx.lineWidth = thickness;
|
|
||||||
ctx.lineCap = cap;
|
|
||||||
ctx.strokeStyle = color;
|
|
||||||
ctx.globalAlpha = fromOpacity;
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
let [sx, sy] = xyOnSpiral(from, margin, cx, cy, rotate);
|
|
||||||
ctx.moveTo(sx, sy);
|
|
||||||
|
|
||||||
const increment = 1 / 60;
|
|
||||||
let angle = from;
|
|
||||||
while (angle <= to) {
|
|
||||||
const [x, y] = xyOnSpiral(angle, margin, cx, cy, rotate);
|
|
||||||
//ctx.lineWidth = angle*thickness;
|
|
||||||
ctx.globalAlpha = ((angle - from) / (to - from)) * toOpacity;
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
angle += increment;
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
Pattern.prototype.spiral = function (options = {}) {
|
|
||||||
const {
|
|
||||||
stretch = 1,
|
|
||||||
size = 80,
|
|
||||||
thickness = size / 2,
|
|
||||||
cap = 'butt', // round butt squar,
|
|
||||||
inset = 3, // start angl,
|
|
||||||
playheadColor = '#ffffff90',
|
|
||||||
playheadLength = 0.02,
|
|
||||||
playheadThickness = thickness,
|
|
||||||
padding = 0,
|
|
||||||
steady = 1,
|
|
||||||
inactiveColor = '#ffffff20',
|
|
||||||
colorizeInactive = 0,
|
|
||||||
fade = true,
|
|
||||||
// logSpiral = true,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
function spiral({ ctx, time, haps, drawTime }) {
|
|
||||||
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
|
|
||||||
ctx.clearRect(0, 0, w * 2, h * 2);
|
|
||||||
const [cx, cy] = [w / 2, h / 2];
|
|
||||||
const settings = {
|
|
||||||
margin: size / stretch,
|
|
||||||
cx,
|
|
||||||
cy,
|
|
||||||
stretch,
|
|
||||||
cap,
|
|
||||||
thickness,
|
|
||||||
};
|
|
||||||
|
|
||||||
const playhead = {
|
|
||||||
...settings,
|
|
||||||
thickness: playheadThickness,
|
|
||||||
from: inset - playheadLength,
|
|
||||||
to: inset,
|
|
||||||
color: playheadColor,
|
|
||||||
};
|
|
||||||
|
|
||||||
const [min] = drawTime;
|
|
||||||
const rotate = steady * time;
|
|
||||||
haps.forEach((hap) => {
|
|
||||||
const isActive = hap.whole.begin <= time && hap.endClipped > time;
|
|
||||||
const from = hap.whole.begin - time + inset;
|
|
||||||
const to = hap.endClipped - time + inset - padding;
|
|
||||||
const { color } = hap.context;
|
|
||||||
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
|
|
||||||
spiralSegment({
|
|
||||||
ctx,
|
|
||||||
...settings,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
rotate,
|
|
||||||
color: colorizeInactive || isActive ? color : inactiveColor,
|
|
||||||
fromOpacity: opacity,
|
|
||||||
toOpacity: opacity,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
spiralSegment({
|
|
||||||
ctx,
|
|
||||||
...playhead,
|
|
||||||
rotate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
controls.test.mjs - <short description TODO>
|
|
||||||
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/controls.test.mjs>
|
|
||||||
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 controls from '../controls.mjs';
|
|
||||||
import { mini } from '../../mini/mini.mjs';
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('controls', () => {
|
|
||||||
it('should support controls', () => {
|
|
||||||
expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
|
|
||||||
});
|
|
||||||
it('should support compound controls', () => {
|
|
||||||
expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
|
|
||||||
expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
|
|
||||||
{ s: 'bd', n: 3 },
|
|
||||||
{ s: 'sd', n: 4, gain: 1.4 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
it('should support ignore extra elements in compound controls', () => {
|
|
||||||
expect(controls.s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
|
|
||||||
{ s: 'bd', n: 3, gain: 0.4 },
|
|
||||||
{ s: 'sd', n: 4, gain: 0.5 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -4,7 +4,8 @@ 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 { fastcat, stack, slowcat, silence, pure } from '../pattern.mjs';
|
import pattern from '../pattern.mjs';
|
||||||
|
const { fastcat, stack, slowcat, silence, pure } = pattern;
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import drawLine from '../drawLine.mjs';
|
import drawLine from '../drawLine.mjs';
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,13 @@ import Fraction from 'fraction.js';
|
|||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
import {
|
import core from '../index.mjs';
|
||||||
TimeSpan,
|
|
||||||
Hap,
|
const {
|
||||||
State,
|
|
||||||
Pattern,
|
Pattern,
|
||||||
pure,
|
pure,
|
||||||
stack,
|
stack,
|
||||||
fastcat,
|
fastcat,
|
||||||
firstOf,
|
|
||||||
slowcat,
|
slowcat,
|
||||||
cat,
|
cat,
|
||||||
sequence,
|
sequence,
|
||||||
@@ -30,6 +28,12 @@ import {
|
|||||||
sub,
|
sub,
|
||||||
mul,
|
mul,
|
||||||
div,
|
div,
|
||||||
|
id,
|
||||||
|
ply,
|
||||||
|
rev,
|
||||||
|
TimeSpan,
|
||||||
|
Hap,
|
||||||
|
State,
|
||||||
saw,
|
saw,
|
||||||
saw2,
|
saw2,
|
||||||
isaw,
|
isaw,
|
||||||
@@ -40,12 +44,7 @@ import {
|
|||||||
square2,
|
square2,
|
||||||
tri,
|
tri,
|
||||||
tri2,
|
tri2,
|
||||||
id,
|
time} = core;
|
||||||
ply,
|
|
||||||
rev,
|
|
||||||
time,
|
|
||||||
run,
|
|
||||||
} from '../index.mjs';
|
|
||||||
|
|
||||||
import { steady } from '../signal.mjs';
|
import { steady } from '../signal.mjs';
|
||||||
|
|
||||||
@@ -155,18 +154,7 @@ 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).add.in(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);
|
||||||
@@ -252,7 +240,7 @@ describe('Pattern', () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can SqueezeOut() structure', () => {
|
it('can squeezeOut() structure', () => {
|
||||||
sameFirst(sequence(1, [2, 3]).keep.squeezeout(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]]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -333,12 +321,12 @@ describe('Pattern', () => {
|
|||||||
it('Can set things with plain values', () => {
|
it('Can set things with plain values', () => {
|
||||||
sameFirst(sequence(1, 2, 3).set(4), sequence(4).fast(3));
|
sameFirst(sequence(1, 2, 3).set(4), sequence(4).fast(3));
|
||||||
});
|
});
|
||||||
describe('setOut()', () => {
|
describe('set.out()', () => {
|
||||||
it('Can set things with structure from second pattern', () => {
|
it('Can set things with structure from second pattern', () => {
|
||||||
sameFirst(sequence(1, 2).set.out(4), pure(4).mask(true, true));
|
sameFirst(sequence(1, 2).set.out(4), pure(4).mask(true, true));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('setSqueeze()', () => {
|
describe('set.squeeze()', () => {
|
||||||
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]).set.squeeze(sequence('a', 'b', 'c')),
|
sequence(1, [2, 3]).set.squeeze(sequence('a', 'b', 'c')),
|
||||||
@@ -419,8 +407,8 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('defaults to accepting sequences', () => {
|
it('defaults to accepting sequences', () => {
|
||||||
expect(sequence('a', 'b', 'c').fast(sequence(1.5, 2)).sortHapsByPart().firstCycle()).toStrictEqual(
|
expect(sequence(1, 2, 3).fast(sequence(1.5, 2)).firstCycle()).toStrictEqual(
|
||||||
sequence('a', 'b', 'c').fast(1.5, 2).sortHapsByPart().firstCycle(),
|
sequence(1, 2, 3).fast(1.5, 2).firstCycle(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('works as a static function', () => {
|
it('works as a static function', () => {
|
||||||
@@ -599,16 +587,6 @@ describe('Pattern', () => {
|
|||||||
.firstCycle(),
|
.firstCycle(),
|
||||||
).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle());
|
).toStrictEqual(sequence(sequence('a', 'a'), 'a', 'a').firstCycle());
|
||||||
});
|
});
|
||||||
it('Works as a toplevel function', () => {
|
|
||||||
expect(firstOf(3, fast(2), pure('a'))._fast(3).firstCycle()).toStrictEqual(
|
|
||||||
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
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', () => {
|
it('works with currying', () => {
|
||||||
expect(pure('a').firstOf(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
|
expect(pure('a').firstOf(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
|
||||||
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
||||||
@@ -909,18 +887,6 @@ describe('Pattern', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('run', () => {
|
|
||||||
it('Can run', () => {
|
|
||||||
expect(run(4).firstCycle()).toStrictEqual(sequence(0, 1, 2, 3).firstCycle());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('ribbon', () => {
|
|
||||||
it('Can ribbon', () => {
|
|
||||||
expect(cat(0, 1, 2, 3, 4, 5, 6, 7).ribbon(2, 4).fast(4).firstCycle()).toStrictEqual(
|
|
||||||
sequence(2, 3, 4, 5).firstCycle(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('linger', () => {
|
describe('linger', () => {
|
||||||
it('Can linger on the first quarter of a cycle', () => {
|
it('Can linger on the first quarter of a cycle', () => {
|
||||||
expect(sequence(0, 1, 2, 3, 4, 5, 6, 7).linger(0.25).firstCycle()).toStrictEqual(
|
expect(sequence(0, 1, 2, 3, 4, 5, 6, 7).linger(0.25).firstCycle()).toStrictEqual(
|
||||||
@@ -930,76 +896,8 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
describe('alignments', () => {
|
describe('alignments', () => {
|
||||||
it('Can squeeze arguments', () => {
|
it('Can squeeze arguments', () => {
|
||||||
expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle());
|
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('press', () => {
|
|
||||||
it('Can syncopate events', () => {
|
|
||||||
sameFirst(sequence('a', 'b', 'c', 'd').press(), sequence(silence, 'a', silence, 'b', silence, 'c', silence, 'd'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('hurry', () => {
|
|
||||||
it('Can speed up patterns and sounds', () => {
|
|
||||||
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
/*describe('composable functions', () => {
|
|
||||||
it('Can compose functions', () => {
|
|
||||||
sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
|
|
||||||
});
|
|
||||||
it('Can compose by method chaining operators with controls', () => {
|
|
||||||
sameFirst(s('bd').apply(set.n(3).fast(2)), s('bd').set.n(3).fast(2));
|
|
||||||
});
|
|
||||||
it('Can compose by method chaining operators and alignments with controls', () => {
|
|
||||||
sameFirst(s('bd').apply(set.in.n(3).fast(2)), s('bd').set.n(3).fast(2));
|
|
||||||
// sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('weave', () => {
|
|
||||||
it('Can distribute patterns along a pattern', () => {
|
|
||||||
sameFirst(n(0, 1).weave(2, s('bd', silence), s(silence, 'sd')), sequence(s('bd').n(0), s('sd').n(1)));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
describe('slice', () => {
|
|
||||||
it('Can slice a sample', () => {
|
|
||||||
sameFirst(
|
|
||||||
s('break').slice(4, sequence(0, 1, 2, 3)),
|
|
||||||
sequence(
|
|
||||||
{ begin: 0, end: 0.25, s: 'break', _slices: 4 },
|
|
||||||
{ begin: 0.25, end: 0.5, s: 'break', _slices: 4 },
|
|
||||||
{ begin: 0.5, end: 0.75, s: 'break', _slices: 4 },
|
|
||||||
{ begin: 0.75, end: 1, s: 'break', _slices: 4 },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('splice', () => {
|
|
||||||
it('Can splice a sample', () => {
|
|
||||||
sameFirst(
|
|
||||||
s('break').splice(4, sequence(0, 1, 2, 3)),
|
|
||||||
sequence(
|
|
||||||
{ begin: 0, end: 0.25, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
|
||||||
{ begin: 0.25, end: 0.5, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
|
||||||
{ begin: 0.5, end: 0.75, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
|
||||||
{ begin: 0.75, end: 1, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
/*test for issue 302 support alternative solmization types */
|
|
||||||
import { sol2note } from '../util.mjs';
|
|
||||||
import { test } from 'vitest';
|
|
||||||
import assert from 'assert';
|
|
||||||
|
|
||||||
test('solmization - letters', () => {
|
|
||||||
const result = sol2note(60, 'letters');
|
|
||||||
const expected = 'C4';
|
|
||||||
assert.equal(result, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('solmization - solfeggio', () => {
|
|
||||||
const result = sol2note(60, 'solfeggio');
|
|
||||||
const expected = 'Do4';
|
|
||||||
assert.equal(result, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('solmization - indian', () => {
|
|
||||||
const result = sol2note(60, 'indian');
|
|
||||||
const expected = 'Sa4';
|
|
||||||
assert.equal(result, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('solmization - german', () => {
|
|
||||||
const result = sol2note(60, 'german');
|
|
||||||
const expected = 'C4';
|
|
||||||
assert.equal(result, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('solmization - byzantine', () => {
|
|
||||||
const result = sol2note(60, 'byzantine');
|
|
||||||
const expected = 'Ni4';
|
|
||||||
assert.equal(result, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('solmization - japanese', () => {
|
|
||||||
const result = sol2note(60, 'japanese');
|
|
||||||
const expected = 'I4';
|
|
||||||
assert.equal(result, expected);
|
|
||||||
});
|
|
||||||
@@ -4,14 +4,14 @@ 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 { pure } from '../pattern.mjs';
|
import pattern from '../pattern.mjs';
|
||||||
|
const { pure } = pattern;
|
||||||
import {
|
import {
|
||||||
isNote,
|
isNote,
|
||||||
tokenizeNote,
|
tokenizeNote,
|
||||||
noteToMidi,
|
toMidi,
|
||||||
midiToFreq,
|
fromMidi,
|
||||||
freqToMidi,
|
mod,
|
||||||
_mod,
|
|
||||||
compose,
|
compose,
|
||||||
getFrequency,
|
getFrequency,
|
||||||
getPlayableNoteValue,
|
getPlayableNoteValue,
|
||||||
@@ -33,12 +33,9 @@ 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);
|
||||||
});
|
});
|
||||||
@@ -75,33 +72,27 @@ describe('isNote', () => {
|
|||||||
expect(tokenizeNote(123)).toStrictEqual([]);
|
expect(tokenizeNote(123)).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('noteToMidi', () => {
|
describe('toMidi', () => {
|
||||||
it('should turn notes into midi', () => {
|
it('should turn notes into midi', () => {
|
||||||
expect(noteToMidi('A4')).toEqual(69);
|
expect(toMidi('A4')).toEqual(69);
|
||||||
expect(noteToMidi('C4')).toEqual(60);
|
expect(toMidi('C4')).toEqual(60);
|
||||||
expect(noteToMidi('Db4')).toEqual(61);
|
expect(toMidi('Db4')).toEqual(61);
|
||||||
expect(noteToMidi('C3')).toEqual(48);
|
expect(toMidi('C3')).toEqual(48);
|
||||||
expect(noteToMidi('Cb3')).toEqual(47);
|
expect(toMidi('Cb3')).toEqual(47);
|
||||||
expect(noteToMidi('Cbb3')).toEqual(46);
|
expect(toMidi('Cbb3')).toEqual(46);
|
||||||
expect(noteToMidi('C#3')).toEqual(49);
|
expect(toMidi('C#3')).toEqual(49);
|
||||||
expect(noteToMidi('C#3')).toEqual(49);
|
expect(toMidi('C#3')).toEqual(49);
|
||||||
expect(noteToMidi('C##3')).toEqual(50);
|
expect(toMidi('C##3')).toEqual(50);
|
||||||
});
|
});
|
||||||
it('should throw an error when given a non-note', () => {
|
it('should throw an error when given a non-note', () => {
|
||||||
expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
|
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||||
expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
|
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('midiToFreq', () => {
|
describe('fromMidi', () => {
|
||||||
it('should turn midi into frequency', () => {
|
it('should turn midi into frequency', () => {
|
||||||
expect(midiToFreq(69)).toEqual(440);
|
expect(fromMidi(69)).toEqual(440);
|
||||||
expect(midiToFreq(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', () => {
|
||||||
@@ -128,22 +119,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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,7 +183,6 @@ 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,8 +6,6 @@ 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', () => {
|
||||||
@@ -23,8 +21,4 @@ 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,9 +20,9 @@ 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)) {
|
||||||
// If begin and end are in the same cycle, we're done.
|
// If begin and end are in the same cycle, we're done.
|
||||||
if (begin.sam().equals(end_sam)) {
|
if (begin.sam().equals(end_sam)) {
|
||||||
@@ -110,7 +110,7 @@ export class TimeSpan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
return this.begin.show() + ' → ' + this.end.show();
|
return this.begin.show() + ' -> ' + this.end.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
ui.mjs - <short description TODO>
|
ui.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/ui.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/ui.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
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/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export const backgroundImage = function (src, animateOptions = {}) {
|
|||||||
({
|
({
|
||||||
style: () => (container.style = bg + ';' + value),
|
style: () => (container.style = bg + ';' + value),
|
||||||
className: () => (container.className = value + ' ' + initialClassName),
|
className: () => (container.className = value + ' ' + initialClassName),
|
||||||
})[option]();
|
}[option]());
|
||||||
};
|
};
|
||||||
const funcOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'function');
|
const funcOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'function');
|
||||||
const stringOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'string');
|
const stringOptions = Object.entries(animateOptions).filter(([_, v]) => typeof v === 'string');
|
||||||
|
|||||||
+20
-114
@@ -5,84 +5,54 @@ 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 isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
||||||
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
|
|
||||||
export const tokenizeNote = (note) => {
|
export const tokenizeNote = (note) => {
|
||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
|
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [pc, acc, oct ? Number(oct) : undefined];
|
return [pc, acc, oct ? Number(oct) : undefined];
|
||||||
};
|
};
|
||||||
|
|
||||||
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
|
||||||
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
|
||||||
|
|
||||||
// turns the given note into its midi number representation
|
// turns the given note into its midi number representation
|
||||||
export const noteToMidi = (note, defaultOctave = 3) => {
|
export const toMidi = (note) => {
|
||||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
const [pc, acc, oct] = tokenizeNote(note);
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
throw new Error('not a note: "' + note + '"');
|
throw new Error('not a note: "' + note + '"');
|
||||||
}
|
}
|
||||||
const chroma = chromas[pc.toLowerCase()];
|
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
|
||||||
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
|
||||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
export const midiToFreq = (n) => {
|
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 noteToMidi(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
|
||||||
* @noAutocomplete
|
|
||||||
*/
|
*/
|
||||||
export const getFreq = (noteOrMidi) => {
|
export const getFreq = (noteOrMidi) => {
|
||||||
if (typeof noteOrMidi === 'number') {
|
if (typeof noteOrMidi === 'number') {
|
||||||
return midiToFreq(noteOrMidi);
|
return fromMidi(noteOrMidi);
|
||||||
}
|
}
|
||||||
return midiToFreq(noteToMidi(noteOrMidi));
|
return fromMidi(toMidi(noteOrMidi));
|
||||||
};
|
};
|
||||||
|
|
||||||
const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
|
||||||
/**
|
/**
|
||||||
* @deprecated only used in workshop (first-notes)
|
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||||
* @noAutocomplete
|
|
||||||
*/
|
*/
|
||||||
export const midi2note = (n) => {
|
export const midi2note = (n) => {
|
||||||
const oct = Math.floor(n / 12) - 1;
|
const oct = Math.floor(n / 12) - 1;
|
||||||
const pc = pcs[n % 12];
|
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
|
||||||
return pc + oct;
|
return pc + oct;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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)
|
// modulo that works with negative numbers e.g. mod(-1, 3) = 2
|
||||||
export const _mod = (n, m) => ((n % m) + m) % m;
|
// 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 getPlayableNoteValue = (hap) => {
|
export const getPlayableNoteValue = (hap) => {
|
||||||
let { value, context } = hap;
|
let { value, context } = hap;
|
||||||
@@ -95,7 +65,7 @@ export const getPlayableNoteValue = (hap) => {
|
|||||||
}
|
}
|
||||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||||
note = midiToFreq(hap.value);
|
note = fromMidi(hap.value);
|
||||||
} else if (typeof note === 'number' && context.type === 'frequency') {
|
} else if (typeof note === 'number' && context.type === 'frequency') {
|
||||||
note = hap.value; // legacy workaround.. will be removed in the future
|
note = hap.value; // legacy workaround.. will be removed in the future
|
||||||
} else if (typeof note !== 'string' || !isNote(note)) {
|
} else if (typeof note !== 'string' || !isNote(note)) {
|
||||||
@@ -114,9 +84,9 @@ export const getFrequency = (hap) => {
|
|||||||
return getFreq(value.note || value.n || value.value);
|
return getFreq(value.note || value.n || value.value);
|
||||||
}
|
}
|
||||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||||
value = midiToFreq(hap.value);
|
value = fromMidi(hap.value);
|
||||||
} else if (typeof value === 'string' && isNote(value)) {
|
} else if (typeof value === 'string' && isNote(value)) {
|
||||||
value = midiToFreq(noteToMidi(hap.value));
|
value = fromMidi(toMidi(hap.value));
|
||||||
} else if (typeof value !== 'number') {
|
} else if (typeof value !== 'number') {
|
||||||
throw new Error('not a note or frequency: ' + value);
|
throw new Error('not a note or frequency: ' + value);
|
||||||
}
|
}
|
||||||
@@ -147,9 +117,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, arity = func.length) {
|
export function curry(func, overload) {
|
||||||
const fn = function curried(...args) {
|
const fn = function curried(...args) {
|
||||||
if (args.length >= arity) {
|
if (args.length >= func.length) {
|
||||||
return func.apply(this, args);
|
return func.apply(this, args);
|
||||||
} else {
|
} else {
|
||||||
const partial = function (...args2) {
|
const partial = function (...args2) {
|
||||||
@@ -174,7 +144,7 @@ export function parseNumeral(numOrString) {
|
|||||||
return asNumber;
|
return asNumber;
|
||||||
}
|
}
|
||||||
if (isNote(numOrString)) {
|
if (isNote(numOrString)) {
|
||||||
return noteToMidi(numOrString);
|
return toMidi(numOrString);
|
||||||
}
|
}
|
||||||
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
||||||
}
|
}
|
||||||
@@ -210,67 +180,3 @@ export function parseFractional(numOrString) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
|
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
|
||||||
|
|
||||||
export const splitAt = function (index, value) {
|
|
||||||
return [value.slice(0, index), value.slice(index)];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
|
||||||
|
|
||||||
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
|
||||||
|
|
||||||
/* solmization, not used yet */
|
|
||||||
const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/
|
|
||||||
const indian = [
|
|
||||||
'Sa',
|
|
||||||
'Re',
|
|
||||||
'Ga',
|
|
||||||
'Ma',
|
|
||||||
'Pa',
|
|
||||||
'Dha',
|
|
||||||
'Ni',
|
|
||||||
]; /*indian musical notes, seems like they do not use flats or sharps*/
|
|
||||||
const german = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Hb', 'H']; /*german & dutch musical notes*/
|
|
||||||
const byzantine = [
|
|
||||||
'Ni',
|
|
||||||
'Pab',
|
|
||||||
'Pa',
|
|
||||||
'Voub',
|
|
||||||
'Vou',
|
|
||||||
'Ga',
|
|
||||||
'Dib',
|
|
||||||
'Di',
|
|
||||||
'Keb',
|
|
||||||
'Ke',
|
|
||||||
'Zob',
|
|
||||||
'Zo',
|
|
||||||
]; /*byzantine musical notes*/
|
|
||||||
const japanese = [
|
|
||||||
'I',
|
|
||||||
'Ro',
|
|
||||||
'Ha',
|
|
||||||
'Ni',
|
|
||||||
'Ho',
|
|
||||||
'He',
|
|
||||||
'To',
|
|
||||||
]; /*traditional japanese musical notes, seems like they do not use falts or sharps*/
|
|
||||||
|
|
||||||
const english = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
|
||||||
|
|
||||||
export const sol2note = (n, notation = 'letters') => {
|
|
||||||
const pc =
|
|
||||||
notation === 'solfeggio'
|
|
||||||
? solfeggio /*check if its is any of the following*/
|
|
||||||
: notation === 'indian'
|
|
||||||
? indian
|
|
||||||
: notation === 'german'
|
|
||||||
? german
|
|
||||||
: notation === 'byzantine'
|
|
||||||
? byzantine
|
|
||||||
: notation === 'japanese'
|
|
||||||
? japanese
|
|
||||||
: english; /*if not use standard version*/
|
|
||||||
const note = pc[n % 12]; /*calculating the midi value to the note*/
|
|
||||||
const oct = Math.floor(n / 12) - 1;
|
|
||||||
return note + oct;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -7,13 +7,6 @@ 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])])));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import { dependencies } from './package.json';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [],
|
|
||||||
build: {
|
|
||||||
lib: {
|
|
||||||
entry: resolve(__dirname, 'index.mjs'),
|
|
||||||
formats: ['es', 'cjs'],
|
|
||||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
external: [...Object.keys(dependencies)],
|
|
||||||
},
|
|
||||||
target: 'esnext',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -44,6 +44,6 @@ function createClock(
|
|||||||
};
|
};
|
||||||
const getPhase = () => phase;
|
const getPhase = () => phase;
|
||||||
// setCallback
|
// setCallback
|
||||||
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
|
return { setDuration, start, stop, pause, duration, getPhase };
|
||||||
}
|
}
|
||||||
export default createClock;
|
export default createClock;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getFrequency, logger, register } from '@strudel.cycles/core';
|
import strudel from '@strudel.cycles/core';
|
||||||
|
const { getFrequency, logger, Pattern } = strudel;
|
||||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||||
import csd from './project.csd?raw';
|
import csd from './project.csd?raw';
|
||||||
// import livecodeOrc from './livecode.orc?raw';
|
// import livecodeOrc from './livecode.orc?raw';
|
||||||
@@ -6,29 +7,19 @@ import presetsOrc from './presets.orc?raw';
|
|||||||
|
|
||||||
let csoundLoader, _csound;
|
let csoundLoader, _csound;
|
||||||
|
|
||||||
// initializes csound + can be used to reevaluate given instrument code
|
// triggers given instrument name using csound.
|
||||||
export async function loadCSound(code = '') {
|
Pattern.prototype._csound = function (instrument) {
|
||||||
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';
|
instrument = instrument || 'triangle';
|
||||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
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)
|
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
||||||
return pat.onTrigger((time, hap) => {
|
return this.onTrigger((time, hap) => {
|
||||||
if (!_csound) {
|
if (!_csound) {
|
||||||
logger('[csound] not loaded yet', 'warning');
|
logger('[csound] not loaded yet', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hap.ensureObjectValue();
|
if (typeof hap.value !== 'object') {
|
||||||
|
throw new Error('csound only support objects as hap values');
|
||||||
|
}
|
||||||
let { gain = 0.8 } = hap.value;
|
let { gain = 0.8 } = hap.value;
|
||||||
gain *= 0.2;
|
gain *= 0.2;
|
||||||
|
|
||||||
@@ -50,7 +41,20 @@ export const csound = register('csound', (instrument, pat) => {
|
|||||||
const msg = `i ${params.join(' ')}`;
|
const msg = `i ${params.join(' ')}`;
|
||||||
_csound.inputMessage(msg);
|
_csound.inputMessage(msg);
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// initializes csound + can be used to reevaluate given instrument code
|
||||||
|
export async function csound(code = '') {
|
||||||
|
await init();
|
||||||
|
if (code) {
|
||||||
|
code = `${code}`;
|
||||||
|
// ^ ^
|
||||||
|
// wrapping in backticks makes sure it works when calling as templated function
|
||||||
|
await _csound?.evalCode(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern.prototype.define('csound', (a, pat) => pat.csound(a), { composable: false, patternified: true });
|
||||||
|
|
||||||
function eventLogger(type, args) {
|
function eventLogger(type, args) {
|
||||||
const [msg] = args;
|
const [msg] = args;
|
||||||
@@ -77,26 +81,21 @@ function eventLogger(type, args) {
|
|||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
if (window.__csound__) {
|
if (window.__csound__) {
|
||||||
// Allows using some other csound instance.
|
// 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__;
|
_csound = window.__csound__;
|
||||||
return _csound;
|
|
||||||
} else {
|
} else {
|
||||||
const { Csound } = await import('@csound/browser');
|
const { Csound } = await import('@csound/browser');
|
||||||
_csound = await Csound({ audioContext: getAudioContext() });
|
_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.setOption('-odac');
|
|
||||||
await _csound.compileCsdText(csd);
|
|
||||||
// await _csound.compileOrc(livecodeOrc);
|
|
||||||
await _csound.compileOrc(presetsOrc);
|
|
||||||
await _csound.start();
|
|
||||||
return _csound;
|
|
||||||
}
|
}
|
||||||
|
_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() {
|
async function init() {
|
||||||
@@ -121,49 +120,3 @@ export async function loadOrc(url) {
|
|||||||
}
|
}
|
||||||
await orcCache[url];
|
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,15 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/csound",
|
"name": "@strudel.cycles/csound",
|
||||||
"version": "0.9.0",
|
"version": "0.3.0",
|
||||||
"description": "csound bindings for strudel",
|
"description": "csound bindings for strudel",
|
||||||
"main": "index.mjs",
|
"main": "csound.mjs",
|
||||||
"publishConfig": {
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"module": "dist/index.mjs"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"test": "echo \"No tests present.\" && exit 0"
|
||||||
"prepublishOnly": "npm run build"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -32,11 +27,6 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@csound/browser": "6.18.7",
|
"@csound/browser": "^6.18.3"
|
||||||
"@strudel.cycles/core": "workspace:*",
|
|
||||||
"@strudel.cycles/webaudio": "workspace:*"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"vite": "^4.3.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import { dependencies } from './package.json';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [],
|
|
||||||
build: {
|
|
||||||
lib: {
|
|
||||||
entry: resolve(__dirname, 'index.mjs'),
|
|
||||||
formats: ['es', 'cjs'],
|
|
||||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
external: [...Object.keys(dependencies)],
|
|
||||||
},
|
|
||||||
target: 'esnext',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# @strudel/desktopbridge
|
|
||||||
|
|
||||||
This package contains utilities used to communicate with the Tauri backend
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { listen } from '@tauri-apps/api/event';
|
|
||||||
import { logger } from '../core/logger.mjs';
|
|
||||||
|
|
||||||
// listen for log events from the Tauri backend and log in the UI
|
|
||||||
await listen('log-event', (e) => {
|
|
||||||
if (e.payload == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { message, message_type } = e.payload;
|
|
||||||
logger(message, message_type);
|
|
||||||
});
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { Invoke } from './utils.mjs';
|
|
||||||
import { Pattern, noteToMidi } from '@strudel.cycles/core';
|
|
||||||
|
|
||||||
const ON_MESSAGE = 0x90;
|
|
||||||
const OFF_MESSAGE = 0x80;
|
|
||||||
const CC_MESSAGE = 0xb0;
|
|
||||||
|
|
||||||
Pattern.prototype.midi = function (output) {
|
|
||||||
return this.onTrigger((time, hap, currentTime) => {
|
|
||||||
const { note, nrpnn, nrpv, ccn, ccv } = hap.value;
|
|
||||||
const offset = (time - currentTime) * 1000;
|
|
||||||
const velocity = Math.floor((hap.context?.velocity ?? 0.9) * 100); // TODO: refactor velocity
|
|
||||||
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
|
|
||||||
const roundedOffset = Math.round(offset);
|
|
||||||
const midichan = (hap.value.midichan ?? 1) - 1;
|
|
||||||
const requestedport = output ?? 'IAC';
|
|
||||||
const messagesfromjs = [];
|
|
||||||
if (note != null) {
|
|
||||||
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
|
||||||
messagesfromjs.push({
|
|
||||||
requestedport,
|
|
||||||
message: [ON_MESSAGE + midichan, midiNumber, velocity],
|
|
||||||
offset: roundedOffset,
|
|
||||||
});
|
|
||||||
messagesfromjs.push({
|
|
||||||
requestedport,
|
|
||||||
message: [OFF_MESSAGE + midichan, midiNumber, velocity],
|
|
||||||
offset: roundedOffset + duration,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (ccv && ccn) {
|
|
||||||
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
|
||||||
throw new Error('expected ccv to be a number between 0 and 1');
|
|
||||||
}
|
|
||||||
if (!['string', 'number'].includes(typeof ccn)) {
|
|
||||||
throw new Error('expected ccn to be a number or a string');
|
|
||||||
}
|
|
||||||
const scaled = Math.round(ccv * 127);
|
|
||||||
messagesfromjs.push({
|
|
||||||
requestedport,
|
|
||||||
message: [CC_MESSAGE + midichan, ccn, scaled],
|
|
||||||
offset: roundedOffset,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// invoke is temporarily blocking, run in an async process
|
|
||||||
if (messagesfromjs.length) {
|
|
||||||
setTimeout(() => {
|
|
||||||
Invoke('sendmidi', { messagesfromjs });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { parseNumeral, Pattern } from '@strudel.cycles/core';
|
|
||||||
import { Invoke } from './utils.mjs';
|
|
||||||
|
|
||||||
Pattern.prototype.osc = function () {
|
|
||||||
return this.onTrigger(async (time, hap, currentTime, cps = 1) => {
|
|
||||||
hap.ensureObjectValue();
|
|
||||||
const cycle = hap.wholeOrPart().begin.valueOf();
|
|
||||||
const delta = hap.duration.valueOf();
|
|
||||||
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
|
|
||||||
// make sure n and note are numbers
|
|
||||||
controls.n && (controls.n = parseNumeral(controls.n));
|
|
||||||
controls.note && (controls.note = parseNumeral(controls.note));
|
|
||||||
|
|
||||||
const params = [];
|
|
||||||
|
|
||||||
const timestamp = Math.round(Date.now() + (time - currentTime) * 1000);
|
|
||||||
|
|
||||||
Object.keys(controls).forEach((key) => {
|
|
||||||
const val = controls[key];
|
|
||||||
const value = typeof val === 'number' ? val.toString() : val;
|
|
||||||
|
|
||||||
if (value == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
params.push({
|
|
||||||
name: key,
|
|
||||||
value,
|
|
||||||
valueisnumber: typeof val === 'number',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const messagesfromjs = [];
|
|
||||||
if (params.length) {
|
|
||||||
messagesfromjs.push({ target: '/dirt/play', timestamp, params });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messagesfromjs.length) {
|
|
||||||
setTimeout(() => {
|
|
||||||
Invoke('sendosc', { messagesfromjs });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@strudel/desktopbridge",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"description": "tools/shims for communicating between the JS and Tauri (Rust) sides of the Studel desktop app",
|
|
||||||
"main": "index.mjs",
|
|
||||||
"type": "module",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"tidalcycles",
|
|
||||||
"strudel",
|
|
||||||
"pattern",
|
|
||||||
"livecoding",
|
|
||||||
"algorave"
|
|
||||||
],
|
|
||||||
"author": "Jade Rowland <jaderowlanddev@gmail.com>",
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@strudel.cycles/core": "workspace:*",
|
|
||||||
"@tauri-apps/api": "^1.4.0"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { invoke } from '@tauri-apps/api/tauri';
|
|
||||||
|
|
||||||
export const Invoke = invoke;
|
|
||||||
export const isTauri = () => window.__TAURI_IPC__ != null;
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
shift-parser
|
||||||
|
shift-reducer
|
||||||
|
!shift-traverser
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# @strudel.cycles/eval
|
||||||
|
|
||||||
|
This package contains the strudel code transformer and evaluator.
|
||||||
|
It allows creating strudel patterns from input code that is optimized for minimal keystrokes and human readability.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i @strudel.cycles/eval --save
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { evalScope } from '@strudel.cycles/core';
|
||||||
|
import { evaluate } from '@strudel.cycles/eval';
|
||||||
|
|
||||||
|
evalScope(
|
||||||
|
import('@strudel.cycles/core'),
|
||||||
|
// import other strudel packages here
|
||||||
|
); // add strudel to eval scope
|
||||||
|
|
||||||
|
async function run(code) {
|
||||||
|
const { pattern } = await evaluate(code);
|
||||||
|
const events = pattern.firstCycle();
|
||||||
|
console.log(events.map((e) => e.show()).join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
run('sequence([a3, [b3, c4]])');
|
||||||
|
```
|
||||||
|
|
||||||
|
yields:
|
||||||
|
|
||||||
|
```js
|
||||||
|
(0/1 -> 1/2, 0/1 -> 1/2, a3)
|
||||||
|
(1/2 -> 3/4, 1/2 -> 3/4, b3)
|
||||||
|
(3/4 -> 1/1, 3/4 -> 1/1, c4)
|
||||||
|
```
|
||||||
|
|
||||||
|
[play with @strudel.cycles/eval on codesandbox](https://codesandbox.io/s/strudel-eval-example-ndz1d8?file=/src/index.js)
|
||||||
|
|
||||||
|
## Dev Notes
|
||||||
|
|
||||||
|
shift-traverser is currently monkey patched because its package.json uses estraverse@^4.2.0,
|
||||||
|
which does not support the spread operator (Error: Unknown node type SpreadProperty.).
|
||||||
|
By monkey patched, I mean I copied the source of shift-traverser to a subfolder and installed the dependencies (shift-spec + estraverse@^5.3.0)
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
index.mjs - <short description TODO>
|
evaluate.mjs - <short description TODO>
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/superdough/index.mjs>
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.mjs>
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './superdough.mjs';
|
import core from '@strudel.cycles/core';
|
||||||
export * from './sampler.mjs';
|
import shapeshifter from './shapeshifter.mjs';
|
||||||
export * from './helpers.mjs';
|
|
||||||
export * from './synth.mjs';
|
export const evaluate = async (code) => {
|
||||||
export * from './zzfx.mjs';
|
return core.evaluate(code, shapeshifter);
|
||||||
export * from './logger.mjs';
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './evaluate.mjs';
|
||||||
Generated
+297
@@ -0,0 +1,297 @@
|
|||||||
|
{
|
||||||
|
"name": "@strudel.cycles/eval",
|
||||||
|
"version": "0.4.1",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@strudel.cycles/eval",
|
||||||
|
"version": "0.1.1",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"dependencies": {
|
||||||
|
"estraverse": "^5.3.0",
|
||||||
|
"shift-ast": "^6.1.0",
|
||||||
|
"shift-codegen": "^7.0.3",
|
||||||
|
"shift-parser": "^7.0.3",
|
||||||
|
"shift-spec": "^2018.0.2",
|
||||||
|
"shift-traverser": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/estraverse": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esutils": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/multimap": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/multimap/-/multimap-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw=="
|
||||||
|
},
|
||||||
|
"node_modules/object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shift-ast": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vj4XUIJIFPIh6VcBGJ1hjH/kM88XGer94Pr7Rvxa+idEylDsrwtLw268HoxGo5xReL6T3DdRl/9/Pr1XihZ/8Q=="
|
||||||
|
},
|
||||||
|
"node_modules/shift-codegen": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-codegen/-/shift-codegen-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-dfCVVdBF0qZ6pkajQ3bjxRdNEltyxEITVe7tBJkQt2eCI3znUkSxq0VSe/tTWq1LKHeAS4HuOiqYEuHMFkSq9w==",
|
||||||
|
"dependencies": {
|
||||||
|
"esutils": "^2.0.2",
|
||||||
|
"object-assign": "^4.1.0",
|
||||||
|
"shift-reducer": "6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shift-parser": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-parser/-/shift-parser-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-uYX2ORyZfKZrUc4iKKkO9KOhzUSxCrSBk7QK6ZmShId+BOo1gh1IwecVy97ynyOTpmhPWUttjC8BzsnQl65Zew==",
|
||||||
|
"dependencies": {
|
||||||
|
"multimap": "^1.0.2",
|
||||||
|
"shift-ast": "6.0.0",
|
||||||
|
"shift-reducer": "6.0.0",
|
||||||
|
"shift-regexp-acceptor": "2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shift-parser/node_modules/shift-ast": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw=="
|
||||||
|
},
|
||||||
|
"node_modules/shift-reducer": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-reducer/-/shift-reducer-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-2rJraRP8drIOjvaE/sALa+0tGJmMVUzlmS3wIJerJbaYuCjpFAiF0WjkTOFVtz1144Nm/ECmqeG+7yRhuMVsMg==",
|
||||||
|
"dependencies": {
|
||||||
|
"shift-ast": "6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shift-reducer/node_modules/shift-ast": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw=="
|
||||||
|
},
|
||||||
|
"node_modules/shift-regexp-acceptor": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-regexp-acceptor/-/shift-regexp-acceptor-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-sxL7e5JNUFxm+gutFRXktX2D6KVgDAHNuDsk5XHB9Z+N5yXooZG6pdZ1GEbo3Jz6lF7ETYLBC4WAjIFm2RKTmA==",
|
||||||
|
"dependencies": {
|
||||||
|
"unicode-match-property-ecmascript": "1.0.4",
|
||||||
|
"unicode-match-property-value-ecmascript": "1.0.2",
|
||||||
|
"unicode-property-aliases-ecmascript": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shift-spec": {
|
||||||
|
"version": "2018.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-spec/-/shift-spec-2018.0.2.tgz",
|
||||||
|
"integrity": "sha512-5CP/cKDEim4rZ6ViCSipTLY2U7HJr8q/kpDuCBmebFqbx/0DeozWO+9ienHmYjgGLDfHrqj+LBAN67FRK2vE6w=="
|
||||||
|
},
|
||||||
|
"node_modules/shift-traverser": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-traverser/-/shift-traverser-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-DMY3512wJbdC+IC+nhLH3/Stgr2BbxbNcg7qyZ6+e5qNnNs8TBQJWdMsRgHlX1JXwF4C0ONKS8VUxsPT0Tf7aw==",
|
||||||
|
"dependencies": {
|
||||||
|
"estraverse": "4.2.0",
|
||||||
|
"shift-spec": "2018.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shift-traverser/node_modules/estraverse": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
|
||||||
|
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shift-traverser/node_modules/shift-spec": {
|
||||||
|
"version": "2018.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-spec/-/shift-spec-2018.0.0.tgz",
|
||||||
|
"integrity": "sha512-/aiPOkj7dbe+CV2VZhIMTHQToZmgniofpRG7Yr7x2/0sO6CSVC++py1Wzf+s+rWSTDHKcLvziVAxjRRV4i4EoQ=="
|
||||||
|
},
|
||||||
|
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unicode-match-property-ecmascript": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==",
|
||||||
|
"dependencies": {
|
||||||
|
"unicode-canonical-property-names-ecmascript": "^1.0.4",
|
||||||
|
"unicode-property-aliases-ecmascript": "^1.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unicode-match-property-value-ecmascript": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unicode-property-aliases-ecmascript": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"estraverse": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="
|
||||||
|
},
|
||||||
|
"esutils": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
|
||||||
|
},
|
||||||
|
"multimap": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/multimap/-/multimap-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw=="
|
||||||
|
},
|
||||||
|
"object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||||
|
},
|
||||||
|
"shift-ast": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vj4XUIJIFPIh6VcBGJ1hjH/kM88XGer94Pr7Rvxa+idEylDsrwtLw268HoxGo5xReL6T3DdRl/9/Pr1XihZ/8Q=="
|
||||||
|
},
|
||||||
|
"shift-codegen": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-codegen/-/shift-codegen-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-dfCVVdBF0qZ6pkajQ3bjxRdNEltyxEITVe7tBJkQt2eCI3znUkSxq0VSe/tTWq1LKHeAS4HuOiqYEuHMFkSq9w==",
|
||||||
|
"requires": {
|
||||||
|
"esutils": "^2.0.2",
|
||||||
|
"object-assign": "^4.1.0",
|
||||||
|
"shift-reducer": "6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shift-parser": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-parser/-/shift-parser-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-uYX2ORyZfKZrUc4iKKkO9KOhzUSxCrSBk7QK6ZmShId+BOo1gh1IwecVy97ynyOTpmhPWUttjC8BzsnQl65Zew==",
|
||||||
|
"requires": {
|
||||||
|
"multimap": "^1.0.2",
|
||||||
|
"shift-ast": "6.0.0",
|
||||||
|
"shift-reducer": "6.0.0",
|
||||||
|
"shift-regexp-acceptor": "2.0.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"shift-ast": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shift-reducer": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-reducer/-/shift-reducer-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-2rJraRP8drIOjvaE/sALa+0tGJmMVUzlmS3wIJerJbaYuCjpFAiF0WjkTOFVtz1144Nm/ECmqeG+7yRhuMVsMg==",
|
||||||
|
"requires": {
|
||||||
|
"shift-ast": "6.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"shift-ast": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-ast/-/shift-ast-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shift-regexp-acceptor": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-regexp-acceptor/-/shift-regexp-acceptor-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-sxL7e5JNUFxm+gutFRXktX2D6KVgDAHNuDsk5XHB9Z+N5yXooZG6pdZ1GEbo3Jz6lF7ETYLBC4WAjIFm2RKTmA==",
|
||||||
|
"requires": {
|
||||||
|
"unicode-match-property-ecmascript": "1.0.4",
|
||||||
|
"unicode-match-property-value-ecmascript": "1.0.2",
|
||||||
|
"unicode-property-aliases-ecmascript": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shift-spec": {
|
||||||
|
"version": "2018.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-spec/-/shift-spec-2018.0.2.tgz",
|
||||||
|
"integrity": "sha512-5CP/cKDEim4rZ6ViCSipTLY2U7HJr8q/kpDuCBmebFqbx/0DeozWO+9ienHmYjgGLDfHrqj+LBAN67FRK2vE6w=="
|
||||||
|
},
|
||||||
|
"shift-traverser": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-traverser/-/shift-traverser-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-DMY3512wJbdC+IC+nhLH3/Stgr2BbxbNcg7qyZ6+e5qNnNs8TBQJWdMsRgHlX1JXwF4C0ONKS8VUxsPT0Tf7aw==",
|
||||||
|
"requires": {
|
||||||
|
"estraverse": "4.2.0",
|
||||||
|
"shift-spec": "2018.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"estraverse": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
|
||||||
|
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM="
|
||||||
|
},
|
||||||
|
"shift-spec": {
|
||||||
|
"version": "2018.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shift-spec/-/shift-spec-2018.0.0.tgz",
|
||||||
|
"integrity": "sha512-/aiPOkj7dbe+CV2VZhIMTHQToZmgniofpRG7Yr7x2/0sO6CSVC++py1Wzf+s+rWSTDHKcLvziVAxjRRV4i4EoQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unicode-canonical-property-names-ecmascript": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ=="
|
||||||
|
},
|
||||||
|
"unicode-match-property-ecmascript": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==",
|
||||||
|
"requires": {
|
||||||
|
"unicode-canonical-property-names-ecmascript": "^1.0.4",
|
||||||
|
"unicode-property-aliases-ecmascript": "^1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unicode-match-property-value-ecmascript": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ=="
|
||||||
|
},
|
||||||
|
"unicode-property-aliases-ecmascript": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "superdough",
|
"name": "@strudel.cycles/eval",
|
||||||
"version": "0.9.8",
|
"version": "0.4.1",
|
||||||
"description": "simple web audio synth and sampler intended for live coding. inspired by superdirt and webdirt.",
|
"description": "Code evaluator for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"publishConfig": {
|
|
||||||
"main": "dist/index.cjs",
|
|
||||||
"module": "dist/index.mjs"
|
|
||||||
},
|
|
||||||
"directories": {
|
"directories": {
|
||||||
"example": "examples"
|
"test": "test"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"test": "vitest run"
|
||||||
"prepublishOnly": "npm run build"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -32,10 +27,13 @@
|
|||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"devDependencies": {
|
|
||||||
"vite": "^4.3.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanostores": "^0.8.1"
|
"@strudel.cycles/core": "^0.4.1",
|
||||||
|
"estraverse": "^5.3.0",
|
||||||
|
"shift-ast": "^7.0.0",
|
||||||
|
"shift-codegen": "^8.1.0",
|
||||||
|
"shift-parser": "^8.0.0",
|
||||||
|
"shift-spec": "^2019.0.0",
|
||||||
|
"shift-traverser": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
/*
|
||||||
|
shapeshifter.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/shapeshifter.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* import { parseScriptWithLocation } from './shift-parser/index.js'; // npm module does not work in the browser
|
||||||
|
import traverser from './shift-traverser/index.js'; // npm module does not work in the browser */
|
||||||
|
import { parseScriptWithLocation } from 'shift-parser';
|
||||||
|
import traverser from './shift-traverser/index.js';
|
||||||
|
const { replace } = traverser;
|
||||||
|
import {
|
||||||
|
LiteralStringExpression,
|
||||||
|
IdentifierExpression,
|
||||||
|
CallExpression,
|
||||||
|
StaticMemberExpression,
|
||||||
|
ReturnStatement,
|
||||||
|
ArrayExpression,
|
||||||
|
LiteralNumericExpression,
|
||||||
|
} from 'shift-ast';
|
||||||
|
import shiftCodegen from 'shift-codegen';
|
||||||
|
const codegen = shiftCodegen.default || shiftCodegen; // parcel module resolution fuckup
|
||||||
|
|
||||||
|
import strudel from '@strudel.cycles/core';
|
||||||
|
|
||||||
|
const { Pattern } = strudel;
|
||||||
|
|
||||||
|
const isNote = (name) => /^[a-gC-G][bs]?[0-9]$/.test(name);
|
||||||
|
|
||||||
|
const addLocations = true;
|
||||||
|
export const addMiniLocations = true;
|
||||||
|
export const minifyStrings = true;
|
||||||
|
export const wrappedAsync = false; // this is now handled by core evaluate by default
|
||||||
|
export const shouldAddReturn = true;
|
||||||
|
|
||||||
|
export default (_code) => {
|
||||||
|
const { code, addReturn } = wrapAsync(_code);
|
||||||
|
const ast = parseScriptWithLocation(disguiseImports(code));
|
||||||
|
const artificialNodes = [];
|
||||||
|
const parents = [];
|
||||||
|
const shifted = replace(ast.tree, {
|
||||||
|
enter(node, parent) {
|
||||||
|
parents.push(parent);
|
||||||
|
const isSynthetic = parents.some((p) => artificialNodes.includes(p));
|
||||||
|
if (isSynthetic) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace template string `xxx` with mini(`xxx`)
|
||||||
|
if (minifyStrings && isBackTickString(node)) {
|
||||||
|
return minifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||||
|
}
|
||||||
|
// allows to use top level strings, which are normally directives... but we don't need directives
|
||||||
|
if (minifyStrings && node.directives?.length === 1 && !node.statements?.length) {
|
||||||
|
const str = new LiteralStringExpression({ value: node.directives[0].rawValue });
|
||||||
|
const wrapped = minifyWithLocation(str, node.directives[0], ast.locations, artificialNodes);
|
||||||
|
return { ...node, directives: [], statements: [wrapped] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace double quote string "xxx" with mini('xxx')
|
||||||
|
if (minifyStrings && isStringWithDoubleQuotes(node, ast.locations, code)) {
|
||||||
|
return minifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// operator overloading => still not done
|
||||||
|
const operators = {
|
||||||
|
'*': 'fast',
|
||||||
|
'/': 'slow',
|
||||||
|
'&': 'stack',
|
||||||
|
'&&': 'append',
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
node.type === 'BinaryExpression' &&
|
||||||
|
operators[node.operator] &&
|
||||||
|
['LiteralNumericExpression', 'LiteralStringExpression', 'IdentifierExpression'].includes(node.right?.type) &&
|
||||||
|
canBeOverloaded(node.left)
|
||||||
|
) {
|
||||||
|
let arg = node.left;
|
||||||
|
if (node.left.type === 'IdentifierExpression') {
|
||||||
|
arg = wrapFunction('reify', node.left);
|
||||||
|
}
|
||||||
|
return new CallExpression({
|
||||||
|
callee: new StaticMemberExpression({
|
||||||
|
property: operators[node.operator],
|
||||||
|
object: wrapFunction('reify', arg),
|
||||||
|
}),
|
||||||
|
arguments: [node.right],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMarkable = isPatternArg(parents) || hasModifierCall(parent);
|
||||||
|
// add to location to pure(x) calls
|
||||||
|
if (node.type === 'CallExpression' && node.callee.name === 'pure') {
|
||||||
|
const literal = node.arguments[0];
|
||||||
|
// const value = literal[{ LiteralNumericExpression: 'value', LiteralStringExpression: 'name' }[literal.type]];
|
||||||
|
return reifyWithLocation(literal, node.arguments[0], ast.locations, artificialNodes);
|
||||||
|
}
|
||||||
|
// replace pseudo note variables
|
||||||
|
if (node.type === 'IdentifierExpression') {
|
||||||
|
if (isNote(node.name)) {
|
||||||
|
const value = node.name[1] === 's' ? node.name.replace('s', '#') : node.name;
|
||||||
|
if (addLocations && isMarkable) {
|
||||||
|
return reifyWithLocation(new LiteralStringExpression({ value }), node, ast.locations, artificialNodes);
|
||||||
|
}
|
||||||
|
return new LiteralStringExpression({ value });
|
||||||
|
}
|
||||||
|
if (node.name === 'r') {
|
||||||
|
return new IdentifierExpression({ name: 'silence' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
addLocations &&
|
||||||
|
['LiteralStringExpression' /* , 'LiteralNumericExpression' */].includes(node.type) &&
|
||||||
|
isMarkable
|
||||||
|
) {
|
||||||
|
// TODO: to make LiteralNumericExpression work, we need to make sure we're not inside timeCat...
|
||||||
|
return reifyWithLocation(node, node, ast.locations, artificialNodes);
|
||||||
|
}
|
||||||
|
if (addMiniLocations) {
|
||||||
|
return addMiniNotationLocations(node, ast.locations, artificialNodes);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
},
|
||||||
|
leave() {
|
||||||
|
parents.pop();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// add return to last statement (because it's wrapped in an async function artificially)
|
||||||
|
if (shouldAddReturn) {
|
||||||
|
addReturn(shifted);
|
||||||
|
}
|
||||||
|
const generated = undisguiseImports(codegen(shifted));
|
||||||
|
return generated;
|
||||||
|
};
|
||||||
|
|
||||||
|
// renames all import statements to "_mport" as Shift doesn't support dynamic import.
|
||||||
|
// there shouldn't be any side-effects from this as this change does not affect
|
||||||
|
// the syntax & will be undone by the equivalent replace in "undisguiseImports".
|
||||||
|
function disguiseImports(code) {
|
||||||
|
return code.replaceAll('import', '_mport'); // Must be the same length!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename the renamed import statements back to "import"
|
||||||
|
function undisguiseImports(code) {
|
||||||
|
return code.replaceAll('_mport', 'import');
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapAsync(code) {
|
||||||
|
// wrap code in async to make await work on top level => this will create 1 line offset to locations
|
||||||
|
// this is why line offset is -1 in getLocationObject calls below
|
||||||
|
if (wrappedAsync) {
|
||||||
|
code = `(async () => {
|
||||||
|
${code}
|
||||||
|
})()`;
|
||||||
|
}
|
||||||
|
const addReturn = (ast) => {
|
||||||
|
const body = wrappedAsync ? ast.statements[0].expression.callee.body : ast;
|
||||||
|
body.statements = body.statements
|
||||||
|
.slice(0, -1)
|
||||||
|
.concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]);
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
addReturn,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMiniNotationLocations(node, locations, artificialNodes) {
|
||||||
|
const miniFunctions = ['mini', 'm'];
|
||||||
|
// const isAlreadyWrapped = parent?.type === 'CallExpression' && parent.callee.name === 'withLocationOffset';
|
||||||
|
if (node.type === 'CallExpression' && miniFunctions.includes(node.callee.name)) {
|
||||||
|
// mini('c3')
|
||||||
|
if (node.arguments.length > 1) {
|
||||||
|
// TODO: transform mini(...args) to cat(...args.map(mini)) ?
|
||||||
|
console.warn('multi arg mini locations not supported yet...');
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
const str = node.arguments[0];
|
||||||
|
return minifyWithLocation(str, str, locations, artificialNodes);
|
||||||
|
}
|
||||||
|
if (node.type === 'StaticMemberExpression' && miniFunctions.includes(node.property)) {
|
||||||
|
// 'c3'.mini or 'c3'.m
|
||||||
|
return minifyWithLocation(node.object, node, locations, artificialNodes);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapFunction(name, ...args) {
|
||||||
|
return new CallExpression({
|
||||||
|
callee: new IdentifierExpression({ name }),
|
||||||
|
arguments: args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBackTickString(node) {
|
||||||
|
return node.type === 'TemplateExpression' && node.elements.length === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStringWithDoubleQuotes(node, locations, code) {
|
||||||
|
if (node.type !== 'LiteralStringExpression') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const loc = locations.get(node);
|
||||||
|
const snippet = code.slice(loc.start.offset, loc.end.offset);
|
||||||
|
return snippet[0] === '"'; // we can trust the end is also ", as the parsing did not fail
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if the given parents belong to a pattern argument node
|
||||||
|
// this is used to check if a node should receive a location for highlighting
|
||||||
|
function isPatternArg(parents) {
|
||||||
|
if (!parents.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const ancestors = parents.slice(0, -1);
|
||||||
|
const parent = parents[parents.length - 1];
|
||||||
|
if (isPatternFactory(parent)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (parent?.type === 'ArrayExpression') {
|
||||||
|
return isPatternArg(ancestors);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasModifierCall(parent) {
|
||||||
|
// TODO: modifiers are more than composables, for example every is not composable but should be seen as modifier..
|
||||||
|
// need all prototypes of Pattern
|
||||||
|
return (
|
||||||
|
parent?.type === 'StaticMemberExpression' && Object.keys(Pattern.prototype.composable).includes(parent.property)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const factories = Object.keys(Pattern.prototype.factories).concat(['mini']);
|
||||||
|
|
||||||
|
function isPatternFactory(node) {
|
||||||
|
return node?.type === 'CallExpression' && factories.includes(node.callee.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function canBeOverloaded(node) {
|
||||||
|
return (node.type === 'IdentifierExpression' && isNote(node.name)) || isPatternFactory(node);
|
||||||
|
// TODO: support sequence(c3).transpose(3).x.y.z
|
||||||
|
}
|
||||||
|
|
||||||
|
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
|
||||||
|
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
|
||||||
|
function reifyWithLocation(literalNode, node, locations, artificialNodes) {
|
||||||
|
const args = getLocationArguments(node, locations);
|
||||||
|
const withLocation = new CallExpression({
|
||||||
|
callee: new StaticMemberExpression({
|
||||||
|
object: wrapFunction('reify', literalNode),
|
||||||
|
property: 'withLocation',
|
||||||
|
}),
|
||||||
|
arguments: args,
|
||||||
|
});
|
||||||
|
artificialNodes.push(withLocation);
|
||||||
|
return withLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// turns node in reify(value).withLocation(location), where location is the node's location in the source code
|
||||||
|
// with this, the reified pattern can pass its location to the event, to know where to highlight when it's active
|
||||||
|
function minifyWithLocation(literalNode, node, locations, artificialNodes) {
|
||||||
|
const args = getLocationArguments(node, locations);
|
||||||
|
const wrapped = wrapFunction('mini', literalNode);
|
||||||
|
if (!addMiniLocations) {
|
||||||
|
artificialNodes.push(wrapped);
|
||||||
|
return wrapped;
|
||||||
|
}
|
||||||
|
const withLocation = new CallExpression({
|
||||||
|
callee: new StaticMemberExpression({
|
||||||
|
object: wrapped,
|
||||||
|
property: 'withMiniLocation',
|
||||||
|
}),
|
||||||
|
arguments: args,
|
||||||
|
});
|
||||||
|
artificialNodes.push(withLocation);
|
||||||
|
return withLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocationArguments(node, locations) {
|
||||||
|
const loc = locations.get(node);
|
||||||
|
const lineOffset = wrappedAsync ? -1 : 0;
|
||||||
|
return [
|
||||||
|
new ArrayExpression({
|
||||||
|
elements: [
|
||||||
|
new LiteralNumericExpression({ value: loc.start.line + lineOffset }), // the minus 1 assumes the code has been wrapped in async iife
|
||||||
|
new LiteralNumericExpression({ value: loc.start.column }),
|
||||||
|
new LiteralNumericExpression({ value: loc.start.offset }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
new ArrayExpression({
|
||||||
|
elements: [
|
||||||
|
new LiteralNumericExpression({ value: loc.end.line + lineOffset }), // the minus 1 assumes the code has been wrapped in async iife
|
||||||
|
new LiteralNumericExpression({ value: loc.end.column }),
|
||||||
|
new LiteralNumericExpression({ value: loc.end.offset }),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
index.js - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/shift-traverser/index.js>
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright (C) 2014 Yusuke Suzuki <utatane.tea@gmail.com>
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||||
|
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import _Spec from 'shift-spec';
|
||||||
|
const Spec = _Spec.default || _Spec; // parcel module resolution fuckup
|
||||||
|
// import { version } from '../package.json'
|
||||||
|
|
||||||
|
// Loading uncached estraverse for changing estraverse.Syntax.
|
||||||
|
import _estraverse from 'estraverse';
|
||||||
|
|
||||||
|
const estraverse = _estraverse.cloneEnvironment();
|
||||||
|
|
||||||
|
// Adjust estraverse members.
|
||||||
|
|
||||||
|
Object.keys(estraverse.Syntax)
|
||||||
|
.filter((key) => key !== 'Property')
|
||||||
|
.forEach((key) => {
|
||||||
|
delete estraverse.Syntax[key];
|
||||||
|
delete estraverse.VisitorKeys[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
estraverse.Syntax,
|
||||||
|
Object.keys(Spec).reduce((result, key) => {
|
||||||
|
result[key] = key;
|
||||||
|
return result;
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
estraverse.VisitorKeys,
|
||||||
|
Object.keys(Spec).reduce((result, key) => {
|
||||||
|
result[key] = Spec[key].fields.map((field) => field.name);
|
||||||
|
return result;
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// estraverse.version = version;
|
||||||
|
export default estraverse;
|
||||||
|
|
||||||
|
/* vim: set sw=4 ts=4 et tw=80 : */
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
evaluate.test.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/test/evaluate.test.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, describe, it } from 'vitest';
|
||||||
|
|
||||||
|
import { evaluate } from '../evaluate.mjs';
|
||||||
|
import { mini } from '@strudel.cycles/mini';
|
||||||
|
import strudel from '@strudel.cycles/core';
|
||||||
|
const { fastcat, evalScope } = strudel;
|
||||||
|
|
||||||
|
describe('evaluate', async () => {
|
||||||
|
await evalScope({ mini }, strudel);
|
||||||
|
const ev = async (code) => (await evaluate(code)).pattern.firstCycleValues;
|
||||||
|
it('Should evaluate strudel functions', async () => {
|
||||||
|
expect(await ev('pure("c3")')).toEqual(['c3']);
|
||||||
|
expect(await ev('cat("c3")')).toEqual(['c3']);
|
||||||
|
expect(await ev('fastcat("c3", "d3")')).toEqual(['c3', 'd3']);
|
||||||
|
expect(await ev('slowcat("c3", "d3")')).toEqual(['c3']);
|
||||||
|
});
|
||||||
|
it('Scope should be extendable', async () => {
|
||||||
|
await evalScope({ myFunction: (...x) => fastcat(...x) });
|
||||||
|
expect(await ev('myFunction("c3", "d3")')).toEqual(['c3', 'd3']);
|
||||||
|
});
|
||||||
|
it('Should evaluate simple double quoted mini notation', async () => {
|
||||||
|
expect(await ev('"c3"')).toEqual(['c3']);
|
||||||
|
expect(await ev('"c3 d3"')).toEqual(['c3', 'd3']);
|
||||||
|
expect(await ev('"<c3 d3>"')).toEqual(['c3']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
shapeshifter.test.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/test/shapeshifter.test.mjs>
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import shapeshifter, { wrappedAsync } from '../shapeshifter.mjs';
|
||||||
|
|
||||||
|
describe('shapeshifter', () => {
|
||||||
|
it('Should shift simple double quote string', () => {
|
||||||
|
if (wrappedAsync) {
|
||||||
|
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
|
||||||
|
} else {
|
||||||
|
expect(shapeshifter('"c3"')).toEqual('return mini("c3").withMiniLocation([1,0,0],[1,4,4])');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (wrappedAsync) {
|
||||||
|
it('Should handle dynamic imports', () => {
|
||||||
|
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
|
||||||
|
'const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -7,3 +7,7 @@ This package adds midi functionality to strudel Patterns.
|
|||||||
```sh
|
```sh
|
||||||
npm i @strudel.cycles/midi --save
|
npm i @strudel.cycles/midi --save
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Dev Notes
|
||||||
|
|
||||||
|
- is this package really necessary? currently, /tone also depends on webmidi through @tonejs/piano. Either move piano out of /tone or merge /midi into /tone...
|
||||||
|
|||||||
+64
-96
@@ -5,38 +5,19 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _WebMidi from 'webmidi';
|
import * as _WebMidi from 'webmidi';
|
||||||
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
|
import core from '@strudel.cycles/core';
|
||||||
import { noteToMidi } from '@strudel.cycles/core';
|
const { Pattern, isPattern, isNote, getPlayableNoteValue, logger } = core;
|
||||||
import { Note } from 'webmidi';
|
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';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMidiDeviceNamesString(outputs) {
|
|
||||||
return outputs.map((o) => `'${o.name}'`).join(' | ');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function enableWebMidi(options = {}) {
|
export function enableWebMidi(options = {}) {
|
||||||
const { onReady, onConnected, onDisconnected, onEnabled } = options;
|
const { onReady, onConnected, onDisconnected } = options;
|
||||||
if (WebMidi.enabled) {
|
|
||||||
return;
|
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.');
|
||||||
}
|
}
|
||||||
WebMidi.addListener('connected', () => {
|
|
||||||
onConnected?.(WebMidi);
|
|
||||||
});
|
|
||||||
WebMidi.addListener('enabled', () => {
|
|
||||||
onEnabled?.(WebMidi);
|
|
||||||
});
|
|
||||||
// Reacting when a device becomes unavailable
|
|
||||||
WebMidi.addListener('disconnected', (e) => {
|
|
||||||
onDisconnected?.(WebMidi, e);
|
|
||||||
});
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (WebMidi.enabled) {
|
if (WebMidi.enabled) {
|
||||||
// if already enabled, just resolve WebMidi
|
// if already enabled, just resolve WebMidi
|
||||||
@@ -47,6 +28,13 @@ export function enableWebMidi(options = {}) {
|
|||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
|
WebMidi.addListener('connected', (e) => {
|
||||||
|
onConnected?.(WebMidi);
|
||||||
|
});
|
||||||
|
// Reacting when a device becomes unavailable
|
||||||
|
WebMidi.addListener('disconnected', (e) => {
|
||||||
|
onDisconnected?.(WebMidi, e);
|
||||||
|
});
|
||||||
onReady?.(WebMidi);
|
onReady?.(WebMidi);
|
||||||
resolve(WebMidi);
|
resolve(WebMidi);
|
||||||
});
|
});
|
||||||
@@ -55,85 +43,65 @@ 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);
|
||||||
|
|
||||||
// output?: string | number, outputs: typeof WebMidi.outputs
|
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||||
function getDevice(output, outputs) {
|
Pattern.prototype.midi = async function (output, channel = 1) {
|
||||||
if (!outputs.length) {
|
await enableWebMidi({
|
||||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
onConnected: ({ outputs }) =>
|
||||||
}
|
logger(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
||||||
if (typeof output === 'number') {
|
onDisconnected: ({ outputs }) =>
|
||||||
return outputs[output];
|
logger(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
||||||
}
|
onReady: ({ outputs }) => {
|
||||||
if (typeof output === 'string') {
|
const chosenOutput = output ?? outputs[0];
|
||||||
return outputByName(output);
|
const otherOutputs = outputs
|
||||||
}
|
.filter((o) => o.name !== chosenOutput.name)
|
||||||
// attempt to default to first IAC device if none is specified
|
.map((o) => `'${o.name}'`)
|
||||||
const IACOutput = outputs.find((output) => output.name.includes('IAC'));
|
.join(' | ');
|
||||||
const device = IACOutput ?? outputs[0];
|
logger(`Midi connected! Using "${chosenOutput.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
|
||||||
if (!device) {
|
},
|
||||||
throw new Error(
|
});
|
||||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${getMidiDeviceNamesString(WebMidi.outputs)}`,
|
if (isPattern(output?.constructor?.name)) {
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return IACOutput ?? outputs[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
Pattern.prototype.midi = function (output) {
|
|
||||||
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'
|
||||||
}')`,
|
}')`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return this.onTrigger((time, hap) => {
|
||||||
enableWebMidi({
|
let note = getPlayableNoteValue(hap);
|
||||||
onEnabled: ({ outputs }) => {
|
const velocity = hap.context?.velocity ?? 0.9;
|
||||||
const device = getDevice(output, outputs);
|
if (!isNote(note)) {
|
||||||
const otherOutputs = outputs.filter((o) => o.name !== device.name);
|
throw new Error('not a note: ' + note);
|
||||||
logger(
|
}
|
||||||
`Midi enabled! Using "${device.name}". ${
|
|
||||||
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onDisconnected: ({ outputs }) =>
|
|
||||||
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.onTrigger((time, hap, currentTime, cps) => {
|
|
||||||
if (!WebMidi.enabled) {
|
if (!WebMidi.enabled) {
|
||||||
return;
|
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||||
}
|
}
|
||||||
const device = getDevice(output, WebMidi.outputs);
|
if (!WebMidi.outputs.length) {
|
||||||
hap.ensureObjectValue();
|
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||||
|
|
||||||
const offset = (time - currentTime) * 1000;
|
|
||||||
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
|
|
||||||
const timeOffsetString = `+${offset}`;
|
|
||||||
|
|
||||||
// destructure value
|
|
||||||
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
|
|
||||||
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
|
|
||||||
|
|
||||||
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
|
|
||||||
const duration = Math.floor(hap.duration.valueOf() * 1000 - 10);
|
|
||||||
if (note != null) {
|
|
||||||
const midiNumber = typeof note === 'number' ? note : noteToMidi(note);
|
|
||||||
const midiNote = new Note(midiNumber, { attack: velocity, duration });
|
|
||||||
device.playNote(midiNote, midichan, {
|
|
||||||
time: timeOffsetString,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (ccv && ccn) {
|
let device;
|
||||||
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
if (typeof output === 'number') {
|
||||||
throw new Error('expected ccv to be a number between 0 and 1');
|
device = WebMidi.outputs[output];
|
||||||
}
|
} else if (typeof output === 'string') {
|
||||||
if (!['string', 'number'].includes(typeof ccn)) {
|
device = outputByName(output);
|
||||||
throw new Error('expected ccn to be a number or a string');
|
} else {
|
||||||
}
|
device = WebMidi.outputs[0];
|
||||||
const scaled = Math.round(ccv * 127);
|
|
||||||
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
|
|
||||||
}
|
}
|
||||||
|
if (!device) {
|
||||||
|
throw new Error(
|
||||||
|
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||||
|
.map((o) => `'${o.name}'`)
|
||||||
|
.join(' | ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// console.log('midi', value, output);
|
||||||
|
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
|
||||||
|
time = time * 1000 + timingOffset;
|
||||||
|
// const inMs = '+' + (time - Tone.getContext().currentTime) * 1000;
|
||||||
|
// await enableWebMidi()
|
||||||
|
device.playNote(note, channel, {
|
||||||
|
time,
|
||||||
|
duration: hap.duration.valueOf() * 1000 - 5,
|
||||||
|
attack: velocity,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+130
@@ -0,0 +1,130 @@
|
|||||||
|
{
|
||||||
|
"name": "@strudel.cycles/midi",
|
||||||
|
"version": "0.4.1",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@strudel.cycles/midi",
|
||||||
|
"version": "0.1.1",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"dependencies": {
|
||||||
|
"tone": "^14.7.77",
|
||||||
|
"webmidi": "^2.5.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.17.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz",
|
||||||
|
"integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==",
|
||||||
|
"dependencies": {
|
||||||
|
"regenerator-runtime": "^0.13.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/automation-events": {
|
||||||
|
"version": "4.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/automation-events/-/automation-events-4.0.14.tgz",
|
||||||
|
"integrity": "sha512-CB2Me0yW8sz7gSGwMiSfgfs1Oqlgs53k+eVESN6axvRyMAD3zlSp2nqndD2TQAtW3yOtSEJWNGsw0r48+f1wtw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.17.2",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.13.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
||||||
|
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
|
||||||
|
},
|
||||||
|
"node_modules/standardized-audio-context": {
|
||||||
|
"version": "25.3.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.21.tgz",
|
||||||
|
"integrity": "sha512-CZEnayJJjNefeU+z1QGDhaid1LAAYxWYa2ipNk75ropwec9rq6fmclyhXb1wGtDsZ402irX3HLt1U/PwP9+1fA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.17.2",
|
||||||
|
"automation-events": "^4.0.14",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tone": {
|
||||||
|
"version": "14.7.77",
|
||||||
|
"resolved": "https://registry.npmjs.org/tone/-/tone-14.7.77.tgz",
|
||||||
|
"integrity": "sha512-tCfK73IkLHyzoKUvGq47gyDyxiKLFvKiVCOobynGgBB9Dl0NkxTM2p+eRJXyCYrjJwy9Y0XCMqD3uOYsYt2Fdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"standardized-audio-context": "^25.1.8",
|
||||||
|
"tslib": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||||
|
},
|
||||||
|
"node_modules/webmidi": {
|
||||||
|
"version": "2.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/webmidi/-/webmidi-2.5.3.tgz",
|
||||||
|
"integrity": "sha512-PyMGvKcDGpvbQUfnmBORQJciyG3VAZ4aHlGy1iRZ3uEs4kG4HCvI7KRthUpM1vuHDPL98lidRIUaoRomkJtWtg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">0.6.x"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": {
|
||||||
|
"version": "7.17.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz",
|
||||||
|
"integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==",
|
||||||
|
"requires": {
|
||||||
|
"regenerator-runtime": "^0.13.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"automation-events": {
|
||||||
|
"version": "4.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/automation-events/-/automation-events-4.0.14.tgz",
|
||||||
|
"integrity": "sha512-CB2Me0yW8sz7gSGwMiSfgfs1Oqlgs53k+eVESN6axvRyMAD3zlSp2nqndD2TQAtW3yOtSEJWNGsw0r48+f1wtw==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.17.2",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"regenerator-runtime": {
|
||||||
|
"version": "0.13.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
||||||
|
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
|
||||||
|
},
|
||||||
|
"standardized-audio-context": {
|
||||||
|
"version": "25.3.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.21.tgz",
|
||||||
|
"integrity": "sha512-CZEnayJJjNefeU+z1QGDhaid1LAAYxWYa2ipNk75ropwec9rq6fmclyhXb1wGtDsZ402irX3HLt1U/PwP9+1fA==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.17.2",
|
||||||
|
"automation-events": "^4.0.14",
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tone": {
|
||||||
|
"version": "14.7.77",
|
||||||
|
"resolved": "https://registry.npmjs.org/tone/-/tone-14.7.77.tgz",
|
||||||
|
"integrity": "sha512-tCfK73IkLHyzoKUvGq47gyDyxiKLFvKiVCOobynGgBB9Dl0NkxTM2p+eRJXyCYrjJwy9Y0XCMqD3uOYsYt2Fdg==",
|
||||||
|
"requires": {
|
||||||
|
"standardized-audio-context": "^25.1.8",
|
||||||
|
"tslib": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tslib": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
|
||||||
|
},
|
||||||
|
"webmidi": {
|
||||||
|
"version": "2.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/webmidi/-/webmidi-2.5.3.tgz",
|
||||||
|
"integrity": "sha512-PyMGvKcDGpvbQUfnmBORQJciyG3VAZ4aHlGy1iRZ3uEs4kG4HCvI7KRthUpM1vuHDPL98lidRIUaoRomkJtWtg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/midi",
|
"name": "@strudel.cycles/midi",
|
||||||
"version": "0.9.0",
|
"version": "0.4.1",
|
||||||
"description": "Midi API for strudel",
|
"description": "Midi API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"module": "dist/index.mjs"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "vite build",
|
|
||||||
"prepublishOnly": "npm run build"
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||||
@@ -29,11 +21,8 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/tone": "^0.4.1",
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"tone": "^14.7.77",
|
||||||
"webmidi": "^3.1.5"
|
"webmidi": "^3.0.21"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"vite": "^4.3.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import { dependencies } from './package.json';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [],
|
|
||||||
build: {
|
|
||||||
lib: {
|
|
||||||
entry: resolve(__dirname, 'index.mjs'),
|
|
||||||
formats: ['es', 'cjs'],
|
|
||||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
external: [...Object.keys(dependencies)],
|
|
||||||
},
|
|
||||||
target: 'esnext',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -32,7 +32,7 @@ yields:
|
|||||||
|
|
||||||
## Mini Notation API
|
## Mini Notation API
|
||||||
|
|
||||||
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/learn/mini-notation)
|
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
|
||||||
|
|
||||||
## Building the Parser
|
## Building the Parser
|
||||||
|
|
||||||
@@ -40,5 +40,5 @@ The parser [krill-parser.js] is generated from [krill.pegjs](./krill.pegjs) usin
|
|||||||
To generate the parser, run
|
To generate the parser, run
|
||||||
|
|
||||||
```js
|
```js
|
||||||
npm build:parser
|
npm run build:parser
|
||||||
```
|
```
|
||||||
|
|||||||
+422
-547
File diff suppressed because it is too large
Load Diff
+59
-90
@@ -5,27 +5,17 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Some terminology:
|
// Some terminology:
|
||||||
// mini(notation) = a series of elements placed between quotes
|
// a sequence = a serie of elements placed between quotes
|
||||||
// a stack = a series of vertically aligned slices sharing the same overall length
|
// a stack = a serie of vertically aligned slices sharing the same overall length
|
||||||
// a sequence = a series of horizontally aligned elements
|
// a slice = a serie of horizontally aligned elements
|
||||||
// a choose = a series of elements, one of which is chosen at random
|
// a choose = a serie of elements, one of which is chosen at random
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
var AtomStub = function(source)
|
var PatternStub = function(source, alignment)
|
||||||
{
|
|
||||||
this.type_ = "atom";
|
|
||||||
this.source_ = source;
|
|
||||||
this.location_ = location();
|
|
||||||
}
|
|
||||||
|
|
||||||
var PatternStub = function(source, alignment, seed)
|
|
||||||
{
|
{
|
||||||
this.type_ = "pattern";
|
this.type_ = "pattern";
|
||||||
this.arguments_ = { alignment: alignment };
|
this.arguments_ = { alignment : alignment};
|
||||||
if (seed !== undefined) {
|
|
||||||
this.arguments_.seed = seed;
|
|
||||||
}
|
|
||||||
this.source_ = source;
|
this.source_ = source;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +41,6 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
this.options_ = options;
|
this.options_ = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
var seed = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start = statement
|
start = statement
|
||||||
@@ -100,103 +89,83 @@ quote = '"' / "'"
|
|||||||
// ------------------ steps and cycles ---------------------------
|
// ------------------ steps and cycles ---------------------------
|
||||||
|
|
||||||
// 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 new AtomStub(chars.join("")) }
|
step = ws chars:step_char+ ws { return 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 polymeter e.g. {1 2, 3 4 5}
|
// define a timeline e.g <1 3 [3 5]>. We simply defer to a stack and change the alignement
|
||||||
polymeter = ws "{" ws s:polymeter_stack ws "}" stepsPerCycle:polymeter_steps? ws
|
timeline = ws "<" ws sc:single_cycle ws ">" ws
|
||||||
{ s.arguments_.stepsPerCycle = stepsPerCycle ; return s; }
|
{ sc.arguments_.alignment = "t"; return sc;}
|
||||||
|
|
||||||
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 / polymeter / slow_sequence
|
slice = step / sub_cycle / timeline
|
||||||
|
|
||||||
// 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_op = op_weight / op_bjorklund / op_slow / op_fast / op_replicate / op_degrade / op_tail
|
slice_modifier = slice_weight / slice_bjorklund / slice_slow / slice_fast / slice_fixed_step / slice_replicate / slice_degrade
|
||||||
|
|
||||||
op_weight = "@" a:number
|
slice_weight = "@" a:number
|
||||||
{ return x => x.options_['weight'] = a }
|
{ return { weight: a} }
|
||||||
|
|
||||||
op_replicate = "!"a:number
|
slice_replicate = "!"a:number
|
||||||
{ return x => x.options_['reps'] = a }
|
{ return { replicate: a } }
|
||||||
|
|
||||||
op_bjorklund = "(" ws p:slice_with_ops ws comma ws s:slice_with_ops ws comma? ws r:slice_with_ops? ws ")"
|
slice_bjorklund = "(" ws p:number ws comma ws s:number ws comma? ws r:number? ws ")"
|
||||||
{ return x => x.options_['ops'].push({ type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r }}) }
|
{ return { operator : { type_: "bjorklund", arguments_ :{ pulse: p, step:s, rotation:r || 0 } } } }
|
||||||
|
|
||||||
op_slow = "/"a:slice
|
slice_slow = "/"a:number
|
||||||
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'slow' }}) }
|
{ return { operator : { type_: "stretch", arguments_ :{ amount:a } } } }
|
||||||
|
|
||||||
op_fast = "*"a:slice
|
slice_fast = "*"a:number
|
||||||
{ return x => x.options_['ops'].push({ type_: "stretch", arguments_ :{ amount:a, type: 'fast' }}) }
|
{ return { operator : { type_: "stretch", arguments_ :{ amount:"1/"+a } } } }
|
||||||
|
|
||||||
op_degrade = "?"a:number?
|
slice_fixed_step = "%"a:number
|
||||||
{ return x => x.options_['ops'].push({ type_: "degradeBy", arguments_ :{ amount:a, seed: seed++ } }) }
|
{ return { operator : { type_: "fixed-step", arguments_ :{ amount:a } } } }
|
||||||
|
|
||||||
op_tail = ":" s:slice
|
slice_degrade = "?"a:number?
|
||||||
{ return x => x.options_['ops'].push({ type_: "tail", arguments_ :{ element:s } }) }
|
{ 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_ops = s:slice ops:slice_op*
|
slice_with_modifier = s:slice o:slice_modifier?
|
||||||
{ const result = new ElementStub(s, {ops: [], weight: 1, reps: 1});
|
{ return new ElementStub(s, o);}
|
||||||
for (const op of ops) {
|
|
||||||
op(result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// a sequence is a combination of one or more successive slices (as an array)
|
// a single cycle is a combination of one or more successive slices (as an array). If we
|
||||||
sequence = s:(slice_with_ops)+
|
// have only one element, we skip the array and return the element itself
|
||||||
{ return new PatternStub(s, 'fastcat'); }
|
single_cycle = s:(slice_with_modifier)+
|
||||||
|
{ return new PatternStub(s,"h"); }
|
||||||
|
|
||||||
// a stack is a series of vertically aligned sequence, separated by a comma
|
// a stack is a serie of vertically aligned single cycles, separated by a comma
|
||||||
stack_tail = tail:(comma @sequence)+
|
stack_tail = tail:(comma @single_cycle)+
|
||||||
{ return { alignment: 'stack', list: tail }; }
|
{ return { alignment: 'v', list: tail }; }
|
||||||
|
|
||||||
// a choose is a series of pipe-separated sequence, one of which is
|
// a choose is a serie of pipe-separated single cycles, one of which is chosen
|
||||||
// chosen at random, each cycle
|
// at random each time through the pattern
|
||||||
choose_tail = tail:(pipe @sequence)+
|
choose_tail = tail:(pipe @single_cycle)+
|
||||||
{ return { alignment: 'rand', list: tail, seed: seed++ }; }
|
{ return { alignment: 'r', 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:sequence tail:(stack_tail / choose_tail)?
|
stack_or_choose = head:single_cycle tail:(stack_tail / choose_tail)?
|
||||||
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment, tail.seed); } else { return head; } }
|
{ if (tail && tail.list.length > 0) { return new PatternStub([head, ...tail.list], tail.alignment); } else { return head; } }
|
||||||
|
|
||||||
polymeter_stack = head:sequence tail:stack_tail?
|
// a sequence is a quoted stack
|
||||||
{ return new PatternStub(tail ? [head, ...tail.list] : [head], 'polymeter'); }
|
sequence = ws quote sc:stack_or_choose quote
|
||||||
|
|
||||||
|
|
||||||
// Mini-notation innards ends
|
|
||||||
// ---------->8---------->8---------->8---------->8---------->8----------
|
|
||||||
// Experimental haskellish parser begins
|
|
||||||
|
|
||||||
// mini-notation = a quoted stack
|
|
||||||
mini = ws quote ws sc:stack_or_choose ws 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:mini_or_operator
|
struct = "struct" ws s:sequence_or_operator
|
||||||
{ return { name: "struct", args: { mini:s }}}
|
{ return { name: "struct", args: { sequence: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: p, step:parseInt(s) }}}
|
{ return { name: "bjorklund", args :{ pulse: parseInt(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}}}
|
||||||
@@ -220,27 +189,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:mini_or_operator ss:(comma v:mini_or_operator { return v})* ws "]"
|
cat = "cat" ws "[" ws s:sequence_or_operator ss:(comma v:sequence_or_operator { return v})* ws "]"
|
||||||
{ ss.unshift(s); return new PatternStub(ss, 'slowcat'); }
|
{ ss.unshift(s); return new PatternStub(ss,"t"); }
|
||||||
|
|
||||||
// ------------------ high level mini ---------------------------
|
// ------------------ high level sequence ---------------------------
|
||||||
|
|
||||||
mini_or_group =
|
sequence_or_group =
|
||||||
group_operator /
|
group_operator /
|
||||||
mini
|
sequence
|
||||||
|
|
||||||
mini_or_operator =
|
sequence_or_operator =
|
||||||
sg:mini_or_group ws (comment)*
|
sg:sequence_or_group ws (comment)*
|
||||||
{return sg}
|
{return sg}
|
||||||
/ o:operator ws "$" ws soc:mini_or_operator
|
/ o:operator ws "$" ws soc:sequence_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: mini_or_operator
|
sc: sequence_or_operator
|
||||||
{ return sc }
|
{ return sc }
|
||||||
/ comment
|
/ comment
|
||||||
|
|
||||||
mini_definition = s:sequ_or_operator_or_comment
|
sequence_definition = s:sequ_or_operator_or_comment
|
||||||
|
|
||||||
// ---------------------- statements ----------------------------
|
// ---------------------- statements ----------------------------
|
||||||
|
|
||||||
@@ -258,4 +227,4 @@ hush = "hush"
|
|||||||
|
|
||||||
// ---------------------- statements ----------------------------
|
// ---------------------- statements ----------------------------
|
||||||
|
|
||||||
statement = mini_definition / command
|
statement = sequence_definition / command
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user