mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-16 07:36:55 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a42adc366 | |||
| cd3765b8fa | |||
| 787ba9872f | |||
| fd2c7fb4bb | |||
| 50c84a973c |
@@ -1,20 +0,0 @@
|
||||
krill-parser.js
|
||||
krill.pegjs
|
||||
.eslintrc.json
|
||||
server.js
|
||||
tidal-sniffer.js
|
||||
*.jsx
|
||||
tunejs.js
|
||||
out/**
|
||||
postcss.config.js
|
||||
postcss.config.cjs
|
||||
tailwind.config.js
|
||||
tailwind.config.cjs
|
||||
vite.config.js
|
||||
/**/dist/**/*
|
||||
!**/*.mjs
|
||||
**/*.tsx
|
||||
**/*.ts
|
||||
**/*.json
|
||||
**/dev-dist
|
||||
**/dist
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": ["eslint:recommended"],
|
||||
"overrides": [],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["import"],
|
||||
"rules": {
|
||||
"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 }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
node-version: 16
|
||||
cache: "npm"
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
run: npm ci && cd repl && npm ci && cd ../tutorial && npm ci
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
run: npm run build
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v2
|
||||
@@ -42,7 +39,7 @@ jobs:
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
# Upload entire repository
|
||||
path: "./website/dist"
|
||||
path: "./out"
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
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
|
||||
@@ -7,18 +7,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18]
|
||||
node-version: [16, 17]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
- run: pnpm install
|
||||
- run: pnpm run format-check
|
||||
- run: pnpm run lint
|
||||
- run: pnpm test
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
|
||||
+1
-7
@@ -37,10 +37,4 @@ talk/public/EmuSP12
|
||||
talk/public/samples
|
||||
server/samples/old
|
||||
repl/stats.html
|
||||
coverage
|
||||
public/icons/apple-splash-*
|
||||
dev-dist
|
||||
Dirt-Samples
|
||||
tidal-drum-machines
|
||||
webaudiofontdata
|
||||
src-tauri/target
|
||||
coverage
|
||||
@@ -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,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"proseWrap": "preserve",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
"printWidth": 120,
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"proseWrap": "preserve",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
|
||||
Vendored
+1
-2
@@ -5,6 +5,5 @@
|
||||
],
|
||||
"yaml.schemas": {
|
||||
"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
|
||||
|
||||
If you have any questions about strudel, make sure you've glanced through the
|
||||
[docs](https://strudel.tidalcycles.org/learn/) to find out if it answers your question.
|
||||
If you have any questions about strudel, make sure you've read the
|
||||
[tutorial](https://strudel.tidalcycles.org/tutorial/) to find out if it answers your question.
|
||||
If not, use one of the Communication Channels above!
|
||||
|
||||
Don't be afraid to ask! Your question might be of great value for other people too.
|
||||
@@ -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.
|
||||
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/),
|
||||
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
|
||||
If you find some weak spots in the [tutorial](https://strudel.tidalcycles.org/),
|
||||
you are welcome to improve them by editing [this file](https://github.com/tidalcycles/strudel/blob/main/tutorial/tutorial.mdx).
|
||||
|
||||
This will even work without setting up a development environment, only a github account is required.
|
||||
|
||||
## Propose a Feature
|
||||
|
||||
@@ -58,22 +60,23 @@ To fix a bug that has been reported,
|
||||
## Write Tests
|
||||
|
||||
There are still many tests that have not been written yet! Reading and writing tests is a great opportunity to get familiar with the codebase.
|
||||
You can find the tests in each package in the `test` folder. To run all tests, run `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
|
||||
|
||||
To get the project up and running for development, make sure you have installed:
|
||||
|
||||
- [git](https://git-scm.com/)
|
||||
- [node](https://nodejs.org/en/) >= 18
|
||||
- [pnpm](https://pnpm.io/) (`npm i pnpm -g`)
|
||||
- git
|
||||
- node, preferably v16
|
||||
|
||||
then, do the following:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/tidalcycles/strudel.git && cd strudel
|
||||
pnpm i # install at root to symlink packages
|
||||
pnpm start # start repl
|
||||
npm i # install at root to symlink packages
|
||||
npx lerna bootstrap # install all dependencies in packages
|
||||
cd repl && npm i # install repl dependencies
|
||||
npm run start # start repl
|
||||
```
|
||||
|
||||
Those commands might look slightly different for your OS.
|
||||
@@ -82,10 +85,6 @@ Please report any problems you've had with the setup instructions!
|
||||
## Code Style
|
||||
|
||||
To make sure the code changes only where it should, we are using prettier to unify the code style.
|
||||
|
||||
- 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
|
||||
|
||||
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..."
|
||||
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
|
||||
|
||||
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,
|
||||
allowing to develop multiple packages at the same time.
|
||||
When you run `npm i` on the root folder, [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces) will symlink all packages
|
||||
in the `node_modules` folder. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
|
||||
which allows developing multiple packages at the same time
|
||||
|
||||
## Package Publishing
|
||||
|
||||
@@ -123,25 +105,15 @@ To publish all packages that have been changed since the last release, run:
|
||||
|
||||
```sh
|
||||
npm login
|
||||
|
||||
# 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
|
||||
npx lerna publish
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
To add a new package, you have to publish it manually the first time, using:
|
||||
|
||||
```sh
|
||||
cd packages/<package-name> && pnpm publish --access public
|
||||
cd packages/<package-name> && npm publish --access public
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
- 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>
|
||||
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
||||
|
||||
## Running Locally
|
||||
|
||||
After cloning the project, you can run the REPL locally:
|
||||
|
||||
```bash
|
||||
pnpm run setup
|
||||
pnpm run repl
|
||||
npm run setup
|
||||
npm run repl
|
||||
```
|
||||
|
||||
## Using Strudel In Your Project
|
||||
|
||||
@@ -1,17 +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/eval/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/tone/index.mjs';
|
||||
export * from './packages/transpiler/index.mjs';
|
||||
export * from './packages/webaudio/index.mjs';
|
||||
export * from './packages/webdirt/index.mjs';
|
||||
export * from './packages/xen/index.mjs';
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"source": {
|
||||
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
|
||||
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
|
||||
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser"
|
||||
},
|
||||
"plugins": ["plugins/markdown"],
|
||||
"opts": {
|
||||
|
||||
+1
-3
@@ -2,7 +2,5 @@
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "independent",
|
||||
"npmClient": "pnpm",
|
||||
"useWorkspaces": true
|
||||
"version": "independent"
|
||||
}
|
||||
|
||||
@@ -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
+22498
File diff suppressed because it is too large
Load Diff
+23
-41
@@ -1,32 +1,26 @@
|
||||
{
|
||||
"name": "@strudel.cycles/monorepo",
|
||||
"version": "0.5.0",
|
||||
"version": "0.0.4",
|
||||
"private": true,
|
||||
"description": "Port of tidalcycles to javascript",
|
||||
"scripts": {
|
||||
"setup": "pnpm i",
|
||||
"pretest": "npm run jsdoc-json",
|
||||
"prebuild": "npm run jsdoc-json",
|
||||
"prestart": "npm run jsdoc-json",
|
||||
"test": "npm run pretest && vitest run --version",
|
||||
"test-ui": "npm run pretest && vitest --ui",
|
||||
"test-coverage": "npm run pretest && vitest --coverage",
|
||||
"snapshot": "npm run pretest && vitest run -u --silent",
|
||||
"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",
|
||||
"test": "vitest run --version",
|
||||
"test-ui": "vitest --ui",
|
||||
"test-coverage": "vitest --coverage",
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i",
|
||||
"snapshot": "cd repl && npm run snapshot",
|
||||
"repl": "cd repl && npm run dev",
|
||||
"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-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
|
||||
"lint": "eslint . --ext mjs,js --quiet",
|
||||
"codeformat": "prettier --write .",
|
||||
"format-check": "prettier --check .",
|
||||
"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"
|
||||
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
@@ -44,29 +38,17 @@
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"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": {
|
||||
"@tauri-apps/cli": "^1.4.0",
|
||||
"@vitest/ui": "^0.28.0",
|
||||
"canvas": "^2.11.2",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"@vitest/ui": "^0.21.1",
|
||||
"c8": "^7.12.0",
|
||||
"events": "^3.3.0",
|
||||
"jsdoc": "^4.0.2",
|
||||
"gh-pages": "^4.0.0",
|
||||
"happy-dom": "^6.0.4",
|
||||
"jsdoc": "^3.6.10",
|
||||
"jsdoc-json": "^2.0.2",
|
||||
"jsdoc-to-markdown": "^8.0.0",
|
||||
"lerna": "^6.6.1",
|
||||
"prettier": "^2.8.8",
|
||||
"jsdoc-to-markdown": "^7.1.1",
|
||||
"lerna": "^4.0.0",
|
||||
"rollup-plugin-visualizer": "^5.8.1",
|
||||
"vitest": "^0.28.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,199 +0,0 @@
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { EditorView, keymap, Decoration, lineNumbers, highlightActiveLineGutter } from '@codemirror/view';
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { StateField, StateEffect } from '@codemirror/state';
|
||||
import { oneDark } from './themes/one-dark';
|
||||
import { repl, Drawer } from '@strudel.cycles/core';
|
||||
|
||||
// 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(),
|
||||
highlightField,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// highlighting
|
||||
//
|
||||
|
||||
export const setHighlights = StateEffect.define();
|
||||
export const highlightField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(highlights, tr) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setHighlights)) {
|
||||
const { haps } = e.value;
|
||||
const marks =
|
||||
haps
|
||||
.map((hap) =>
|
||||
(hap.context.locations || []).map(({ start, end }) => {
|
||||
// const color = hap.context.color || e.value.color || '#FFCA28';
|
||||
let from = tr.newDoc.line(start.line).from + start.column;
|
||||
let to = tr.newDoc.line(end.line).from + end.column;
|
||||
const l = tr.newDoc.length;
|
||||
if (from > l || to > l) {
|
||||
return; // dont mark outside of range, as it will throw an error
|
||||
}
|
||||
const mark = Decoration.mark({
|
||||
attributes: { style: `outline: 2px solid #FFCA28;` },
|
||||
});
|
||||
return mark.range(from, to);
|
||||
}),
|
||||
)
|
||||
.flat()
|
||||
.filter(Boolean) || [];
|
||||
highlights = Decoration.set(marks, true);
|
||||
}
|
||||
}
|
||||
return highlights;
|
||||
} catch (err) {
|
||||
// console.warn('highlighting error', err);
|
||||
return Decoration.set([]);
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
// helper to simply trigger highlighting for given haps
|
||||
export function highlightHaps(view, haps) {
|
||||
view.dispatch({ effects: setHighlights.of({ haps }) });
|
||||
}
|
||||
|
||||
//
|
||||
// flash
|
||||
//
|
||||
|
||||
export const setFlash = StateEffect.define();
|
||||
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) {
|
||||
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);
|
||||
};
|
||||
|
||||
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);
|
||||
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) {
|
||||
highlightHaps(this.editor, haps);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"name": "@strudel/codemirror",
|
||||
"version": "0.8.4",
|
||||
"description": "Codemirror Extensions for Strudel",
|
||||
"main": "codemirror.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, 'codemirror.mjs'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...Object.keys(dependencies)],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
examples
|
||||
@@ -33,7 +33,6 @@ b: 3/2 - 7/4
|
||||
c: 7/4 - 2
|
||||
```
|
||||
|
||||
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-forked-9ywhv7?file=/src/index.js).
|
||||
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/canvas.html)
|
||||
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/vanilla.html)
|
||||
- [open minimal vite example](./examples/vite-vanilla-repl/)
|
||||
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-qmz6qr?file=/src/index.js).
|
||||
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/package-examples/packages/core/examples/canvas.html)
|
||||
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/package-examples/packages/core/examples/metro.html)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
+360
-401
File diff suppressed because it is too large
Load Diff
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
cyclist.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/cyclist.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 createClock from './zyklus.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
export class Cyclist {
|
||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||
this.started = false;
|
||||
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.latency = latency; // fixed trigger time offset
|
||||
const round = (x) => Math.round(x * 1000) / 1000;
|
||||
this.clock = createClock(
|
||||
getTime,
|
||||
// called slightly before each cycle
|
||||
(phase, duration, tick) => {
|
||||
if (tick === 0) {
|
||||
this.origin = phase;
|
||||
}
|
||||
try {
|
||||
const time = getTime();
|
||||
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) => {
|
||||
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
|
||||
const duration = hap.duration / this.cps;
|
||||
onTrigger?.(hap, deadline, duration, this.cps);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger(`[cyclist] error: ${e.message}`);
|
||||
onError?.(e);
|
||||
}
|
||||
},
|
||||
interval, // duration of each cycle
|
||||
);
|
||||
}
|
||||
now() {
|
||||
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
|
||||
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
|
||||
}
|
||||
setStarted(v) {
|
||||
this.started = v;
|
||||
this.onToggle?.(v);
|
||||
}
|
||||
start() {
|
||||
if (!this.pattern) {
|
||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||
}
|
||||
logger('[cyclist] start');
|
||||
this.clock.start();
|
||||
this.setStarted(true);
|
||||
}
|
||||
pause() {
|
||||
logger('[cyclist] pause');
|
||||
this.clock.pause();
|
||||
this.setStarted(false);
|
||||
}
|
||||
stop() {
|
||||
logger('[cyclist] stop');
|
||||
this.clock.stop();
|
||||
this.lastEnd = 0;
|
||||
this.setStarted(false);
|
||||
}
|
||||
setPattern(pat, autostart = false) {
|
||||
this.pattern = pat;
|
||||
if (autostart && !this.started) {
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
setCps(cps = 1) {
|
||||
this.cps = cps;
|
||||
}
|
||||
log(begin, end, haps) {
|
||||
const onsets = haps.filter((h) => h.hasOnset());
|
||||
console.log(`${begin.toFixed(4)} - ${end.toFixed(4)} ${Array(onsets.length).fill('I').join('')}`);
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
/*
|
||||
draw.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/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/>.
|
||||
*/
|
||||
|
||||
import { Pattern, getTime, State, TimeSpan } from './index.mjs';
|
||||
|
||||
export const getDrawContext = (id = 'test-canvas') => {
|
||||
let canvas = document.querySelector('#' + id);
|
||||
if (!canvas) {
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.id = id;
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
|
||||
document.body.prepend(canvas);
|
||||
}
|
||||
return canvas.getContext('2d');
|
||||
};
|
||||
|
||||
Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
||||
if (window.strudelAnimation) {
|
||||
cancelAnimationFrame(window.strudelAnimation);
|
||||
}
|
||||
const ctx = getDrawContext();
|
||||
let cycle,
|
||||
events = [];
|
||||
const animate = (time) => {
|
||||
const t = getTime();
|
||||
if (from !== undefined && to !== undefined) {
|
||||
const currentCycle = Math.floor(t);
|
||||
if (cycle !== currentCycle) {
|
||||
cycle = currentCycle;
|
||||
const begin = currentCycle + from;
|
||||
const end = currentCycle + to;
|
||||
setTimeout(() => {
|
||||
events = this.query(new State(new TimeSpan(begin, end)))
|
||||
.filter(Boolean)
|
||||
.filter((event) => event.part.begin.equals(event.whole.begin));
|
||||
onQuery?.(events);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
callback(ctx, events, t, time);
|
||||
window.strudelAnimation = requestAnimationFrame(animate);
|
||||
};
|
||||
requestAnimationFrame(animate);
|
||||
return this;
|
||||
};
|
||||
|
||||
export const cleanupDraw = (clearScreen = true) => {
|
||||
const ctx = getDrawContext();
|
||||
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||
if (window.strudelAnimation) {
|
||||
cancelAnimationFrame(window.strudelAnimation);
|
||||
}
|
||||
if (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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import Fraction, { gcd } from './fraction.mjs';
|
||||
* @example
|
||||
* const line = drawLine("0 [1 2 3]", 10); // |0--123|0--123
|
||||
* console.log(line);
|
||||
* silence;
|
||||
*/
|
||||
function drawLine(pat, chars = 60) {
|
||||
let cycle = 0;
|
||||
|
||||
+29
-96
@@ -1,91 +1,47 @@
|
||||
/*
|
||||
euclid.mjs - Bjorklund/Euclidean/Diaspora rhythms
|
||||
Copyright (C) 2023 Rohan Drape and strudel contributors
|
||||
|
||||
See <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.mjs> for authors of this file.
|
||||
|
||||
The Bjorklund algorithm implementation is ported from the Haskell Music Theory Haskell module by Rohan Drape -
|
||||
https://rohandrape.net/?t=hmt
|
||||
|
||||
euclid.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/euclid.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 { Pattern, timeCat, register, silence } from './pattern.mjs';
|
||||
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
|
||||
import { Pattern, timeCat } from './pattern.mjs';
|
||||
import bjork from 'bjork';
|
||||
import { rotate } from './util.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
|
||||
const left = function (n, x) {
|
||||
const [ons, offs] = n;
|
||||
const [xs, ys] = x;
|
||||
const [_xs, __xs] = splitAt(offs, xs);
|
||||
return [
|
||||
[offs, ons - offs],
|
||||
[zipWith((a, b) => a.concat(b), _xs, ys), __xs],
|
||||
];
|
||||
};
|
||||
|
||||
const right = function (n, x) {
|
||||
const [ons, offs] = n;
|
||||
const [xs, ys] = x;
|
||||
const [_ys, __ys] = splitAt(ons, ys);
|
||||
const result = [
|
||||
[ons, offs - ons],
|
||||
[zipWith((a, b) => a.concat(b), xs, _ys), __ys],
|
||||
];
|
||||
return result;
|
||||
};
|
||||
|
||||
const _bjork = function (n, x) {
|
||||
const [ons, offs] = n;
|
||||
return Math.min(ons, offs) <= 1 ? [n, x] : _bjork(...(ons > offs ? left(n, x) : right(n, x)));
|
||||
};
|
||||
|
||||
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]));
|
||||
const euclid = (pulses, steps, rotation = 0) => {
|
||||
const b = bjork(steps, pulses);
|
||||
if (rotation) {
|
||||
return rotate(b, -rotation);
|
||||
}
|
||||
return b;
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes the structure of the pattern to form an euclidean rhythm.
|
||||
* Euclidian rhythms are rhythms obtained using the greatest common
|
||||
* divisor of two numbers. They were described in 2004 by Godfried
|
||||
* Toussaint, a canadian computer scientist. Euclidian rhythms are
|
||||
* really useful for computer/algorithmic music because they can
|
||||
* describe a large number of rhythms with a couple of numbers.
|
||||
* Euclidian rhythms are rhythms obtained using the greatest common divisor of two numbers.
|
||||
* They were described in 2004 by Godfried Toussaint, a canadian computer scientist.
|
||||
* Euclidian rhythms are really useful for computer/algorithmic music because they can accurately
|
||||
* describe a large number of rhythms used in the most important music world traditions.
|
||||
*
|
||||
* @memberof Pattern
|
||||
* @name euclid
|
||||
* @param {number} pulses the number of onsets / beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param {number} rotation (optional) offset in steps
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* // The Cuban tresillo pattern.
|
||||
* 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.
|
||||
* note("c3").euclid(2,5)
|
||||
* @example // The archetypal pattern of the Cumbia from Colombia, as well as a Calypso rhythm from Trinidad.
|
||||
* note("c3").euclid(3,4)
|
||||
* @example // Another thirteenth century Persian rhythm by the name of Khafif-e-ramal, as well as a Rumanian folk-dance rhythm.
|
||||
* note("c3").euclidRot(3,5,2)
|
||||
* note("c3").euclid(3,5,2)
|
||||
* @example // A Ruchenitza rhythm used in a Bulgarian folk-dance.
|
||||
* note("c3").euclid(3,7)
|
||||
* @example // The Cuban tresillo pattern.
|
||||
@@ -115,57 +71,34 @@ export const bjork = function (ons, steps) {
|
||||
* @example // A common West African bell pattern.
|
||||
* note("c3").euclid(7,12)
|
||||
* @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.
|
||||
* note("c3").euclid(9,16)
|
||||
* @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.
|
||||
* note("c3").euclidRot(13,24,5)
|
||||
* note("c3").euclid(13,24,5)
|
||||
*/
|
||||
|
||||
const _euclidRot = function (pulses, steps, rotation) {
|
||||
const b = bjork(pulses, steps);
|
||||
if (rotation) {
|
||||
return rotate(b, -rotation);
|
||||
}
|
||||
return b;
|
||||
Pattern.prototype.euclid = function (pulses, steps, rotation = 0) {
|
||||
return this.struct(euclid(pulses, steps, rotation));
|
||||
};
|
||||
|
||||
export const euclid = register('euclid', function (pulses, steps, pat) {
|
||||
return pat.struct(_euclidRot(pulses, steps, 0));
|
||||
});
|
||||
|
||||
export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], function (pulses, steps, rotation, pat) {
|
||||
return pat.struct(_euclidRot(pulses, steps, rotation));
|
||||
});
|
||||
|
||||
/**
|
||||
* Similar to `euclid`, but each pulse is held until the next pulse,
|
||||
* so there will be no gaps.
|
||||
* Similar to `.euclid`, but each pulse is held until the next pulse, so there will be no gaps.
|
||||
* @name euclidLegato
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
|
||||
*/
|
||||
|
||||
const _euclidLegato = function (pulses, steps, rotation, pat) {
|
||||
if (pulses < 1) {
|
||||
return silence;
|
||||
}
|
||||
const bin_pat = _euclidRot(pulses, steps, rotation);
|
||||
const gapless = bin_pat
|
||||
Pattern.prototype.euclidLegato = function (pulses, steps, rotation = 0) {
|
||||
const bin_pat = euclid(pulses, steps, rotation);
|
||||
const firstOne = bin_pat.indexOf(1);
|
||||
const gapless = rotate(bin_pat, firstOne)
|
||||
.join('')
|
||||
.split('1')
|
||||
.slice(1)
|
||||
.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) {
|
||||
return _euclidLegato(pulses, steps, 0, pat);
|
||||
});
|
||||
|
||||
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
||||
return _euclidLegato(pulses, steps, rotation, pat);
|
||||
});
|
||||
export default euclid;
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
evaluate.mjs - <short description TODO>
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { isPattern } from './index.mjs';
|
||||
|
||||
export const evalScope = async (...args) => {
|
||||
const results = await Promise.allSettled(args);
|
||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||
results.forEach((result, i) => {
|
||||
if (result.status === 'rejected') {
|
||||
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
|
||||
}
|
||||
});
|
||||
// Object.assign(globalThis, ...modules);
|
||||
// below is a fix for above commented out line
|
||||
// same error as https://github.com/vitest-dev/vitest/issues/1807 when running this on astro server
|
||||
modules.forEach((module) => {
|
||||
Object.entries(module).forEach(([name, value]) => {
|
||||
globalThis[name] = value;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function safeEval(str, options = {}) {
|
||||
const { wrapExpression = true, wrapAsync = true } = options;
|
||||
if (wrapExpression) {
|
||||
str = `{${str}}`;
|
||||
}
|
||||
if (wrapAsync) {
|
||||
str = `(async ()=>${str})()`;
|
||||
}
|
||||
const body = `"use strict";return (${str})`;
|
||||
return Function(body)();
|
||||
}
|
||||
|
||||
export const evaluate = async (code, transpiler) => {
|
||||
if (transpiler) {
|
||||
code = transpiler(code); // transform syntactically correct js code to semantically usable code
|
||||
}
|
||||
// if no transpiler is given, we expect a single instruction (!wrapExpression)
|
||||
const options = { wrapExpression: !!transpiler };
|
||||
let evaluated = await safeEval(code, options);
|
||||
if (!isPattern(evaluated)) {
|
||||
console.log('evaluated', evaluated);
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||
}
|
||||
return { mode: 'javascript', pattern: evaluated };
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
<input
|
||||
type="text"
|
||||
id="text"
|
||||
value="seq('a', ['b', 'c'])"
|
||||
value="cat('a', 'b')"
|
||||
style="width: 100%; font-size: 2em; outline: none; margin-bottom: 10px"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<div id="output"></div>
|
||||
<script type="module">
|
||||
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.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
|
||||
const input = document.getElementById('text');
|
||||
const getEvents = () => {
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<input
|
||||
type="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"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<canvas id="canvas"></canvas>
|
||||
<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
|
||||
Object.assign(window, strudel);
|
||||
// setup elements
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
<!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; grid-template-rows: 32px auto">
|
||||
<button id="start" style="width: 100vw; height: 32px">evaluate</button>
|
||||
<textarea
|
||||
id="text"
|
||||
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
</div>
|
||||
<div id="output"></div>
|
||||
<script type="module">
|
||||
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.6.8';
|
||||
import { mini } from 'https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0';
|
||||
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.6.0';
|
||||
import {
|
||||
getAudioContext,
|
||||
webaudioOutput,
|
||||
initAudioOnFirstClick,
|
||||
} from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0';
|
||||
|
||||
initAudioOnFirstClick();
|
||||
const ctx = getAudioContext();
|
||||
const input = document.getElementById('text');
|
||||
input.innerHTML = getTune();
|
||||
|
||||
evalScope(
|
||||
controls,
|
||||
import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8'),
|
||||
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0'),
|
||||
import('https://cdn.skypack.dev/@strudel.cycles/tonal@0.6.0'),
|
||||
import('https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0'),
|
||||
);
|
||||
|
||||
const { evaluate } = repl({
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime: () => ctx.currentTime,
|
||||
transpiler,
|
||||
});
|
||||
document.getElementById('start').addEventListener('click', () => evaluate(input.value));
|
||||
|
||||
function getTune() {
|
||||
return `await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
|
||||
stack(
|
||||
// amen
|
||||
n("0 1 2 3 4 5 6 7")
|
||||
.sometimes(x=>x.ply(2))
|
||||
.rarely(x=>x.speed("2 | -2"))
|
||||
.sometimesBy(.4, x=>x.delay(".5"))
|
||||
.s("amencutup")
|
||||
.slow(2)
|
||||
.room(.5)
|
||||
,
|
||||
// bass
|
||||
sine.add(saw.slow(4)).range(0,7).segment(8)
|
||||
.superimpose(x=>x.add(.1))
|
||||
.scale('G0 minor').note()
|
||||
.s("sawtooth").decay(.1).sustain(0)
|
||||
.gain(.4).cutoff(perlin.range(300,3000).slow(8)).resonance(10)
|
||||
.degradeBy("0 0.1 .5 .1")
|
||||
.rarely(add(note("12")))
|
||||
,
|
||||
// chord
|
||||
note("Bb3,D4".superimpose(x=>x.add(.2)))
|
||||
.s('sawtooth').cutoff(1000).struct("<~@3 [~ x]>")
|
||||
.decay(.05).sustain(.0).delay(.8).delaytime(.125).room(.8)
|
||||
,
|
||||
// alien
|
||||
s("breath").room(1).shape(.6).chop(16).rev().mask("<x ~@7>")
|
||||
,
|
||||
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
|
||||
).reset("<x@7 x(5,8)>")`;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
|
||||
)`;
|
||||
@@ -1 +0,0 @@
|
||||
!dist
|
||||
@@ -1,8 +0,0 @@
|
||||
# vite-vanilla-repl
|
||||
|
||||
This folder demonstrates how to set up a strudel repl using vite and vanilla JS. Run it using:
|
||||
|
||||
```sh
|
||||
npm i
|
||||
npm run dev
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
import{b as s,h as i,m,a as n,c as p,p as t}from"./index.4cbc0a10.js";export{s as SyntaxError,i as h,m as mini,n as minify,p as parse,t as patternifyAST};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{g as s,f as d,i as t,n as r,l as u,j as o,d as f,k as p,r as g,s as i,w as l,e as m}from"./index.4cbc0a10.js";export{s as getAudioContext,d as getCachedBuffer,t as getLoadedBuffer,r as getLoadedSamples,u as loadBuffer,o as loadGithubSamples,f as panic,p as resetLoadedSamples,g as reverseBuffer,i as samples,l as webaudioOutput,m as webaudioOutputTrigger};
|
||||
@@ -1,36 +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>
|
||||
<script type="module" crossorigin src="/tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/assets/index.4cbc0a10.js"></script>
|
||||
</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"
|
||||
></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>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,35 +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 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"
|
||||
></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" src="./main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,28 +0,0 @@
|
||||
import { controls, repl, evalScope } from '@strudel.cycles/core';
|
||||
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
||||
import { transpiler } from '@strudel.cycles/transpiler';
|
||||
import tune from './tune.mjs';
|
||||
|
||||
const ctx = getAudioContext();
|
||||
const input = document.getElementById('text');
|
||||
input.innerHTML = tune;
|
||||
initAudioOnFirstClick();
|
||||
|
||||
evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
);
|
||||
|
||||
const { evaluate } = repl({
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime: () => ctx.currentTime,
|
||||
transpiler,
|
||||
});
|
||||
document.getElementById('start').addEventListener('click', () => {
|
||||
ctx.resume();
|
||||
console.log('eval', input.value);
|
||||
evaluate(input.value);
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "vite-vanilla-repl",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build-githack": "vite build --base /tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/mini": "workspace:*",
|
||||
"@strudel.cycles/transpiler": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"@strudel.cycles/tonal": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
export default `await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
|
||||
stack(
|
||||
// amen
|
||||
n("0 1 2 3 4 5 6 7")
|
||||
.sometimes(x=>x.ply(2))
|
||||
.rarely(x=>x.speed("2 | -2"))
|
||||
.sometimesBy(.4, x=>x.delay(".5"))
|
||||
.s("amencutup")
|
||||
.slow(2)
|
||||
.room(.5)
|
||||
,
|
||||
// bass
|
||||
sine.add(saw.slow(4)).range(0,7).segment(8)
|
||||
.superimpose(x=>x.add(.1))
|
||||
.scale('G0 minor').note()
|
||||
.s("sawtooth").decay(.1).sustain(0)
|
||||
.gain(.4).cutoff(perlin.range(300,3000).slow(8)).resonance(10)
|
||||
.degradeBy("0 0.1 .5 .1")
|
||||
.rarely(add(note("12")))
|
||||
,
|
||||
// chord
|
||||
note("Bb3,D4".superimpose(x=>x.add(.2)))
|
||||
.s('sawtooth').cutoff(1000).struct("<~@3 [~ x]>")
|
||||
.decay(.05).sustain(.0).delay(.8).delaytime(.125).room(.8)
|
||||
,
|
||||
// alien
|
||||
s("breath").room(1).shape(.6).chop(16).rev().mask("<x ~@7>")
|
||||
,
|
||||
n("0 1").s("east").delay(.5).degradeBy(.8).speed(rand.range(.5,1.5))
|
||||
).reset("<x@7 x(5,8)>")`;
|
||||
@@ -55,8 +55,7 @@ Fraction.prototype.min = function (other) {
|
||||
return this.lt(other) ? this : other;
|
||||
};
|
||||
|
||||
Fraction.prototype.show = function (/* excludeWhole = false */) {
|
||||
// return this.toFraction(excludeWhole);
|
||||
Fraction.prototype.show = function () {
|
||||
return this.s * this.n + '/' + this.d;
|
||||
};
|
||||
|
||||
@@ -74,7 +73,7 @@ const fraction = (n) => {
|
||||
-> those farey sequences turn out to make pattern querying ~20 times slower! always use strings!
|
||||
-> still, some optimizations could be done: .mul .div .add .sub calls still use numbers
|
||||
*/
|
||||
// n = String(n); // this is actually faster but imprecise...
|
||||
n = String(n);
|
||||
}
|
||||
return Fraction(n);
|
||||
};
|
||||
|
||||
+7
-51
@@ -32,11 +32,7 @@ export class Hap {
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
|
||||
}
|
||||
|
||||
get endClipped() {
|
||||
return this.whole.begin.add(this.duration);
|
||||
return this.whole.end.sub(this.whole.begin);
|
||||
}
|
||||
|
||||
wholeOrPart() {
|
||||
@@ -83,42 +79,15 @@ export class Hap {
|
||||
);
|
||||
}
|
||||
|
||||
show(compact = false) {
|
||||
const value =
|
||||
typeof this.value === 'object'
|
||||
? compact
|
||||
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||
: JSON.stringify(this.value)
|
||||
: this.value;
|
||||
var spans = '';
|
||||
if (this.whole == undefined) {
|
||||
spans = '~' + this.part.show;
|
||||
} else {
|
||||
var is_whole = this.whole.begin.equals(this.part.begin) && this.whole.end.equals(this.part.end);
|
||||
if (!this.whole.begin.equals(this.part.begin)) {
|
||||
spans = this.whole.begin.show() + ' ⇜ ';
|
||||
}
|
||||
if (!is_whole) {
|
||||
spans += '(';
|
||||
}
|
||||
spans += this.part.show();
|
||||
if (!is_whole) {
|
||||
spans += ')';
|
||||
}
|
||||
if (!this.whole.end.equals(this.part.end)) {
|
||||
spans += ' ⇝ ' + this.whole.end.show();
|
||||
}
|
||||
}
|
||||
return '[ ' + spans + ' | ' + value + ' ]';
|
||||
show() {
|
||||
return (
|
||||
'(' + (this.whole == undefined ? '~' : this.whole.show()) + ', ' + this.part.show() + ', ' + this.value + ')'
|
||||
);
|
||||
}
|
||||
|
||||
showWhole(compact = false) {
|
||||
showWhole() {
|
||||
return `${this.whole == undefined ? '~' : this.whole.show()}: ${
|
||||
typeof this.value === 'object'
|
||||
? compact
|
||||
? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ')
|
||||
: JSON.stringify(this.value)
|
||||
: this.value
|
||||
typeof this.value === 'object' ? JSON.stringify(this.value) : this.value
|
||||
}`;
|
||||
}
|
||||
|
||||
@@ -130,19 +99,6 @@ export class Hap {
|
||||
setContext(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;
|
||||
|
||||
+6
-15
@@ -4,11 +4,10 @@ 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/>.
|
||||
*/
|
||||
|
||||
import controls from './controls.mjs';
|
||||
export * from './controls.mjs';
|
||||
export * from './euclid.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
export { Fraction, controls };
|
||||
export { Fraction };
|
||||
export * from './hap.mjs';
|
||||
export * from './pattern.mjs';
|
||||
export * from './signal.mjs';
|
||||
@@ -16,22 +15,14 @@ export * from './state.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './util.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * from './evaluate.mjs';
|
||||
export * from './repl.mjs';
|
||||
export * from './cyclist.mjs';
|
||||
export * from './logger.mjs';
|
||||
export * from './time.mjs';
|
||||
export * from './draw.mjs';
|
||||
export * from './animate.mjs';
|
||||
export * from './pianoroll.mjs';
|
||||
export * from './spiral.mjs';
|
||||
export * from './ui.mjs';
|
||||
export { default as drawLine } from './drawLine.mjs';
|
||||
export { default as gist } from './gist.js';
|
||||
// below won't work with runtime.mjs (json import fails)
|
||||
/* import * as p from './package.json';
|
||||
export const version = p.version; */
|
||||
logger('🌀 @strudel.cycles/core loaded 🌀');
|
||||
console.log(
|
||||
'%c // 🌀 @strudel.cycles/core loaded 🌀', // keep "//" for runnable snapshot source..
|
||||
'background-color: black;color:white;padding:4px;border-radius:15px',
|
||||
);
|
||||
if (globalThis._strudelLoaded) {
|
||||
console.warn(
|
||||
`@strudel.cycles/core was loaded more than once...
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
export const logKey = 'strudel.log';
|
||||
|
||||
export function logger(message, type, data = {}) {
|
||||
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
||||
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(logKey, {
|
||||
detail: {
|
||||
message,
|
||||
type,
|
||||
data,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.key = logKey;
|
||||
Generated
+1639
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,11 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.8.2",
|
||||
"version": "0.2.0",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "pnpm build"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -31,11 +25,8 @@
|
||||
},
|
||||
"homepage": "https://strudel.tidalcycles.org",
|
||||
"dependencies": {
|
||||
"bjork": "^0.0.1",
|
||||
"fraction.js": "^4.2.0"
|
||||
},
|
||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.28.0"
|
||||
}
|
||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2"
|
||||
}
|
||||
|
||||
+1264
-1703
File diff suppressed because it is too large
Load Diff
@@ -1,328 +0,0 @@
|
||||
/*
|
||||
pianoroll.mjs - <short description TODO>
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
||||
|
||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||
const getValue = (e) => {
|
||||
let { value } = e;
|
||||
if (typeof e.value !== 'object') {
|
||||
value = { 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;
|
||||
};
|
||||
|
||||
Pattern.prototype.pianoroll = function ({
|
||||
cycles = 4,
|
||||
playhead = 0.5,
|
||||
overscan = 1,
|
||||
flipTime = 0,
|
||||
flipValues = 0,
|
||||
hideNegative = false,
|
||||
// inactive = '#C9E597',
|
||||
// inactive = '#FFCA28',
|
||||
inactive = '#7491D2',
|
||||
active = '#FFCA28',
|
||||
// background = '#2A3236',
|
||||
background = 'transparent',
|
||||
smear = 0,
|
||||
playheadColor = 'white',
|
||||
minMidi = 10,
|
||||
maxMidi = 90,
|
||||
autorange = 0,
|
||||
timeframe: timeframeProp,
|
||||
fold = 0,
|
||||
vertical = 0,
|
||||
labels = 0,
|
||||
} = {}) {
|
||||
const ctx = getDrawContext();
|
||||
const w = ctx.canvas.width;
|
||||
const h = ctx.canvas.height;
|
||||
let from = -cycles * playhead;
|
||||
let to = cycles * (1 - playhead);
|
||||
|
||||
if (timeframeProp) {
|
||||
console.warn('timeframe is deprecated! use from/to instead');
|
||||
from = 0;
|
||||
to = timeframeProp;
|
||||
}
|
||||
const timeAxis = vertical ? h : w;
|
||||
const valueAxis = vertical ? w : h;
|
||||
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
|
||||
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
|
||||
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
|
||||
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
|
||||
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
|
||||
let foldValues = [];
|
||||
flipTime && timeRange.reverse();
|
||||
flipValues && valueRange.reverse();
|
||||
|
||||
this.draw(
|
||||
(ctx, events, t) => {
|
||||
ctx.fillStyle = background;
|
||||
ctx.globalAlpha = 1; // reset!
|
||||
if (!smear) {
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
const inFrame = (event) =>
|
||||
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
|
||||
events.filter(inFrame).forEach((event) => {
|
||||
const isActive = event.whole.begin <= t && event.endClipped > t;
|
||||
ctx.fillStyle = event.context?.color || inactive;
|
||||
ctx.strokeStyle = event.context?.color || active;
|
||||
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 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);
|
||||
if (labels) {
|
||||
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
|
||||
ctx.font = `${barThickness * 0.75}px monospace`;
|
||||
ctx.strokeStyle = 'black';
|
||||
ctx.fillStyle = isActive ? 'white' : '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();
|
||||
},
|
||||
{
|
||||
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) => String(a).localeCompare(String(b)));
|
||||
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
||||
},
|
||||
},
|
||||
);
|
||||
return this;
|
||||
};
|
||||
|
||||
// this function allows drawing a pianoroll without ties to Pattern.prototype
|
||||
// it will probably replace the above in the future
|
||||
export function pianoroll({
|
||||
time,
|
||||
haps,
|
||||
cycles = 4,
|
||||
playhead = 0.5,
|
||||
flipTime = 0,
|
||||
flipValues = 0,
|
||||
hideNegative = false,
|
||||
// inactive = '#C9E597',
|
||||
// inactive = '#FFCA28',
|
||||
inactive = '#7491D2',
|
||||
active = '#FFCA28',
|
||||
// background = '#2A3236',
|
||||
background = 'transparent',
|
||||
smear = 0,
|
||||
playheadColor = 'white',
|
||||
minMidi = 10,
|
||||
maxMidi = 90,
|
||||
autorange = 0,
|
||||
timeframe: timeframeProp,
|
||||
fold = 0,
|
||||
vertical = 0,
|
||||
labels = false,
|
||||
ctx,
|
||||
} = {}) {
|
||||
const w = ctx.canvas.width;
|
||||
const h = ctx.canvas.height;
|
||||
let from = -cycles * playhead;
|
||||
let to = cycles * (1 - playhead);
|
||||
|
||||
if (timeframeProp) {
|
||||
console.warn('timeframe is deprecated! use from/to instead');
|
||||
from = 0;
|
||||
to = timeframeProp;
|
||||
}
|
||||
const timeAxis = vertical ? h : w;
|
||||
const valueAxis = vertical ? w : h;
|
||||
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
|
||||
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
|
||||
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
|
||||
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
|
||||
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
|
||||
let foldValues = [];
|
||||
flipTime && timeRange.reverse();
|
||||
flipValues && valueRange.reverse();
|
||||
|
||||
// onQuery
|
||||
const { min, max, values } = haps.reduce(
|
||||
({ min, max, values }, e) => {
|
||||
const v = getValue(e);
|
||||
return {
|
||||
min: v < min ? v : min,
|
||||
max: v > max ? v : max,
|
||||
values: values.includes(v) ? values : [...values, v],
|
||||
};
|
||||
},
|
||||
{ min: Infinity, max: -Infinity, values: [] },
|
||||
);
|
||||
if (autorange) {
|
||||
minMidi = min;
|
||||
maxMidi = max;
|
||||
valueExtent = maxMidi - minMidi + 1;
|
||||
}
|
||||
// foldValues = values.sort((a, b) => a - b);
|
||||
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
|
||||
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
||||
|
||||
ctx.fillStyle = background;
|
||||
ctx.globalAlpha = 1; // reset!
|
||||
if (!smear) {
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
/* const inFrame = (event) =>
|
||||
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
|
||||
haps
|
||||
// .filter(inFrame)
|
||||
.forEach((event) => {
|
||||
const isActive = event.whole.begin <= time && event.whole.end > time;
|
||||
const color = event.value?.color || event.context?.color;
|
||||
ctx.fillStyle = color || inactive;
|
||||
ctx.strokeStyle = color || active;
|
||||
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
|
||||
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
||||
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||
const value = getValue(event);
|
||||
const valuePx = scale(
|
||||
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
|
||||
...valueRange,
|
||||
);
|
||||
let margin = 0;
|
||||
const offset = scale(time / timeExtent, ...timeRange);
|
||||
let coords;
|
||||
if (vertical) {
|
||||
coords = [
|
||||
valuePx + 1 - (flipValues ? barThickness : 0), // x
|
||||
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
|
||||
barThickness - 2, // width
|
||||
durationPx - 2, // height
|
||||
];
|
||||
} else {
|
||||
coords = [
|
||||
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
|
||||
valuePx + 1 - (flipValues ? 0 : barThickness), // y
|
||||
durationPx - 2, // widith
|
||||
barThickness - 2, // height
|
||||
];
|
||||
}
|
||||
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
||||
if (labels) {
|
||||
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
|
||||
ctx.font = `${barThickness * 0.75}px monospace`;
|
||||
ctx.strokeStyle = 'black';
|
||||
ctx.fillStyle = isActive ? 'white' : '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;
|
||||
}
|
||||
|
||||
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) =>
|
||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, 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 });
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import { Cyclist } from './cyclist.mjs';
|
||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { setTime } from './time.mjs';
|
||||
import { evalScope } from './evaluate.mjs';
|
||||
import { register } from './pattern.mjs';
|
||||
|
||||
export function repl({
|
||||
interval,
|
||||
defaultOutput,
|
||||
onSchedulerError,
|
||||
onEvalError,
|
||||
beforeEval,
|
||||
afterEval,
|
||||
getTime,
|
||||
transpiler,
|
||||
onToggle,
|
||||
editPattern,
|
||||
}) {
|
||||
const scheduler = new Cyclist({
|
||||
interval,
|
||||
onTrigger: getTrigger({ defaultOutput, getTime }),
|
||||
onError: onSchedulerError,
|
||||
getTime,
|
||||
onToggle,
|
||||
});
|
||||
const setPattern = (pattern, autostart = true) => {
|
||||
pattern = editPattern?.(pattern) || pattern;
|
||||
scheduler.setPattern(pattern, autostart);
|
||||
};
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
const evaluate = async (code, autostart = true) => {
|
||||
if (!code) {
|
||||
throw new Error('no code to evaluate');
|
||||
}
|
||||
try {
|
||||
await beforeEval?.({ code });
|
||||
let { pattern } = await _evaluate(code, transpiler);
|
||||
|
||||
logger(`[eval] code updated`);
|
||||
setPattern(pattern, autostart);
|
||||
afterEval?.({ code, pattern });
|
||||
return pattern;
|
||||
} catch (err) {
|
||||
// console.warn(`[repl] eval error: ${err.message}`);
|
||||
logger(`[eval] error: ${err.message}`, 'error');
|
||||
onEvalError?.(err);
|
||||
}
|
||||
};
|
||||
const stop = () => scheduler.stop();
|
||||
const start = () => scheduler.start();
|
||||
const pause = () => scheduler.pause();
|
||||
const setCps = (cps) => scheduler.setCps(cps);
|
||||
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');
|
||||
}
|
||||
};
|
||||
+91
-66
@@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { Hap } from './hap.mjs';
|
||||
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
|
||||
import { Pattern, fastcat, reify, silence, stack, isPattern } from './pattern.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
import { id } from './util.mjs';
|
||||
|
||||
@@ -20,20 +20,20 @@ export const signal = (func) => {
|
||||
};
|
||||
|
||||
export const isaw = signal((t) => 1 - (t % 1));
|
||||
export const isaw2 = isaw.toBipolar();
|
||||
export const isaw2 = isaw._toBipolar();
|
||||
|
||||
/**
|
||||
* A sawtooth signal between 0 and 1.
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4))
|
||||
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
|
||||
* @example
|
||||
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
||||
*
|
||||
*/
|
||||
export const saw = signal((t) => t % 1);
|
||||
export const saw2 = saw.toBipolar();
|
||||
export const saw2 = saw._toBipolar();
|
||||
|
||||
export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||
|
||||
@@ -45,7 +45,7 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
|
||||
*
|
||||
*/
|
||||
export const sine = sine2.fromBipolar();
|
||||
export const sine = sine2._fromBipolar();
|
||||
|
||||
/**
|
||||
* A cosine signal between 0 and 1.
|
||||
@@ -67,7 +67,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||
*
|
||||
*/
|
||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
||||
export const square2 = square.toBipolar();
|
||||
export const square2 = square._toBipolar();
|
||||
|
||||
/**
|
||||
* A triangle signal between 0 and 1.
|
||||
@@ -114,14 +114,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.
|
||||
*
|
||||
@@ -135,7 +127,7 @@ export const rand = signal(timeToRand);
|
||||
/**
|
||||
* A continuous pattern of random numbers, between -1 and 1
|
||||
*/
|
||||
export const rand2 = rand.toBipolar();
|
||||
export const rand2 = rand._toBipolar();
|
||||
|
||||
export const _brandBy = (p) => rand.fmap((x) => x < p);
|
||||
export const brandBy = (pPat) => reify(pPat).fmap(_brandBy).innerJoin();
|
||||
@@ -209,7 +201,7 @@ Pattern.prototype.choose = function (...xs) {
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
Pattern.prototype.choose2 = function (...xs) {
|
||||
return chooseWith(this.fromBipolar(), xs);
|
||||
return chooseWith(this._fromBipolar(), xs);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -246,7 +238,6 @@ export const wchoose = (...pairs) => wchooseWith(rand, ...pairs);
|
||||
|
||||
export const wchooseCycles = (...pairs) => _wchooseWith(rand, ...pairs).innerJoin();
|
||||
|
||||
// this function expects pat to be a pattern of floats...
|
||||
export const perlinWith = (pat) => {
|
||||
const pata = pat.fmap(Math.floor);
|
||||
const patb = pat.fmap((t) => Math.floor(t) + 1);
|
||||
@@ -264,11 +255,11 @@ export const perlinWith = (pat) => {
|
||||
* s("bd sd,hh*4").cutoff(perlin.range(500,2000))
|
||||
*
|
||||
*/
|
||||
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
||||
export const perlin = perlinWith(time);
|
||||
|
||||
export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
|
||||
pat.fmap((a) => (_) => a).appLeft(withPat.filterValues((v) => v > x)),
|
||||
);
|
||||
Pattern.prototype._degradeByWith = function (withPat, x) {
|
||||
return this.fmap((a) => (_) => a).appLeft(withPat._filterValues((v) => v > x));
|
||||
};
|
||||
|
||||
/**
|
||||
* Randomly removes events from the pattern by a given amount.
|
||||
@@ -284,9 +275,9 @@ export const degradeByWith = register('degradeByWith', (withPat, x, pat) =>
|
||||
* @example
|
||||
* s("[hh?0.2]*8")
|
||||
*/
|
||||
export const degradeBy = register('degradeBy', function (x, pat) {
|
||||
return pat._degradeByWith(rand, x);
|
||||
});
|
||||
Pattern.prototype._degradeBy = function (x) {
|
||||
return this._degradeByWith(rand, x);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -300,7 +291,9 @@ export const degradeBy = register('degradeBy', function (x, pat) {
|
||||
* @example
|
||||
* 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.
|
||||
@@ -315,14 +308,26 @@ export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
||||
* @example
|
||||
* s("hh*8").undegradeBy(0.2)
|
||||
*/
|
||||
export const undegradeBy = register('undegradeBy', function (x, pat) {
|
||||
return pat._degradeByWith(
|
||||
Pattern.prototype._undegradeBy = function (x) {
|
||||
return this._degradeByWith(
|
||||
rand.fmap((r) => 1 - r),
|
||||
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 +342,24 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
||||
* @example
|
||||
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
|
||||
*/
|
||||
|
||||
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
||||
Pattern.prototype.sometimesBy = function (patx, func) {
|
||||
const pat = this;
|
||||
return reify(patx)
|
||||
.fmap((x) => stack(pat._degradeBy(x), func(pat._undegradeBy(1 - x))))
|
||||
.fmap((x) => pat._sometimesBy(x, func))
|
||||
.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 +372,20 @@ export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
||||
* @example
|
||||
* s("hh*4").sometimes(x=>x.speed("0.5"))
|
||||
*/
|
||||
export const sometimes = register('sometimes', function (func, pat) {
|
||||
return pat._sometimesBy(0.5, func);
|
||||
});
|
||||
Pattern.prototype.sometimes = function (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 +400,12 @@ export const sometimes = register('sometimes', function (func, pat) {
|
||||
* @example
|
||||
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
|
||||
*/
|
||||
|
||||
export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
|
||||
Pattern.prototype.someCyclesBy = function (patx, func) {
|
||||
const pat = this;
|
||||
return reify(patx)
|
||||
.fmap((x) =>
|
||||
stack(
|
||||
pat._degradeByWith(rand._segment(1), x),
|
||||
func(pat._degradeByWith(rand.fmap((r) => 1 - r)._segment(1), 1 - x)),
|
||||
),
|
||||
)
|
||||
.fmap((x) => pat._someCyclesBy(x, func))
|
||||
.innerJoin();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -394,9 +417,9 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat)
|
||||
* @example
|
||||
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
|
||||
*/
|
||||
export const someCycles = register('someCycles', function (func, pat) {
|
||||
return pat._someCyclesBy(0.5, func);
|
||||
});
|
||||
Pattern.prototype.someCycles = function (func) {
|
||||
return this._someCyclesBy(0.5, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -408,9 +431,9 @@ export const someCycles = register('someCycles', function (func, pat) {
|
||||
* @example
|
||||
* s("hh*8").often(x=>x.speed("0.5"))
|
||||
*/
|
||||
export const often = register('often', function (func, pat) {
|
||||
return pat.sometimesBy(0.75, func);
|
||||
});
|
||||
Pattern.prototype.often = function (func) {
|
||||
return this.sometimesBy(0.75, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -422,9 +445,9 @@ export const often = register('often', function (func, pat) {
|
||||
* @example
|
||||
* s("hh*8").rarely(x=>x.speed("0.5"))
|
||||
*/
|
||||
export const rarely = register('rarely', function (func, pat) {
|
||||
return pat.sometimesBy(0.25, func);
|
||||
});
|
||||
Pattern.prototype.rarely = function (func) {
|
||||
return this.sometimesBy(0.25, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -436,9 +459,9 @@ export const rarely = register('rarely', function (func, pat) {
|
||||
* @example
|
||||
* s("hh*8").almostNever(x=>x.speed("0.5"))
|
||||
*/
|
||||
export const almostNever = register('almostNever', function (func, pat) {
|
||||
return pat.sometimesBy(0.1, func);
|
||||
});
|
||||
Pattern.prototype.almostNever = function (func) {
|
||||
return this.sometimesBy(0.1, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -450,9 +473,9 @@ export const almostNever = register('almostNever', function (func, pat) {
|
||||
* @example
|
||||
* s("hh*8").almostAlways(x=>x.speed("0.5"))
|
||||
*/
|
||||
export const almostAlways = register('almostAlways', function (func, pat) {
|
||||
return pat.sometimesBy(0.9, func);
|
||||
});
|
||||
Pattern.prototype.almostAlways = function (func) {
|
||||
return this.sometimesBy(0.9, func);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -464,9 +487,9 @@ export const almostAlways = register('almostAlways', function (func, pat) {
|
||||
* @example
|
||||
* s("hh*8").never(x=>x.speed("0.5"))
|
||||
*/
|
||||
export const never = register('never', function (_, pat) {
|
||||
return pat;
|
||||
});
|
||||
Pattern.prototype.never = function (func) {
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -478,6 +501,8 @@ export const never = register('never', function (_, pat) {
|
||||
* @example
|
||||
* s("hh*8").always(x=>x.speed("0.5"))
|
||||
*/
|
||||
export const always = register('always', function (func, pat) {
|
||||
return func(pat);
|
||||
});
|
||||
Pattern.prototype.always = function (func) {
|
||||
return func(this);
|
||||
};
|
||||
|
||||
Pattern.prototype.patternified.push('degradeBy', 'undegradeBy');
|
||||
|
||||
+13
-6
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { register } from './index.mjs';
|
||||
import { Pattern, patternify2 } from './index.mjs';
|
||||
|
||||
let synth;
|
||||
try {
|
||||
@@ -16,7 +16,7 @@ try {
|
||||
let allVoices = synth?.getVoices();
|
||||
// console.log('voices', allVoices);
|
||||
|
||||
function triggerSpeech(words, lang, voice) {
|
||||
function speak(words, lang, voice) {
|
||||
synth.cancel();
|
||||
const utterance = new SpeechSynthesisUtterance(words);
|
||||
utterance.lang = lang;
|
||||
@@ -31,8 +31,15 @@ function triggerSpeech(words, lang, voice) {
|
||||
speechSynthesis.speak(utterance);
|
||||
}
|
||||
|
||||
export const speak = register('speak', function (lang, voice, pat) {
|
||||
return pat.onTrigger((_, hap) => {
|
||||
triggerSpeech(hap.value, lang, voice);
|
||||
Pattern.prototype._speak = function (lang, voice) {
|
||||
return this._withHap((hap) => {
|
||||
const onTrigger = (time, hap) => {
|
||||
speak(hap.value, lang, voice);
|
||||
};
|
||||
return hap.setContext({ ...hap.context, onTrigger });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Pattern.prototype.speak = function (lang, voice) {
|
||||
return patternify2(Pattern.prototype._speak)(reify(lang), reify(voice), this);
|
||||
};
|
||||
|
||||
@@ -1,117 +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 }) {
|
||||
ctx.clearRect(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight);
|
||||
const [cx, cy] = [ctx.canvas.width / 2, ctx.canvas.height / 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 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
pure,
|
||||
stack,
|
||||
fastcat,
|
||||
firstOf,
|
||||
slowcat,
|
||||
cat,
|
||||
sequence,
|
||||
@@ -44,14 +43,10 @@ import {
|
||||
ply,
|
||||
rev,
|
||||
time,
|
||||
run,
|
||||
} from '../index.mjs';
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
|
||||
import controls from '../controls.mjs';
|
||||
|
||||
const { n, s } = controls;
|
||||
const st = (begin, end) => new State(ts(begin, end));
|
||||
const ts = (begin, end) => new TimeSpan(Fraction(begin), Fraction(end));
|
||||
const hap = (whole, part, value, context = {}) => new Hap(whole, part, value, context);
|
||||
@@ -60,7 +55,7 @@ const third = Fraction(1, 3);
|
||||
const twothirds = Fraction(2, 3);
|
||||
|
||||
const sameFirst = (a, b) => {
|
||||
return expect(a.sortHapsByPart().firstCycle()).toStrictEqual(b.sortHapsByPart().firstCycle());
|
||||
return expect(a._sortHapsByPart().firstCycle()).toStrictEqual(b._sortHapsByPart().firstCycle());
|
||||
};
|
||||
|
||||
describe('TimeSpan', () => {
|
||||
@@ -142,9 +137,6 @@ describe('Pattern', () => {
|
||||
it('Can make a pattern', () => {
|
||||
expect(pure('hello').query(st(0.5, 2.5)).length).toBe(3);
|
||||
});
|
||||
it('Supports zero-width queries', () => {
|
||||
expect(pure('hello').queryArc(0, 0).length).toBe(1);
|
||||
});
|
||||
});
|
||||
describe('fmap()', () => {
|
||||
it('Can add things', () => {
|
||||
@@ -155,46 +147,35 @@ describe('Pattern', () => {
|
||||
).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()', () => {
|
||||
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()', () => {
|
||||
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).addIn(pure(4)).query(st(0, 1))[0].value).toBe(7);
|
||||
});
|
||||
it('can structure Out()', () => {
|
||||
sameFirst(sequence(1, 2).add.out(4), sequence(5, 6).struct(true));
|
||||
sameFirst(sequence(1, 2).addOut(4), sequence(5, 6).struct(true));
|
||||
});
|
||||
it('can Mix() structure', () => {
|
||||
expect(sequence(1, 2).add.mix(silence, 5, silence).firstCycle()).toStrictEqual([
|
||||
expect(sequence(1, 2).addMix(silence, 5, silence).firstCycle()).toStrictEqual([
|
||||
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 6),
|
||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 7),
|
||||
]);
|
||||
});
|
||||
it('can Trig() structure', () => {
|
||||
sameFirst(
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trig(20, 30).early(2),
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).addTrig(20, 30).early(2),
|
||||
sequence(26, 27, 36, 37),
|
||||
);
|
||||
});
|
||||
it('can Trigzero() structure', () => {
|
||||
sameFirst(
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trigzero(20, 30).early(2),
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).addTrigzero(20, 30).early(2),
|
||||
sequence(21, 22, 31, 32),
|
||||
);
|
||||
});
|
||||
it('can Squeeze() structure', () => {
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).add.squeeze(sequence(10, 20, 30)),
|
||||
sequence(1, [2, 3]).addSqueeze(sequence(10, 20, 30)),
|
||||
sequence(
|
||||
[11, 21, 31],
|
||||
[
|
||||
@@ -206,43 +187,40 @@ describe('Pattern', () => {
|
||||
});
|
||||
it('can SqueezeOut() structure', () => {
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).add.squeezeout(10, 20, 30),
|
||||
sequence(1, [2, 3]).addSqueezeOut(10, 20, 30),
|
||||
sequence([11, [12, 13]], [21, [22, 23]], [31, [32, 33]]),
|
||||
);
|
||||
});
|
||||
it('can add object patterns', () => {
|
||||
sameFirst(n(sequence(1, [2, 3])).add(n(10)), n(sequence(11, [12, 13])));
|
||||
});
|
||||
});
|
||||
describe('keep()', () => {
|
||||
it('can structure In()', () => {
|
||||
expect(pure(3).keep(pure(4)).query(st(0, 1))[0].value).toBe(3);
|
||||
expect(pure(3).keep.in(pure(4)).query(st(0, 1))[0].value).toBe(3);
|
||||
expect(pure(3).keepIn(pure(4)).query(st(0, 1))[0].value).toBe(3);
|
||||
});
|
||||
it('can structure Out()', () => {
|
||||
sameFirst(sequence(1, 2).keep.out(4), sequence(1, 2).struct(true));
|
||||
sameFirst(sequence(1, 2).keepOut(4), sequence(1, 2).struct(true));
|
||||
});
|
||||
it('can Mix() structure', () => {
|
||||
expect(sequence(1, 2).keep.mix(silence, 5, silence).firstCycle()).toStrictEqual([
|
||||
expect(sequence(1, 2).keepMix(silence, 5, silence).firstCycle()).toStrictEqual([
|
||||
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
|
||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
||||
]);
|
||||
});
|
||||
it('can Trig() structure', () => {
|
||||
sameFirst(
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trig(20, 30).early(2),
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepTrig(20, 30).early(2),
|
||||
sequence(6, 7, 6, 7),
|
||||
);
|
||||
});
|
||||
it('can Trigzero() structure', () => {
|
||||
sameFirst(
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trigzero(20, 30).early(2),
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepTrigzero(20, 30).early(2),
|
||||
sequence(1, 2, 1, 2),
|
||||
);
|
||||
});
|
||||
it('can Squeeze() structure', () => {
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).keep.squeeze(sequence(10, 20, 30)),
|
||||
sequence(1, [2, 3]).keepSqueeze(sequence(10, 20, 30)),
|
||||
sequence(
|
||||
[1, 1, 1],
|
||||
[
|
||||
@@ -253,38 +231,38 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
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]).keepSqueezeOut(10, 20, 30), sequence([1, [2, 3]], [1, [2, 3]], [1, [2, 3]]));
|
||||
});
|
||||
});
|
||||
describe('keepif()', () => {
|
||||
it('can structure In()', () => {
|
||||
sameFirst(sequence(3, 4).keepif(true, false), sequence(3, silence));
|
||||
sameFirst(sequence(3, 4).keepif.in(true, false), sequence(3, silence));
|
||||
sameFirst(sequence(3, 4).keepifIn(true, false), sequence(3, silence));
|
||||
});
|
||||
it('can structure Out()', () => {
|
||||
sameFirst(pure(1).keepif.out(true, false), sequence(1, silence));
|
||||
sameFirst(pure(1).keepifOut(true, false), sequence(1, silence));
|
||||
});
|
||||
it('can Mix() structure', () => {
|
||||
expect(sequence(1, 2).keepif.mix(false, true, false).firstCycle()).toStrictEqual([
|
||||
expect(sequence(1, 2).keepifMix(false, true, false).firstCycle()).toStrictEqual([
|
||||
new Hap(ts(1 / 3, 1 / 2), ts(1 / 3, 1 / 2), 1),
|
||||
new Hap(ts(1 / 2, 2 / 3), ts(1 / 2, 2 / 3), 2),
|
||||
]);
|
||||
});
|
||||
it('can Trig() structure', () => {
|
||||
sameFirst(
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trig(false, true).early(2),
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepifTrig(false, true).early(2),
|
||||
sequence(silence, silence, 6, 7),
|
||||
);
|
||||
});
|
||||
it('can Trigzero() structure', () => {
|
||||
sameFirst(
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trigzero(false, true).early(2),
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepifTrigzero(false, true).early(2),
|
||||
sequence(silence, silence, 1, 2),
|
||||
);
|
||||
});
|
||||
it('can Squeeze() structure', () => {
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).keepif.squeeze(sequence(true, true, false)),
|
||||
sequence(1, [2, 3]).keepifSqueeze(sequence(true, true, false)),
|
||||
sequence(
|
||||
[1, 1, silence],
|
||||
[
|
||||
@@ -295,7 +273,7 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
it('can SqueezeOut() structure', () => {
|
||||
sameFirst(sequence(1, [2, 3]).keepif.squeezeout(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
|
||||
sameFirst(sequence(1, [2, 3]).keepifSqueezeOut(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
|
||||
});
|
||||
});
|
||||
describe('sub()', () => {
|
||||
@@ -335,13 +313,13 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('setOut()', () => {
|
||||
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).setOut(4), pure(4).mask(true, true));
|
||||
});
|
||||
});
|
||||
describe('setSqueeze()', () => {
|
||||
it('Can squeeze one pattern inside the haps of another', () => {
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).set.squeeze(sequence('a', 'b', 'c')),
|
||||
sequence(1, [2, 3]).setSqueeze(sequence('a', 'b', 'c')),
|
||||
sequence(
|
||||
['a', 'b', 'c'],
|
||||
[
|
||||
@@ -351,7 +329,7 @@ describe('Pattern', () => {
|
||||
),
|
||||
);
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).set.squeeze('a', 'b', 'c'),
|
||||
sequence(1, [2, 3]).setSqueeze('a', 'b', 'c'),
|
||||
sequence(
|
||||
['a', 'b', 'c'],
|
||||
[
|
||||
@@ -395,10 +373,9 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
it('copes with breaking up events across cycles', () => {
|
||||
expect(pure('a').slow(2)._fastGap(2).setContext({}).query(st(0, 2))).toStrictEqual([
|
||||
hap(ts(0, 1), ts(0, 0.5), 'a'),
|
||||
hap(ts(0.5, 1.5), ts(1, 1.5), 'a'),
|
||||
]);
|
||||
expect(pure('a').slow(2)._fastGap(2)._setContext({}).query(st(0, 2))).toStrictEqual(
|
||||
[hap(ts(0, 1), ts(0, 0.5), 'a'), hap(ts(0.5, 1.5), ts(1, 1.5), 'a')]
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_compressSpan()', () => {
|
||||
@@ -419,8 +396,8 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
it('defaults to accepting sequences', () => {
|
||||
expect(sequence('a', 'b', 'c').fast(sequence(1.5, 2)).sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
sequence('a', 'b', 'c').fast(1.5, 2).sortHapsByPart().firstCycle(),
|
||||
expect(sequence(1, 2, 3).fast(sequence(1.5, 2)).firstCycle()).toStrictEqual(
|
||||
sequence(1, 2, 3).fast(1.5, 2).firstCycle(),
|
||||
);
|
||||
});
|
||||
it('works as a static function', () => {
|
||||
@@ -453,16 +430,6 @@ describe('Pattern', () => {
|
||||
// mini('[c3 g3]/2 eb3') always plays [c3 eb3]
|
||||
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
|
||||
});
|
||||
it('Supports zero-length queries', () => {
|
||||
expect(steady('a')._slow(1).queryArc(0, 0)).toStrictEqual(steady('a').queryArc(0, 0));
|
||||
});
|
||||
});
|
||||
describe('slow()', () => {
|
||||
it('Supports zero-length queries', () => {
|
||||
expect(steady('a').slow(1).setContext({}).queryArc(0, 0)).toStrictEqual(
|
||||
steady('a').setContext({}).queryArc(0, 0),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('inside', () => {
|
||||
it('can rev inside a cycle', () => {
|
||||
@@ -478,7 +445,7 @@ describe('Pattern', () => {
|
||||
it('Filters true', () => {
|
||||
expect(
|
||||
pure(true)
|
||||
.filterValues((x) => x)
|
||||
._filterValues((x) => x)
|
||||
.firstCycle().length,
|
||||
).toBe(1);
|
||||
});
|
||||
@@ -503,7 +470,7 @@ describe('Pattern', () => {
|
||||
pure(10)
|
||||
.when(slowcat(true, false), (x) => x.add(3))
|
||||
.fast(4)
|
||||
.sortHapsByPart()
|
||||
._sortHapsByPart()
|
||||
.firstCycle(),
|
||||
).toStrictEqual(fastcat(13, 10, 13, 10).firstCycle());
|
||||
});
|
||||
@@ -590,36 +557,26 @@ describe('Pattern', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('firstOf()', () => {
|
||||
describe('every()', () => {
|
||||
it('Can apply a function every 3rd time', () => {
|
||||
expect(
|
||||
pure('a')
|
||||
.firstOf(3, (x) => x._fast(2))
|
||||
.every(3, (x) => x._fast(2))
|
||||
._fast(3)
|
||||
.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', () => {
|
||||
expect(pure('a').firstOf(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
|
||||
expect(pure('a').every(3, fast(2))._fast(3).firstCycle()).toStrictEqual(
|
||||
sequence(sequence('a', 'a'), 'a', 'a').firstCycle(),
|
||||
);
|
||||
expect(sequence(3, 4, 5).firstOf(3, add(3)).fast(5).firstCycle()).toStrictEqual(
|
||||
expect(sequence(3, 4, 5).every(3, add(3)).fast(5).firstCycle()).toStrictEqual(
|
||||
sequence(6, 7, 8, 3, 4, 5, 3, 4, 5, 6, 7, 8, 3, 4, 5).firstCycle(),
|
||||
);
|
||||
expect(sequence(3, 4, 5).firstOf(2, sub(1)).fast(5).firstCycle()).toStrictEqual(
|
||||
expect(sequence(3, 4, 5).every(2, sub(1)).fast(5).firstCycle()).toStrictEqual(
|
||||
sequence(2, 3, 4, 3, 4, 5, 2, 3, 4, 3, 4, 5, 2, 3, 4).firstCycle(),
|
||||
);
|
||||
expect(sequence(3, 4, 5).firstOf(3, add(3)).firstOf(2, sub(1)).fast(2).firstCycle()).toStrictEqual(
|
||||
expect(sequence(3, 4, 5).every(3, add(3)).every(2, sub(1)).fast(2).firstCycle()).toStrictEqual(
|
||||
sequence(5, 6, 7, 3, 4, 5).firstCycle(),
|
||||
);
|
||||
});
|
||||
@@ -715,7 +672,7 @@ describe('Pattern', () => {
|
||||
it('Can set the hap context', () => {
|
||||
expect(
|
||||
pure('a')
|
||||
.setContext([
|
||||
._setContext([
|
||||
[
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
@@ -736,13 +693,13 @@ describe('Pattern', () => {
|
||||
it('Can update the hap context', () => {
|
||||
expect(
|
||||
pure('a')
|
||||
.setContext([
|
||||
._setContext([
|
||||
[
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
],
|
||||
])
|
||||
.withContext((c) => [
|
||||
._withContext((c) => [
|
||||
...c,
|
||||
[
|
||||
[3, 4],
|
||||
@@ -766,7 +723,7 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('apply', () => {
|
||||
it('Can apply a function', () => {
|
||||
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
|
||||
expect(sequence('a', 'b')._apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
|
||||
}),
|
||||
it('Can apply a pattern of functions', () => {
|
||||
expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle());
|
||||
@@ -807,18 +764,18 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('jux', () => {
|
||||
it('Can juxtapose', () => {
|
||||
expect(pure({ a: 1 }).jux(fast(2)).sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
expect(pure({ a: 1 }).jux(fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
stack(pure({ a: 1, pan: 0 }), pure({ a: 1, pan: 1 }).fast(2))
|
||||
.sortHapsByPart()
|
||||
._sortHapsByPart()
|
||||
.firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('juxBy', () => {
|
||||
it('Can juxtapose by half', () => {
|
||||
expect(pure({ a: 1 }).juxBy(0.5, fast(2)).sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
expect(pure({ a: 1 }).juxBy(0.5, fast(2))._sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
stack(pure({ a: 1, pan: 0.25 }), pure({ a: 1, pan: 0.75 }).fast(2))
|
||||
.sortHapsByPart()
|
||||
._sortHapsByPart()
|
||||
.firstCycle(),
|
||||
);
|
||||
});
|
||||
@@ -828,7 +785,7 @@ describe('Pattern', () => {
|
||||
expect(
|
||||
sequence('a', ['a', 'a'])
|
||||
.fmap((a) => fastcat('b', 'c'))
|
||||
.squeezeJoin()
|
||||
._squeezeJoin()
|
||||
.firstCycle(),
|
||||
).toStrictEqual(
|
||||
sequence(
|
||||
@@ -842,11 +799,10 @@ describe('Pattern', () => {
|
||||
});
|
||||
it('Squeezes to the correct cycle', () => {
|
||||
expect(
|
||||
pure(time.struct(true))
|
||||
.squeezeJoin()
|
||||
.queryArc(3, 4)
|
||||
.map((x) => x.value),
|
||||
).toStrictEqual([Fraction(3.5)]);
|
||||
pure(time.struct(true))._squeezeJoin().queryArc(3,4).map(x => x.value)
|
||||
).toStrictEqual(
|
||||
[Fraction(3.5)]
|
||||
)
|
||||
});
|
||||
});
|
||||
describe('ply', () => {
|
||||
@@ -880,7 +836,7 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
it('Can chop(2,3)', () => {
|
||||
expect(pure({ sound: 'a' }).fast(2).chop(2, 3).sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
expect(pure({ sound: 'a' }).fast(2).chop(2, 3)._sortHapsByPart().firstCycle()).toStrictEqual(
|
||||
sequence(
|
||||
[
|
||||
{ sound: 'a', begin: 0, end: 0.5 },
|
||||
@@ -892,14 +848,16 @@ describe('Pattern', () => {
|
||||
{ sound: 'a', begin: 2 / 3, end: 1 },
|
||||
],
|
||||
)
|
||||
.sortHapsByPart()
|
||||
._sortHapsByPart()
|
||||
.firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('range', () => {
|
||||
it('Can be patterned', () => {
|
||||
expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(sequence(0, 0.5).firstCycle());
|
||||
expect(sequence(0, 0).range(sequence(0, 0.5), 1).firstCycle()).toStrictEqual(
|
||||
sequence(0, 0.5).firstCycle(),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('range2', () => {
|
||||
@@ -909,18 +867,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', () => {
|
||||
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(
|
||||
@@ -928,79 +874,4 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('alignments', () => {
|
||||
it('Can squeeze arguments', () => {
|
||||
expect(sequence(1, 2).add.squeeze(4, 5).firstCycle()).toStrictEqual(sequence(5, 6, 6, 7).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('defragmentHaps', () => {
|
||||
it('Can merge two touching haps with same whole and value', () => {
|
||||
expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1);
|
||||
});
|
||||
it('Doesnt merge two overlapping haps', () => {
|
||||
expect(stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(
|
||||
2,
|
||||
);
|
||||
});
|
||||
it('Doesnt merge two touching haps with different values', () => {
|
||||
expect(stack(pure('a').mask(1, 0), pure('b').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
|
||||
});
|
||||
it('Doesnt merge two touching haps with different wholes', () => {
|
||||
expect(stack(sequence('a', silence), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
|
||||
});
|
||||
});
|
||||
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);
|
||||
});
|
||||
@@ -5,21 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { pure } from '../pattern.mjs';
|
||||
import {
|
||||
isNote,
|
||||
tokenizeNote,
|
||||
noteToMidi,
|
||||
midiToFreq,
|
||||
freqToMidi,
|
||||
_mod,
|
||||
compose,
|
||||
getFrequency,
|
||||
getPlayableNoteValue,
|
||||
parseNumeral,
|
||||
parseFractional,
|
||||
numeralArgs,
|
||||
fractionalArgs,
|
||||
} from '../util.mjs';
|
||||
import { isNote, tokenizeNote, toMidi, fromMidi, mod, compose, getFrequency, getPlayableNoteValue } from '../util.mjs';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('isNote', () => {
|
||||
@@ -33,12 +19,9 @@ describe('isNote', () => {
|
||||
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', () => {
|
||||
expect(isNote('H5')).toBe(false);
|
||||
expect(isNote('C')).toBe(false);
|
||||
expect(isNote('X')).toBe(false);
|
||||
expect(isNote(1)).toBe(false);
|
||||
});
|
||||
@@ -75,33 +58,27 @@ describe('isNote', () => {
|
||||
expect(tokenizeNote(123)).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
describe('noteToMidi', () => {
|
||||
describe('toMidi', () => {
|
||||
it('should turn notes into midi', () => {
|
||||
expect(noteToMidi('A4')).toEqual(69);
|
||||
expect(noteToMidi('C4')).toEqual(60);
|
||||
expect(noteToMidi('Db4')).toEqual(61);
|
||||
expect(noteToMidi('C3')).toEqual(48);
|
||||
expect(noteToMidi('Cb3')).toEqual(47);
|
||||
expect(noteToMidi('Cbb3')).toEqual(46);
|
||||
expect(noteToMidi('C#3')).toEqual(49);
|
||||
expect(noteToMidi('C#3')).toEqual(49);
|
||||
expect(noteToMidi('C##3')).toEqual(50);
|
||||
expect(toMidi('A4')).toEqual(69);
|
||||
expect(toMidi('C4')).toEqual(60);
|
||||
expect(toMidi('Db4')).toEqual(61);
|
||||
expect(toMidi('C3')).toEqual(48);
|
||||
expect(toMidi('Cb3')).toEqual(47);
|
||||
expect(toMidi('Cbb3')).toEqual(46);
|
||||
expect(toMidi('C#3')).toEqual(49);
|
||||
expect(toMidi('C#3')).toEqual(49);
|
||||
expect(toMidi('C##3')).toEqual(50);
|
||||
});
|
||||
it('should throw an error when given a non-note', () => {
|
||||
expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||
expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||
});
|
||||
});
|
||||
describe('midiToFreq', () => {
|
||||
describe('fromMidi', () => {
|
||||
it('should turn midi into frequency', () => {
|
||||
expect(midiToFreq(69)).toEqual(440);
|
||||
expect(midiToFreq(57)).toEqual(220);
|
||||
});
|
||||
});
|
||||
describe('freqToMidi', () => {
|
||||
it('should turn frequency into midi', () => {
|
||||
expect(freqToMidi(440)).toEqual(69);
|
||||
expect(freqToMidi(220)).toEqual(57);
|
||||
expect(fromMidi(69)).toEqual(440);
|
||||
expect(fromMidi(57)).toEqual(220);
|
||||
});
|
||||
});
|
||||
describe('getFrequency', () => {
|
||||
@@ -115,35 +92,35 @@ describe('getFrequency', () => {
|
||||
expect(getFrequency(happify(57, { type: 'midi' }))).toEqual(220);
|
||||
});
|
||||
it('should return frequencies unchanged', () => {
|
||||
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440);
|
||||
expect(getFrequency(happify(440, { type: 'frequency' }))).toEqual(440);
|
||||
expect(getFrequency(happify(432, { type: 'frequency' }))).toEqual(432);
|
||||
});
|
||||
it('should turn object with a "freq" property into frequency', () => {
|
||||
expect(getFrequency(happify({ freq: 220 }))).toEqual(220);
|
||||
expect(getFrequency(happify({ freq: 440 }))).toEqual(440);
|
||||
expect(getFrequency(happify({freq: 220}))).toEqual(220)
|
||||
expect(getFrequency(happify({freq: 440}))).toEqual(440)
|
||||
});
|
||||
it('should throw an error when given a non-note', () => {
|
||||
expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`);
|
||||
expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`);
|
||||
expect(() => getFrequency(happify('Q'))).toThrowError(`not a note or frequency: Q`)
|
||||
expect(() => getFrequency(happify('Z'))).toThrowError(`not a note or frequency: Z`)
|
||||
});
|
||||
});
|
||||
|
||||
describe('_mod', () => {
|
||||
describe('mod', () => {
|
||||
it('should work like regular modulo with positive numbers', () => {
|
||||
expect(_mod(0, 3)).toEqual(0);
|
||||
expect(_mod(1, 3)).toEqual(1);
|
||||
expect(_mod(2, 3)).toEqual(2);
|
||||
expect(_mod(3, 3)).toEqual(0);
|
||||
expect(_mod(4, 3)).toEqual(1);
|
||||
expect(_mod(4, 2)).toEqual(0);
|
||||
expect(mod(0, 3)).toEqual(0);
|
||||
expect(mod(1, 3)).toEqual(1);
|
||||
expect(mod(2, 3)).toEqual(2);
|
||||
expect(mod(3, 3)).toEqual(0);
|
||||
expect(mod(4, 3)).toEqual(1);
|
||||
expect(mod(4, 2)).toEqual(0);
|
||||
});
|
||||
it('should work with negative numbers', () => {
|
||||
expect(_mod(-1, 3)).toEqual(2);
|
||||
expect(_mod(-2, 3)).toEqual(1);
|
||||
expect(_mod(-3, 3)).toEqual(0);
|
||||
expect(_mod(-4, 3)).toEqual(2);
|
||||
expect(_mod(-5, 3)).toEqual(1);
|
||||
expect(_mod(-3, 2)).toEqual(1);
|
||||
expect(mod(-1, 3)).toEqual(2);
|
||||
expect(mod(-2, 3)).toEqual(1);
|
||||
expect(mod(-3, 3)).toEqual(0);
|
||||
expect(mod(-4, 3)).toEqual(2);
|
||||
expect(mod(-5, 3)).toEqual(1);
|
||||
expect(mod(-3, 2)).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,73 +140,22 @@ describe('compose', () => {
|
||||
describe('getPlayableNoteValue', () => {
|
||||
const happify = (val, context = {}) => pure(val).firstCycle()[0].setContext(context);
|
||||
it('should return object "note" property', () => {
|
||||
expect(getPlayableNoteValue(happify({ note: 'a4' }))).toEqual('a4');
|
||||
expect(getPlayableNoteValue(happify({note: "a4"}))).toEqual('a4')
|
||||
});
|
||||
it('should return object "n" property', () => {
|
||||
expect(getPlayableNoteValue(happify({ n: 'a4' }))).toEqual('a4');
|
||||
expect(getPlayableNoteValue(happify({n: "a4"}))).toEqual('a4')
|
||||
});
|
||||
it('should return object "value" property', () => {
|
||||
expect(getPlayableNoteValue(happify({ value: 'a4' }))).toEqual('a4');
|
||||
expect(getPlayableNoteValue(happify({value: "a4"}))).toEqual('a4')
|
||||
});
|
||||
it('should turn midi into frequency', () => {
|
||||
expect(getPlayableNoteValue(happify(57, { type: 'midi' }))).toEqual(220);
|
||||
});
|
||||
expect(getPlayableNoteValue(happify(57, {type: 'midi'}))).toEqual(220)
|
||||
})
|
||||
it('should return frequency value', () => {
|
||||
expect(getPlayableNoteValue(happify(220, { type: 'frequency' }))).toEqual(220);
|
||||
});
|
||||
expect(getPlayableNoteValue(happify(220, {type: 'frequency'}))).toEqual(220)
|
||||
})
|
||||
it('should throw an error if value is not an object, number, or string', () => {
|
||||
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`);
|
||||
expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseNumeral', () => {
|
||||
it('should parse numbers as is', () => {
|
||||
expect(parseNumeral(4)).toBe(4);
|
||||
expect(parseNumeral(0)).toBe(0);
|
||||
expect(parseNumeral(20)).toBe(20);
|
||||
expect(parseNumeral('20')).toBe(20);
|
||||
expect(parseNumeral(1.5)).toBe(1.5);
|
||||
});
|
||||
it('should parse notes', () => {
|
||||
expect(parseNumeral('c')).toBe(48);
|
||||
expect(parseNumeral('c4')).toBe(60);
|
||||
expect(parseNumeral('c#4')).toBe(61);
|
||||
expect(parseNumeral('db4')).toBe(61);
|
||||
});
|
||||
it('should throw an error for unknown strings', () => {
|
||||
expect(() => parseNumeral('xyz')).toThrowError('cannot parse as numeral: "xyz"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseFractional', () => {
|
||||
it('should parse numbers as is', () => {
|
||||
expect(parseFractional(4)).toBe(4);
|
||||
expect(parseFractional(0)).toBe(0);
|
||||
expect(parseFractional(20)).toBe(20);
|
||||
expect(parseFractional('20')).toBe(20);
|
||||
expect(parseFractional(1.5)).toBe(1.5);
|
||||
});
|
||||
it('should parse fractional shorthands values', () => {
|
||||
expect(parseFractional('w')).toBe(1);
|
||||
expect(parseFractional('h')).toBe(0.5);
|
||||
expect(parseFractional('q')).toBe(0.25);
|
||||
expect(parseFractional('e')).toBe(0.125);
|
||||
});
|
||||
it('should throw an error for unknown strings', () => {
|
||||
expect(() => parseFractional('xyz')).toThrowError('cannot parse as fractional: "xyz"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('numeralArgs', () => {
|
||||
it('should convert function arguments to numbers', () => {
|
||||
const add = numeralArgs((a, b) => a + b);
|
||||
expect(add('c4', 2)).toBe(62);
|
||||
});
|
||||
});
|
||||
describe('fractionalArgs', () => {
|
||||
it('should convert function arguments to numbers', () => {
|
||||
const add = fractionalArgs((a, b) => a + b);
|
||||
expect(add('q', 2)).toBe(2.25);
|
||||
});
|
||||
});
|
||||
expect(() => getPlayableNoteValue(happify(false))).toThrowError(`not a note: false`)
|
||||
expect(() => getPlayableNoteValue(happify(undefined))).toThrowError(`not a note: undefined`)
|
||||
})
|
||||
});
|
||||
@@ -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 { map, valued, mul } from '../value.mjs';
|
||||
import controls from '../controls.mjs';
|
||||
const { n } = controls;
|
||||
|
||||
describe('Value', () => {
|
||||
it('unionWith', () => {
|
||||
@@ -23,8 +21,4 @@ describe('Value', () => {
|
||||
expect(valued(mul).ap(3).ap(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 }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
let time;
|
||||
export function getTime() {
|
||||
if (!time) {
|
||||
throw new Error('no time set! use setTime to define a time source');
|
||||
}
|
||||
return time();
|
||||
}
|
||||
|
||||
export function setTime(func) {
|
||||
time = func;
|
||||
}
|
||||
@@ -18,11 +18,6 @@ export class TimeSpan {
|
||||
const end = this.end;
|
||||
const end_sam = end.sam();
|
||||
|
||||
// Support zero-width timespans
|
||||
if (begin.equals(end)) {
|
||||
return [new TimeSpan(begin, end)];
|
||||
}
|
||||
|
||||
while (end.gt(begin)) {
|
||||
// If begin and end are in the same cycle, we're done.
|
||||
if (begin.sam().equals(end_sam)) {
|
||||
@@ -110,7 +105,7 @@ export class TimeSpan {
|
||||
}
|
||||
|
||||
show() {
|
||||
return this.begin.show() + ' → ' + this.end.show();
|
||||
return this.begin.show() + ' -> ' + this.end.show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+25
-159
@@ -5,13 +5,12 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
// 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][#bsf]*[0-9]?$/.test(name);
|
||||
export const isNote = (name) => /^[a-gA-G][#b]*[0-9]$/.test(name);
|
||||
export const tokenizeNote = (note) => {
|
||||
if (typeof note !== 'string') {
|
||||
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) {
|
||||
return [];
|
||||
}
|
||||
@@ -19,80 +18,50 @@ export const tokenizeNote = (note) => {
|
||||
};
|
||||
|
||||
// turns the given note into its midi number representation
|
||||
export const noteToMidi = (note) => {
|
||||
const [pc, acc, oct = 3] = tokenizeNote(note);
|
||||
export const toMidi = (note) => {
|
||||
const [pc, acc, oct] = tokenizeNote(note);
|
||||
if (!pc) {
|
||||
throw new Error('not a note: "' + note + '"');
|
||||
}
|
||||
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 + { '#': 1, b: -1, s: 1, f: -1 }[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;
|
||||
};
|
||||
export const midiToFreq = (n) => {
|
||||
export const fromMidi = (n) => {
|
||||
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
|
||||
* @noAutocomplete
|
||||
*/
|
||||
export const getFreq = (noteOrMidi) => {
|
||||
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)
|
||||
* @noAutocomplete
|
||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||
*/
|
||||
export const midi2note = (n) => {
|
||||
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;
|
||||
};
|
||||
|
||||
// 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)
|
||||
export const _mod = (n, m) => ((n % m) + m) % m;
|
||||
// modulo that works with negative numbers e.g. mod(-1, 3) = 2
|
||||
// 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) => {
|
||||
let { value, context } = hap;
|
||||
let note = value;
|
||||
let { value: note, context } = hap;
|
||||
if (typeof note === 'object' && !Array.isArray(note)) {
|
||||
note = note.note || note.n || note.value;
|
||||
if (note === undefined) {
|
||||
throw new Error(`cannot find a playable note for ${JSON.stringify(value)}`);
|
||||
}
|
||||
}
|
||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||
note = midiToFreq(hap.value);
|
||||
note = fromMidi(hap.value);
|
||||
} else if (typeof note === 'number' && context.type === 'frequency') {
|
||||
note = hap.value; // legacy workaround.. will be removed in the future
|
||||
} else if (typeof note !== 'string' || !isNote(note)) {
|
||||
@@ -104,16 +73,13 @@ export const getPlayableNoteValue = (hap) => {
|
||||
export const getFrequency = (hap) => {
|
||||
let { value, context } = hap;
|
||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||
if (typeof value === 'object') {
|
||||
if (value.freq) {
|
||||
return value.freq;
|
||||
}
|
||||
return getFreq(value.note || value.n || value.value);
|
||||
if (typeof value === 'object' && value.freq) {
|
||||
return value.freq;
|
||||
}
|
||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||
value = midiToFreq(hap.value);
|
||||
value = fromMidi(hap.value);
|
||||
} else if (typeof value === 'string' && isNote(value)) {
|
||||
value = midiToFreq(noteToMidi(hap.value));
|
||||
value = fromMidi(toMidi(hap.value));
|
||||
} else if (typeof value !== 'number') {
|
||||
throw new Error('not a note or frequency: ' + value);
|
||||
}
|
||||
@@ -144,9 +110,9 @@ export const constant = (a, b) => a;
|
||||
|
||||
export const listRange = (min, max) => Array.from({ length: max - min + 1 }, (_, i) => i + min);
|
||||
|
||||
export function curry(func, overload, arity = func.length) {
|
||||
export function curry(func, overload) {
|
||||
const fn = function curried(...args) {
|
||||
if (args.length >= arity) {
|
||||
if (args.length >= func.length) {
|
||||
return func.apply(this, args);
|
||||
} else {
|
||||
const partial = function (...args2) {
|
||||
@@ -165,109 +131,9 @@ export function curry(func, overload, arity = func.length) {
|
||||
return fn;
|
||||
}
|
||||
|
||||
export function parseNumeral(numOrString) {
|
||||
const asNumber = Number(numOrString);
|
||||
if (!isNaN(asNumber)) {
|
||||
return asNumber;
|
||||
export function objectify(value) {
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value; // do nothing if its already an object
|
||||
}
|
||||
if (isNote(numOrString)) {
|
||||
return noteToMidi(numOrString);
|
||||
}
|
||||
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
||||
return { value };
|
||||
}
|
||||
|
||||
export function mapArgs(fn, mapFn) {
|
||||
return (...args) => fn(...args.map(mapFn));
|
||||
}
|
||||
|
||||
export function numeralArgs(fn) {
|
||||
return mapArgs(fn, parseNumeral);
|
||||
}
|
||||
|
||||
export function parseFractional(numOrString) {
|
||||
const asNumber = Number(numOrString);
|
||||
if (!isNaN(asNumber)) {
|
||||
return asNumber;
|
||||
}
|
||||
const specialValue = {
|
||||
pi: Math.PI,
|
||||
w: 1,
|
||||
h: 0.5,
|
||||
q: 0.25,
|
||||
e: 0.125,
|
||||
s: 0.0625,
|
||||
t: 1 / 3,
|
||||
f: 0.2,
|
||||
x: 1 / 6,
|
||||
}[numOrString];
|
||||
if (typeof specialValue !== 'undefined') {
|
||||
return specialValue;
|
||||
}
|
||||
throw new Error(`cannot parse as fractional: "${numOrString}"`);
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
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));
|
||||
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',
|
||||
},
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
// will move to https://github.com/felixroos/zyklus
|
||||
// TODO: started flag
|
||||
|
||||
function createClock(
|
||||
getTime,
|
||||
callback, // called slightly before each cycle
|
||||
duration = 0.05, // duration of each cycle
|
||||
interval = 0.1, // interval between callbacks
|
||||
overlap = 0.1, // overlap between callbacks
|
||||
) {
|
||||
let tick = 0; // counts callbacks
|
||||
let phase = 0; // next callback time
|
||||
let precision = 10 ** 4; // used to round phase
|
||||
let minLatency = 0.01;
|
||||
const setDuration = (setter) => (duration = setter(duration));
|
||||
overlap = overlap || interval / 2;
|
||||
const onTick = () => {
|
||||
const t = getTime();
|
||||
const lookahead = t + interval + overlap; // the time window for this tick
|
||||
if (phase === 0) {
|
||||
phase = t + minLatency;
|
||||
}
|
||||
// callback as long as we're inside the lookahead
|
||||
while (phase < lookahead) {
|
||||
phase = Math.round(phase * precision) / precision;
|
||||
phase >= t && callback(phase, duration, tick);
|
||||
phase < t && console.log('TOO LATE', phase); // what if latency is added from outside?
|
||||
phase += duration; // increment phase by duration
|
||||
tick++;
|
||||
}
|
||||
};
|
||||
let intervalID;
|
||||
const start = () => {
|
||||
clear(); // just in case start was called more than once
|
||||
onTick();
|
||||
intervalID = setInterval(onTick, interval * 1000);
|
||||
};
|
||||
const clear = () => intervalID !== undefined && clearInterval(intervalID);
|
||||
const pause = () => clear();
|
||||
const stop = () => {
|
||||
tick = 0;
|
||||
phase = 0;
|
||||
clear();
|
||||
};
|
||||
const getPhase = () => phase;
|
||||
// setCallback
|
||||
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
|
||||
}
|
||||
export default createClock;
|
||||
@@ -1,169 +0,0 @@
|
||||
import { getFrequency, logger, register } from '@strudel.cycles/core';
|
||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||
import csd from './project.csd?raw';
|
||||
// import livecodeOrc from './livecode.orc?raw';
|
||||
import presetsOrc from './presets.orc?raw';
|
||||
|
||||
let csoundLoader, _csound;
|
||||
|
||||
// initializes csound + can be used to reevaluate given instrument code
|
||||
export async function loadCSound(code = '') {
|
||||
await init();
|
||||
if (code) {
|
||||
code = `${code}`;
|
||||
// ^ ^
|
||||
// wrapping in backticks makes sure it works when calling as templated function
|
||||
await _csound?.evalCode(code);
|
||||
}
|
||||
}
|
||||
export const loadcsound = loadCSound;
|
||||
export const loadCsound = loadCSound;
|
||||
|
||||
export const csound = register('csound', (instrument, pat) => {
|
||||
instrument = instrument || 'triangle';
|
||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||
// TODO: find a alternative way to wait for csound to load (to wait with first time playback)
|
||||
return pat.onTrigger((time, hap) => {
|
||||
if (!_csound) {
|
||||
logger('[csound] not loaded yet', 'warning');
|
||||
return;
|
||||
}
|
||||
hap.ensureObjectValue();
|
||||
let { gain = 0.8 } = hap.value;
|
||||
gain *= 0.2;
|
||||
|
||||
const freq = Math.round(getFrequency(hap));
|
||||
const controls = Object.entries({ ...hap.value, freq })
|
||||
.flat()
|
||||
.join('/');
|
||||
// TODO: find out how to send a precise ctx based time
|
||||
// http://www.csounds.com/manual/html/i.html
|
||||
const params = [
|
||||
`"${instrument}"`, // p1: instrument name
|
||||
time - getAudioContext().currentTime, //.toFixed(precision), // p2: starting time in arbitrary unit called beats
|
||||
hap.duration + 0, // p3: duration in beats
|
||||
// instrument specific params:
|
||||
freq, //.toFixed(precision), // p4: frequency
|
||||
gain, // p5: gain
|
||||
`"${controls}"`, // p6 controls as string (like superdirt osc message)
|
||||
];
|
||||
const msg = `i ${params.join(' ')}`;
|
||||
_csound.inputMessage(msg);
|
||||
});
|
||||
});
|
||||
|
||||
function eventLogger(type, args) {
|
||||
const [msg] = args;
|
||||
if (
|
||||
type === 'message' &&
|
||||
(['[commit: HEAD]'].includes(msg) ||
|
||||
msg.startsWith('--Csound version') ||
|
||||
msg.startsWith('libsndfile') ||
|
||||
msg.startsWith('sr =') ||
|
||||
msg.startsWith('0dBFS') ||
|
||||
msg.startsWith('audio buffered') ||
|
||||
msg.startsWith('writing') ||
|
||||
msg.startsWith('SECTION 1:'))
|
||||
) {
|
||||
// ignore
|
||||
return;
|
||||
}
|
||||
let logType = 'info';
|
||||
if (msg.startsWith('error:')) {
|
||||
logType = 'error';
|
||||
}
|
||||
logger(`[csound] ${msg || ''}`, logType);
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (window.__csound__) {
|
||||
// Allows using some other csound instance.
|
||||
// In that case, the external Csound is responsible
|
||||
// for compiling an orchestra and starting to perform.
|
||||
logger('[load] Using external Csound', 'warning');
|
||||
_csound = window.__csound__;
|
||||
return _csound;
|
||||
} else {
|
||||
const { Csound } = await import('@csound/browser');
|
||||
_csound = await Csound({ audioContext: getAudioContext() });
|
||||
_csound.removeAllListeners('message');
|
||||
['message'].forEach((k) => _csound.on(k, (...args) => eventLogger(k, args)));
|
||||
await _csound.setOption('-m0d'); // see -m flag https://csound.com/docs/manual/CommandFlags.html
|
||||
await _csound.setOption('--sample-accurate');
|
||||
await _csound.setOption('-odac');
|
||||
await _csound.compileCsdText(csd);
|
||||
// await _csound.compileOrc(livecodeOrc);
|
||||
await _csound.compileOrc(presetsOrc);
|
||||
await _csound.start();
|
||||
return _csound;
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
csoundLoader = csoundLoader || load();
|
||||
return csoundLoader;
|
||||
}
|
||||
|
||||
let orcCache = {};
|
||||
export async function loadOrc(url) {
|
||||
await init();
|
||||
if (typeof url !== 'string') {
|
||||
throw new Error('loadOrc: expected url string');
|
||||
}
|
||||
if (url.startsWith('github:')) {
|
||||
const [_, path] = url.split('github:');
|
||||
url = `https://raw.githubusercontent.com/${path}`;
|
||||
}
|
||||
if (!orcCache[url]) {
|
||||
orcCache[url] = fetch(url)
|
||||
.then((res) => res.text())
|
||||
.then((code) => _csound.compileOrc(code));
|
||||
}
|
||||
await orcCache[url];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends notes to Csound for rendering with MIDI semantics. The hap value is
|
||||
* translated to these Csound pfields:
|
||||
*
|
||||
* p1 -- Csound instrument either as a number (1-based, can be a fraction),
|
||||
* or as a string name.
|
||||
* p2 -- time in beats (usually seconds) from start of performance.
|
||||
* p3 -- duration in beats (usually seconds).
|
||||
* p4 -- MIDI key number (as a real number, not an integer but in [0, 127].
|
||||
* p5 -- MIDI velocity (as a real number, not an integer but in [0, 127].
|
||||
* p6 -- Strudel controls, as a string.
|
||||
*/
|
||||
export const csoundm = register('csoundm', (instrument, pat) => {
|
||||
let p1 = instrument;
|
||||
if (typeof instrument === 'string') {
|
||||
p1 = `"{instrument}"`;
|
||||
}
|
||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||
return pat.onTrigger((tidal_time, hap) => {
|
||||
if (!_csound) {
|
||||
logger('[csound] not loaded yet', 'warning');
|
||||
return;
|
||||
}
|
||||
if (typeof hap.value !== 'object') {
|
||||
throw new Error('csound only support objects as hap values');
|
||||
}
|
||||
// Time in seconds counting from now.
|
||||
const p2 = tidal_time - getAudioContext().currentTime;
|
||||
const p3 = hap.duration.valueOf() + 0;
|
||||
const frequency = getFrequency(hap);
|
||||
// Translate frequency to MIDI key number _without_ rounding.
|
||||
const C4 = 261.62558;
|
||||
let octave = Math.log(frequency / C4) / Math.log(2.0) + 8.0;
|
||||
const p4 = octave * 12.0 - 36.0;
|
||||
// We prefer floating point precision, but over the MIDI range [0, 127].
|
||||
const p5 = 127 * (hap.context?.velocity ?? 0.9);
|
||||
// The Strudel controls as a string.
|
||||
const p6 = Object.entries({ ...hap.value, frequency })
|
||||
.flat()
|
||||
.join('/');
|
||||
const i_statement = `i ${p1} ${p2} ${p3} ${p4} ${p5} "${p6}"`;
|
||||
console.log('[csoundm]:', i_statement);
|
||||
_csound.inputMessage(i_statement);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"name": "@strudel.cycles/csound",
|
||||
"version": "0.8.0",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"contributors": [
|
||||
"Alex McLean <alex@slab.org>"
|
||||
],
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@csound/browser": "6.18.7",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3"
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
; returns value of given key in given "string map"
|
||||
; keymap("freq", "note/c3/freq/220/gain/0.5")
|
||||
; yields "220"
|
||||
opcode keymap, S, SS
|
||||
Skey, Smap xin
|
||||
idelimiter = strindex(Smap, strcat(Skey, "/"))
|
||||
ifrom = idelimiter + strlen(Skey) + 1
|
||||
Svalue = strsub(Smap, ifrom, strlen(Smap))
|
||||
Svalue = strsub(Svalue, 0, strindex(Svalue, "/"))
|
||||
xout Svalue
|
||||
endop
|
||||
|
||||
; TODO add incredibly dope synths
|
||||
instr organ
|
||||
iduration = p3
|
||||
ifreq = p4
|
||||
igain = p5
|
||||
ioct = octcps(ifreq)
|
||||
|
||||
asig = vco2(igain, ifreq, 12, .5) ; my edit
|
||||
kpwm = oscili(.1, 5)
|
||||
asig = vco2(igain, ifreq, 4, .5 + kpwm)
|
||||
asig += vco2(igain/4, ifreq * 2)
|
||||
|
||||
; filter
|
||||
; idepth = 2
|
||||
; acut = transegr:a(0, .005, 0, idepth, .06, -4.2, 0.001, .01, -4.2, 0) ; filter envelope
|
||||
; asig = zdf_2pole(asig, cpsoct(ioct + acut), 0.5)
|
||||
|
||||
; amp envelope
|
||||
iattack = .001
|
||||
irelease = .05
|
||||
asig *= linsegr:a(0, iattack, 1, iduration, 1, irelease, 0)
|
||||
|
||||
out(asig, asig)
|
||||
|
||||
endin
|
||||
|
||||
instr triangle
|
||||
iduration = p3
|
||||
ifreq = p4
|
||||
igain = p5
|
||||
ioct = octcps(ifreq)
|
||||
|
||||
asig = vco2(igain, ifreq, 12, .5)
|
||||
|
||||
; amp envelope
|
||||
iattack = .001
|
||||
irelease = .05
|
||||
asig *= linsegr:a(0, iattack, 1, iduration, 1, irelease, 0)
|
||||
|
||||
out(asig, asig)
|
||||
endin
|
||||
|
||||
instr pad
|
||||
iduration = p3
|
||||
ifreq = p4
|
||||
igain = p5
|
||||
ioct = octcps(ifreq)
|
||||
|
||||
asig = vco2(igain, ifreq, 0)
|
||||
|
||||
; amp envelope
|
||||
iattack = .5
|
||||
irelease = .1
|
||||
asig *= linsegr:a(0, iattack, 1, iduration, 1, irelease, 0)
|
||||
|
||||
idepth = 2
|
||||
acut = transegr:a(0, .005, 0, idepth, .06, -4.2, 0.001, .01, -4.2, 0)
|
||||
asig = zdf_2pole(asig, 1000, 2)
|
||||
|
||||
out(asig, asig)
|
||||
endin
|
||||
|
||||
|
||||
gisine ftgen 0, 0, 4096, 10, 1
|
||||
|
||||
instr bow
|
||||
kpres = 2
|
||||
krat = 0.16
|
||||
kvibf = 6.12723
|
||||
|
||||
kvib linseg 0, 0.5, 0, 1, 1, p3-0.5, 1
|
||||
kvamp = kvib * 0.01
|
||||
asig wgbow .7, p4, kpres, krat, kvibf, kvamp, gisine
|
||||
asig = asig*p5
|
||||
outs asig, asig
|
||||
endin
|
||||
|
||||
|
||||
instr Meta
|
||||
Smap = strget(p6)
|
||||
Sinstrument = keymap("s", Smap)
|
||||
schedule(Sinstrument, 0, p3, p4, p5)
|
||||
; TODO find a way to pipe Sinstrument through effects
|
||||
endin
|
||||
@@ -1,10 +0,0 @@
|
||||
<CsoundSynthesizer>
|
||||
<CsInstruments>
|
||||
|
||||
sr=48000
|
||||
ksmps=64
|
||||
nchnls=2
|
||||
0dbfs=1
|
||||
|
||||
</CsInstruments>
|
||||
</CsoundSynthesizer>
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -10,22 +10,15 @@ Either install with `npm i @strudel.cycles/embed` or just use a cdn to import th
|
||||
<script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
|
||||
<strudel-repl>
|
||||
<!--
|
||||
note(`[[e5 [b4 c5] d5 [c5 b4]]
|
||||
[a4 [a4 c5] e5 [d5 c5]]
|
||||
[b4 [~ c5] d5 e5]
|
||||
[c5 a4 a4 ~]
|
||||
[[~ d5] [~ f5] a5 [g5 f5]]
|
||||
[e5 [~ c5] e5 [d5 c5]]
|
||||
[b4 [b4 c5] d5 e5]
|
||||
[c5 a4 a4 ~]],
|
||||
[[e2 e3]*4]
|
||||
[[a2 a3]*4]
|
||||
[[g#2 g#3]*2 [e2 e3]*2]
|
||||
[a2 a3 a2 a3 a2 a3 b1 c2]
|
||||
[[d2 d3]*4]
|
||||
[[c2 c3]*4]
|
||||
[[b1 b2]*2 [e2 e3]*2]
|
||||
[[a1 a2]*4]`).slow(16)
|
||||
"a4 [a3 c3] a3 c3".color('#F9D649')
|
||||
.sub("<7 12 5 12>".slow(2))
|
||||
.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928"))
|
||||
.off(1/8,x=>x.add(12).color('#215CB6'))
|
||||
.slow(2)
|
||||
.legato(sine.range(0.3, 2).slow(28))
|
||||
.wave("sawtooth square".fast(2))
|
||||
.filter('lowpass', cosine.range(500,4000).slow(16))
|
||||
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
|
||||
-->
|
||||
</strudel-repl>
|
||||
```
|
||||
|
||||
@@ -2,21 +2,14 @@
|
||||
<!-- <script src="./embed.js"></script> -->
|
||||
<strudel-repl>
|
||||
<!--
|
||||
note(`[[e5 [b4 c5] d5 [c5 b4]]
|
||||
[a4 [a4 c5] e5 [d5 c5]]
|
||||
[b4 [~ c5] d5 e5]
|
||||
[c5 a4 a4 ~]
|
||||
[[~ d5] [~ f5] a5 [g5 f5]]
|
||||
[e5 [~ c5] e5 [d5 c5]]
|
||||
[b4 [b4 c5] d5 e5]
|
||||
[c5 a4 a4 ~]],
|
||||
[[e2 e3]*4]
|
||||
[[a2 a3]*4]
|
||||
[[g#2 g#3]*2 [e2 e3]*2]
|
||||
[a2 a3 a2 a3 a2 a3 b1 c2]
|
||||
[[d2 d3]*4]
|
||||
[[c2 c3]*4]
|
||||
[[b1 b2]*2 [e2 e3]*2]
|
||||
[[a1 a2]*4]`).slow(16)
|
||||
"a4 [a3 c3] a3 c3".color('#F9D649')
|
||||
.sub("<7 12 5 12>".slow(2))
|
||||
.off(1/4,x=>x.add(7).color("#FFFFFF #0C3AA1 #C63928"))
|
||||
.off(1/8,x=>x.add(12).color('#215CB6'))
|
||||
.slow(2)
|
||||
.legato(sine.range(0.3, 2).slow(28))
|
||||
.wave("sawtooth square".fast(2))
|
||||
.filter('lowpass', cosine.range(500,4000).slow(16))
|
||||
.pianoroll({minMidi:20,maxMidi:120,background:'#202124'})
|
||||
-->
|
||||
</strudel-repl>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strudel.cycles/embed",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
||||
"main": "embed.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
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.
|
||||
|
||||
## Deprecation Note
|
||||
|
||||
This package will not be developed further. Consider using `@strudel.cycles/transpiler` as a replacement.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
@@ -15,9 +11,10 @@ npm i @strudel.cycles/eval --save
|
||||
|
||||
## Example
|
||||
|
||||
<!-- TODO: -extend +evalScope -->
|
||||
|
||||
```js
|
||||
import { evalScope } from '@strudel.cycles/core';
|
||||
import { evaluate } from '@strudel.cycles/eval';
|
||||
import { evaluate, extend } from '@strudel.cycles/eval';
|
||||
|
||||
evalScope(
|
||||
import('@strudel.cycles/core'),
|
||||
|
||||
@@ -4,9 +4,46 @@ 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/>.
|
||||
*/
|
||||
|
||||
import { evaluate as _evaluate } from '@strudel.cycles/core';
|
||||
import shapeshifter from './shapeshifter.mjs';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
const { isPattern, Pattern } = strudel;
|
||||
|
||||
export const extend = (...args) => {
|
||||
console.warn('@strudel.cycles/eval extend is deprecated, please use evalScope instead');
|
||||
Object.assign(globalThis, ...args);
|
||||
};
|
||||
|
||||
let scoped = false;
|
||||
export const evalScope = async (...args) => {
|
||||
if (scoped) {
|
||||
console.warn('@strudel.cycles/eval evalScope was called more than once.');
|
||||
}
|
||||
scoped = true;
|
||||
const results = await Promise.allSettled(args);
|
||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||
results.forEach((result, i) => {
|
||||
if (result.status === 'rejected') {
|
||||
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
|
||||
}
|
||||
});
|
||||
Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap());
|
||||
};
|
||||
|
||||
function safeEval(str) {
|
||||
return Function('"use strict";return (' + str + ')')();
|
||||
}
|
||||
|
||||
export const evaluate = async (code) => {
|
||||
return _evaluate(code, shapeshifter);
|
||||
if (!scoped) {
|
||||
await evalScope(); // at least scope Pattern.prototype.boostrap
|
||||
}
|
||||
const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code
|
||||
let evaluated = await safeEval(shapeshifted);
|
||||
if (!isPattern(evaluated)) {
|
||||
console.log('evaluated', evaluated);
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||
}
|
||||
return { mode: 'javascript', pattern: evaluated };
|
||||
};
|
||||
|
||||
Generated
+297
@@ -0,0 +1,297 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.2.0",
|
||||
"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,21 +1,15 @@
|
||||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.8.0",
|
||||
"version": "0.2.0",
|
||||
"description": "Code evaluator for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "vitest run",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"type": "module",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
@@ -34,17 +28,12 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/core": "^0.2.0",
|
||||
"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-ast": "^6.1.0",
|
||||
"shift-codegen": "^7.0.3",
|
||||
"shift-parser": "^7.0.3",
|
||||
"shift-spec": "^2018.0.2",
|
||||
"shift-traverser": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@strudel.cycles/mini": "workspace:*",
|
||||
"vite": "^4.3.3",
|
||||
"vitest": "^0.28.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,7 @@ 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 const wrappedAsync = true;
|
||||
|
||||
export default (_code) => {
|
||||
const { code, addReturn } = wrapAsync(_code);
|
||||
@@ -126,7 +125,7 @@ export default (_code) => {
|
||||
},
|
||||
});
|
||||
// add return to last statement (because it's wrapped in an async function artificially)
|
||||
if (shouldAddReturn) {
|
||||
if (wrappedAsync) {
|
||||
addReturn(shifted);
|
||||
}
|
||||
const generated = undisguiseImports(codegen(shifted));
|
||||
@@ -154,7 +153,7 @@ ${code}
|
||||
})()`;
|
||||
}
|
||||
const addReturn = (ast) => {
|
||||
const body = wrappedAsync ? ast.statements[0].expression.callee.body : ast;
|
||||
const body = ast.statements[0].expression.callee.body; // actual code ast inside async function body
|
||||
body.statements = body.statements
|
||||
.slice(0, -1)
|
||||
.concat([new ReturnStatement({ expression: body.statements.slice(-1)[0] })]);
|
||||
@@ -225,8 +224,9 @@ function isPatternArg(parents) {
|
||||
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)
|
||||
return (
|
||||
parent?.type === 'StaticMemberExpression' && Object.keys(Pattern.prototype.composable).includes(parent.property)
|
||||
);
|
||||
}
|
||||
const factories = Object.keys(Pattern.prototype.factories).concat(['mini']);
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import { expect, describe, it } from 'vitest';
|
||||
|
||||
import { evaluate } from '../evaluate.mjs';
|
||||
import { evaluate, evalScope } from '../evaluate.mjs';
|
||||
import { mini } from '@strudel.cycles/mini';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
const { fastcat, evalScope } = strudel;
|
||||
const { fastcat } = strudel;
|
||||
|
||||
describe('evaluate', async () => {
|
||||
await evalScope({ mini }, strudel);
|
||||
const ev = async (code) => (await evaluate(code)).pattern.firstCycleValues;
|
||||
const ev = async (code) => (await evaluate(code)).pattern._firstCycleValues;
|
||||
it('Should evaluate strudel functions', async () => {
|
||||
expect(await ev('pure("c3")')).toEqual(['c3']);
|
||||
expect(await ev('cat("c3")')).toEqual(['c3']);
|
||||
|
||||
@@ -5,21 +5,15 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import shapeshifter, { wrappedAsync } from '../shapeshifter.mjs';
|
||||
import shapeshifter 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])');
|
||||
}
|
||||
expect(shapeshifter('"c3"')).toEqual('(async()=>{return mini("c3").withMiniLocation([1,0,15],[1,4,19])})()');
|
||||
});
|
||||
it('Should handle dynamic imports', () => {
|
||||
expect(shapeshifter('const { default: foo } = await import(\'https://bar.com/foo.js\');"c3"')).toEqual(
|
||||
'(async()=>{const{default:foo}=await import("https://bar.com/foo.js");return mini("c3").withMiniLocation([1,64,79],[1,68,83])})()',
|
||||
);
|
||||
});
|
||||
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])',
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -7,3 +7,7 @@ This package adds midi functionality to strudel Patterns.
|
||||
```sh
|
||||
npm i @strudel.cycles/midi --save
|
||||
```
|
||||
|
||||
## Dev Notes
|
||||
|
||||
- is this package really necessary? currently, /tone also depends on webmidi through @tonejs/piano. Either move piano out of /tone or merge /midi into /tone...
|
||||
|
||||
+37
-85
@@ -4,24 +4,13 @@ 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/>.
|
||||
*/
|
||||
|
||||
import * as _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern, logger } from '@strudel.cycles/core';
|
||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||
import { noteToMidi } from '@strudel.cycles/core';
|
||||
import _WebMidi from 'webmidi';
|
||||
import { Pattern, isPattern, isNote, getPlayableNoteValue, objectify } from '@strudel.cycles/core';
|
||||
|
||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||
export const { WebMidi } = _WebMidi;
|
||||
export const WebMidi = _WebMidi;
|
||||
|
||||
function supportsMidi() {
|
||||
return typeof navigator.requestMIDIAccess === 'function';
|
||||
}
|
||||
|
||||
export function enableWebMidi(options = {}) {
|
||||
const { onReady, onConnected, onDisconnected } = options;
|
||||
|
||||
if (!supportsMidi()) {
|
||||
throw new Error('Your Browser does not support WebMIDI.');
|
||||
}
|
||||
export function enableWebMidi() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (WebMidi.enabled) {
|
||||
// if already enabled, just resolve WebMidi
|
||||
@@ -32,14 +21,6 @@ export function enableWebMidi(options = {}) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
WebMidi.addListener('connected', (e) => {
|
||||
onConnected?.(WebMidi);
|
||||
});
|
||||
// Reacting when a device becomes unavailable
|
||||
WebMidi.addListener('disconnected', (e) => {
|
||||
onDisconnected?.(WebMidi, e);
|
||||
});
|
||||
onReady?.(WebMidi);
|
||||
resolve(WebMidi);
|
||||
});
|
||||
});
|
||||
@@ -47,54 +28,37 @@ export function enableWebMidi(options = {}) {
|
||||
// const outputByName = (name: string) => WebMidi.getOutputByName(name);
|
||||
const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||
|
||||
let midiReady;
|
||||
|
||||
// output?: string | number, outputs: typeof WebMidi.outputs
|
||||
function getDevice(output, outputs) {
|
||||
if (!outputs.length) {
|
||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||
}
|
||||
if (typeof output === 'number') {
|
||||
return outputs[output];
|
||||
}
|
||||
if (typeof output === 'string') {
|
||||
return outputByName(output);
|
||||
}
|
||||
return outputs[0];
|
||||
}
|
||||
|
||||
// Pattern.prototype.midi = function (output: string | number, channel = 1) {
|
||||
Pattern.prototype.midi = function (output) {
|
||||
if (!supportsMidi()) {
|
||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||
}
|
||||
/* await */ enableWebMidi({
|
||||
onConnected: ({ outputs }) =>
|
||||
logger(`Midi device connected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
||||
onDisconnected: ({ outputs }) =>
|
||||
logger(`Midi device disconnected! Available: ${outputs.map((o) => `'${o.name}'`).join(', ')}`),
|
||||
onReady: ({ outputs }) => {
|
||||
const device = getDevice(output, outputs);
|
||||
const otherOutputs = outputs
|
||||
.filter((o) => o.name !== device.name)
|
||||
.map((o) => `'${o.name}'`)
|
||||
.join(' | ');
|
||||
midiReady = true;
|
||||
logger(`Midi connected! Using "${device.name}". ${otherOutputs ? `Also available: ${otherOutputs}` : ''}`);
|
||||
},
|
||||
});
|
||||
if (isPattern(output)) {
|
||||
Pattern.prototype.midi = function (output, channel = 1) {
|
||||
if (output?.constructor?.name && isPattern(output?.constructor?.name)) {
|
||||
throw new Error(
|
||||
`.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'
|
||||
}')`,
|
||||
);
|
||||
}
|
||||
return this.onTrigger((time, hap) => {
|
||||
if (!midiReady) {
|
||||
return;
|
||||
return this.onTrigger((time, hap, currentTime) => {
|
||||
let note;
|
||||
try {
|
||||
note = getPlayableNoteValue(hap);
|
||||
} catch (err) {
|
||||
// dont care if no note is found as we can still send cvs
|
||||
}
|
||||
const { velocity, nrpnn, nrpv, ccn, ccv, legato, duration: durationValue } = objectify(hap.value);
|
||||
if (!WebMidi.enabled) {
|
||||
throw new Error(`🎹 WebMidi is not enabled. Supported Browsers: https://caniuse.com/?search=webmidi`);
|
||||
}
|
||||
if (!WebMidi.outputs.length) {
|
||||
throw new Error(`🔌 No MIDI devices found. Connect a device or enable IAC Driver.`);
|
||||
}
|
||||
let device;
|
||||
if (typeof output === 'number') {
|
||||
device = WebMidi.outputs[output];
|
||||
} else if (typeof output === 'string') {
|
||||
device = outputByName(output);
|
||||
} else {
|
||||
device = WebMidi.outputs[0];
|
||||
}
|
||||
const device = getDevice(output, WebMidi.outputs);
|
||||
if (!device) {
|
||||
throw new Error(
|
||||
`🔌 MIDI device '${output ? output : ''}' not found. Use one of ${WebMidi.outputs
|
||||
@@ -102,34 +66,22 @@ Pattern.prototype.midi = function (output) {
|
||||
.join(' | ')}`,
|
||||
);
|
||||
}
|
||||
hap.ensureObjectValue();
|
||||
|
||||
// calculate time
|
||||
const timingOffset = WebMidi.time - getAudioContext().getOutputTimestamp().contextTime * 1000;
|
||||
time = time * 1000 + timingOffset;
|
||||
|
||||
// destructure value
|
||||
const { note, nrpnn, nrpv, ccn, ccv, midichan = 1 } = hap.value;
|
||||
const velocity = hap.context?.velocity ?? 0.9; // TODO: refactor velocity
|
||||
const duration = hap.duration.valueOf() * 1000 - 5;
|
||||
// console.log('midi', value, output);
|
||||
const deadline = (time - currentTime) * 1000;
|
||||
time = WebMidi.time + deadline;
|
||||
/* const timingOffset = WebMidi.time - currentTime * 1000;
|
||||
time = time * 1000 + timingOffset; */
|
||||
const duration = (durationValue ?? hap.duration.valueOf()) * (legato ?? 1) * 1000 - 5;
|
||||
|
||||
if (note) {
|
||||
const midiNumber = noteToMidi(note);
|
||||
device.playNote(midiNumber, midichan, {
|
||||
device.playNote(note, channel, {
|
||||
time,
|
||||
duration,
|
||||
attack: velocity,
|
||||
velocity,
|
||||
});
|
||||
}
|
||||
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);
|
||||
device.sendControlChange(ccn, scaled, midichan, { time });
|
||||
if (ccn && ccv) {
|
||||
device.sendControlChange(ccn, ccv, channel, { time });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Generated
+130
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.2.0",
|
||||
"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",
|
||||
"version": "0.8.0",
|
||||
"version": "0.2.0",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
@@ -29,11 +21,8 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"webmidi": "^3.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.3"
|
||||
"@strudel.cycles/tone": "^0.2.0",
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^2.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
-10
@@ -32,13 +32,4 @@ yields:
|
||||
|
||||
## Mini Notation API
|
||||
|
||||
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/learn/mini-notation)
|
||||
|
||||
## Building the Parser
|
||||
|
||||
The parser [krill-parser.js] is generated from [krill.pegjs](./krill.pegjs) using [peggy](https://peggyjs.org/).
|
||||
To generate the parser, run
|
||||
|
||||
```js
|
||||
npm build:parser
|
||||
```
|
||||
See "Mini Notation" in the [Strudel Tutorial](https://strudel.tidalcycles.org/tutorial/)
|
||||
|
||||
+421
-546
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user